613 lines
21 KiB
HTML
613 lines
21 KiB
HTML
<!doctype html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Relatório Técnico — Sessão de Correção: Subscription Health & Entitlements (2026-03-01) | Agência PSI</title>
|
|
<style>
|
|
:root{
|
|
--bg0:#f6f8fc;
|
|
--bg1:#eef2f8;
|
|
--panel:rgba(255,255,255,.78);
|
|
--panel2:rgba(255,255,255,.92);
|
|
--border:rgba(15,23,42,.10);
|
|
--text:rgba(15,23,42,.92);
|
|
--muted:rgba(15,23,42,.70);
|
|
--muted2:rgba(15,23,42,.56);
|
|
--accent:#2563eb;
|
|
--accent2:#4f46e5;
|
|
--warn:#b45309;
|
|
--danger:#b91c1c;
|
|
--ok:#047857;
|
|
--shadow: 0 18px 60px rgba(2,6,23,.10);
|
|
--radius: 16px;
|
|
--radius2: 22px;
|
|
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
}
|
|
*{box-sizing:border-box;}
|
|
body{
|
|
margin:0;
|
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
|
background: radial-gradient(1200px 600px at 10% 0%, rgba(79,70,229,.10), transparent 55%),
|
|
radial-gradient(1100px 500px at 90% 10%, rgba(37,99,235,.10), transparent 55%),
|
|
linear-gradient(180deg, var(--bg0), var(--bg1));
|
|
color:var(--text);
|
|
}
|
|
a{color:inherit; text-decoration:none;}
|
|
a:hover{text-decoration:underline;}
|
|
.layout{
|
|
display:grid;
|
|
grid-template-columns: 320px 1fr;
|
|
gap: 20px;
|
|
max-width: 1320px;
|
|
margin: 0 auto;
|
|
padding: 28px 18px 42px;
|
|
}
|
|
header{
|
|
grid-column: 1 / -1;
|
|
padding: 18px;
|
|
border: 1px solid var(--border);
|
|
background: linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,.72));
|
|
border-radius: var(--radius2);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.kicker{
|
|
font-size:12px;
|
|
letter-spacing:.08em;
|
|
text-transform:uppercase;
|
|
color:var(--muted2);
|
|
margin:0 0 8px;
|
|
}
|
|
h1{
|
|
margin:0 0 8px;
|
|
font-size:30px;
|
|
letter-spacing:-0.02em;
|
|
}
|
|
.subtitle{
|
|
margin:0;
|
|
color:var(--muted);
|
|
max-width:980px;
|
|
line-height:1.55;
|
|
font-size:14px;
|
|
}
|
|
.meta-row{
|
|
margin-top:12px;
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
gap:10px;
|
|
align-items:center;
|
|
}
|
|
.pill{
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:8px;
|
|
padding:8px 12px;
|
|
border-radius:999px;
|
|
border:1px solid var(--border);
|
|
background:rgba(255,255,255,.72);
|
|
color:var(--muted);
|
|
font-size:12px;
|
|
}
|
|
.dot{
|
|
width:8px;height:8px;border-radius:50%;
|
|
background:var(--accent);
|
|
box-shadow:0 0 0 4px rgba(37,99,235,.12);
|
|
}
|
|
aside{
|
|
position:sticky;
|
|
top:18px;
|
|
align-self:start;
|
|
border:1px solid var(--border);
|
|
background:var(--panel2);
|
|
border-radius:var(--radius);
|
|
box-shadow:var(--shadow);
|
|
overflow:hidden;
|
|
}
|
|
.toc-head{
|
|
padding:14px;
|
|
border-bottom:1px solid var(--border);
|
|
background:rgba(15,23,42,.02);
|
|
}
|
|
.toc-title{ margin:0 0 6px; font-weight:700; font-size:14px; }
|
|
.toc-sub{ margin:0; color:var(--muted); font-size:12px; line-height:1.45; }
|
|
nav{ padding: 10px 6px 14px; }
|
|
nav a{
|
|
display:block;
|
|
padding:10px 12px;
|
|
margin:4px 6px;
|
|
border-radius:12px;
|
|
color:var(--muted);
|
|
font-size:13px;
|
|
border:1px solid transparent;
|
|
}
|
|
nav a:hover{
|
|
background:rgba(37,99,235,.06);
|
|
border-color:rgba(37,99,235,.12);
|
|
color:var(--text);
|
|
text-decoration:none;
|
|
}
|
|
.nav-sec{
|
|
margin:10px 12px 6px;
|
|
color:var(--muted2);
|
|
font-size:11px;
|
|
letter-spacing:.08em;
|
|
text-transform:uppercase;
|
|
}
|
|
main{
|
|
border:1px solid var(--border);
|
|
background:var(--panel);
|
|
border-radius:var(--radius2);
|
|
box-shadow:var(--shadow);
|
|
overflow:hidden;
|
|
}
|
|
.content{ padding: 18px 18px 22px; }
|
|
.section{
|
|
padding: 18px;
|
|
border:1px solid var(--border);
|
|
border-radius: var(--radius2);
|
|
background: rgba(255,255,255,.80);
|
|
margin-bottom: 16px;
|
|
}
|
|
.section h2{
|
|
margin:0 0 10px;
|
|
font-size:18px;
|
|
letter-spacing:-0.01em;
|
|
}
|
|
.section h3{
|
|
margin:14px 0 8px;
|
|
font-size:14px;
|
|
}
|
|
.section p, .section li{
|
|
color:var(--muted);
|
|
line-height:1.65;
|
|
font-size:13.5px;
|
|
}
|
|
.grid{
|
|
display:grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
@media (max-width: 980px){
|
|
.layout{ grid-template-columns: 1fr; }
|
|
aside{ position:relative; top:auto; }
|
|
.grid{ grid-template-columns: 1fr; }
|
|
}
|
|
.callout{
|
|
border-radius: 16px;
|
|
padding: 12px 12px 12px 14px;
|
|
border:1px solid var(--border);
|
|
background: rgba(15,23,42,.02);
|
|
margin-top: 10px;
|
|
}
|
|
.callout strong{color:var(--text);}
|
|
.callout.ok{ border-left: 4px solid var(--ok); background: rgba(4,120,87,.06); }
|
|
.callout.warn{ border-left: 4px solid var(--warn); background: rgba(180,83,9,.08); }
|
|
.callout.danger{ border-left: 4px solid var(--danger); background: rgba(185,28,28,.08); }
|
|
.callout.info{ border-left: 4px solid var(--accent); background: rgba(37,99,235,.08); }
|
|
pre{
|
|
margin: 10px 0 0;
|
|
padding: 14px;
|
|
background: #0b1220;
|
|
color:#e2e8f0;
|
|
border-radius: 16px;
|
|
overflow:auto;
|
|
border: 1px solid rgba(226,232,240,.08);
|
|
}
|
|
code{ font-family: var(--mono); font-size:12.5px; }
|
|
.kbd{
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
padding: 2px 6px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
background: rgba(255,255,255,.65);
|
|
color: var(--text);
|
|
}
|
|
.hr{
|
|
height:1px;
|
|
background: var(--border);
|
|
margin: 12px 0;
|
|
}
|
|
footer{
|
|
margin-top: 12px;
|
|
padding: 14px 18px 18px;
|
|
color: var(--muted2);
|
|
font-size: 12px;
|
|
border-top: 1px solid var(--border);
|
|
background: rgba(255,255,255,.70);
|
|
}
|
|
.small{ font-size:12px; color:var(--muted2); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="layout">
|
|
<header>
|
|
<p class="kicker">Relatório Técnico • Billing Health • Agência PSI</p>
|
|
<h1>Subscription Health & Entitlements — Sessão de Correção (2026-03-01)</h1>
|
|
<p class="subtitle">Este documento registra, de forma minuciosa e operacional, a sessão de diagnóstico e correção dos problemas
|
|
na página <strong>Saúde das Assinaturas</strong> (Subscription Health) e no pipeline de <strong>Entitlements</strong>.
|
|
O objetivo é permitir que qualquer programador entenda o incidente, replique o diagnóstico e aplique correções
|
|
com segurança, mesmo sem ter acompanhado a conversa original.</p>
|
|
<div class="meta-row">
|
|
<span class="pill"><span class="dot"></span><strong>Estado:</strong> resolvido e hardenizado</span>
|
|
<span class="pill"><strong>Atualizado:</strong> 2026-03-01 11:46:44 UTC</span>
|
|
<span class="pill"><strong>Stack:</strong> Supabase + Postgres + Vue/PrimeVue</span>
|
|
</div>
|
|
</header>
|
|
|
|
<aside>
|
|
<div class="toc-head">
|
|
<div class="toc-title">Sumário</div>
|
|
<p class="toc-sub">Leitura rápida com passos reproduzíveis (SQL + patches + checklist).</p>
|
|
</div>
|
|
|
|
<nav>
|
|
<div class="nav-sec">Visão geral</div>
|
|
<a href="#01">Resumo executivo</a>
|
|
<a href="#02">Escopo e componentes</a>
|
|
<a href="#03">Sintomas e evidências</a>
|
|
<div class="nav-sec">Diagnóstico</div>
|
|
<a href="#04">Causa raiz</a>
|
|
<a href="#05">SQLs de diagnóstico</a>
|
|
<div class="nav-sec">Correções</div>
|
|
<a href="#06">Patches aplicados</a>
|
|
<a href="#07">Hardening</a>
|
|
<div class="nav-sec">Validação</div>
|
|
<a href="#08">Checklist pós-correção</a>
|
|
<div class="nav-sec">Contexto</div>
|
|
<a href="#09">Notas de front-end</a>
|
|
<a href="#10">Linha do tempo</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
<main>
|
|
<div class="content">
|
|
|
|
<section class="section" id="01">
|
|
<h2>1. Resumo executivo</h2>
|
|
|
|
<p>
|
|
<strong>Sintoma principal:</strong> a tela <em>Saúde das Assinaturas</em> exibia divergências e a coluna <strong>Owner</strong> aparecia vazia
|
|
(linhas com <code>owner_id = NULL</code>). Além disso, os botões <strong>Fix</strong> e <strong>Fix All</strong> falhavam.
|
|
</p>
|
|
<div class="callout danger">
|
|
<strong>Impacto:</strong> a ferramenta de diagnóstico do Billing ficou pouco confiável e os reparos automáticos não executavam, impedindo
|
|
correções rápidas após mudanças de plano/feature.
|
|
</div>
|
|
<div class="callout ok">
|
|
<strong>Resultado final:</strong> view de entitlements corrigida (filtra <code>subscriptions.status = 'active'</code> e exclui NULL),
|
|
funções RPC alinhadas ao schema atual (<code>subscriptions.user_id</code>), dados inválidos removidos e constraints/índices adicionados
|
|
para impedir regressões.
|
|
</div>
|
|
|
|
</section>
|
|
<section class="section" id="02">
|
|
<h2>2. Escopo e componentes envolvidos</h2>
|
|
|
|
<div class="grid">
|
|
<div class="callout">
|
|
<strong>View de saúde</strong>
|
|
<p><code>public.v_subscription_feature_mismatch</code> — compara o esperado (plan_features do plano ativo) com o atual (entitlements).</p>
|
|
</div>
|
|
<div class="callout">
|
|
<strong>Entitlements agregados</strong>
|
|
<p><code>public.owner_feature_entitlements</code> — <em>VIEW</em> agregada (sources + limits_list), derivada de <code>subscriptions</code> e <code>tenant_modules</code>.</p>
|
|
</div>
|
|
<div class="callout">
|
|
<strong>Rotinas de reparo</strong>
|
|
<p><code>public.rebuild_owner_entitlements(uuid)</code> e <code>public.fix_all_subscription_mismatches()</code>.</p>
|
|
</div>
|
|
<div class="callout">
|
|
<strong>Tabelas de configuração</strong>
|
|
<p><code>plans</code>, <code>features</code>, <code>plan_features</code>, <code>module_features</code>, <code>tenant_modules</code>, <code>subscriptions</code>.</p>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
<section class="section" id="03">
|
|
<h2>3. Sintomas observados e evidências</h2>
|
|
|
|
<p>Foram observados os seguintes indícios no banco:</p>
|
|
<ul>
|
|
<li>View <code>v_subscription_feature_mismatch</code> retornando <code>owner_id = NULL</code> tanto em <em>missing</em> quanto em <em>unexpected</em>.</li>
|
|
<li>Contagem estável em 4/4 divergências, mesmo após tentativa de reparo.</li>
|
|
<li>Existência de uma <code>subscription</code> com <code>status='active'</code> e <code>user_id = NULL</code> (dado inválido).</li>
|
|
<li>Falha de execução do FixAll com erro de coluna inexistente: <code>subscriptions.owner_id</code> (schema drift).</li>
|
|
</ul>
|
|
|
|
<div class="callout warn">
|
|
<strong>Nota de leitura:</strong> ao ver <code>owner_id = NULL</code> em divergências, trate como anomalia de dados ou view/joins permissivos.
|
|
Na prática, “owner nulo” não é um caso de negócio — é um caso de <em>integridade</em>.
|
|
</div>
|
|
|
|
</section>
|
|
<section class="section" id="04">
|
|
<h2>4. Diagnóstico e causa raiz</h2>
|
|
|
|
<h3>4.1 Causa raiz #1 — Schema drift nas funções RPC</h3>
|
|
<p>
|
|
As funções de reparo estavam escritas para um schema anterior, usando <code>subscriptions.owner_id</code>. No schema atual, o owner do contexto
|
|
“terapeuta” é <code>subscriptions.user_id</code>. Isso quebrou:
|
|
</p>
|
|
<ul>
|
|
<li><strong>Fix owner</strong>: falha ao buscar o plano ativo do owner.</li>
|
|
<li><strong>Fix all</strong>: falha ao iterar owners e chamar o rebuild.</li>
|
|
</ul>
|
|
|
|
<div class="callout danger">
|
|
<strong>Erro observado:</strong> <code>column "owner_id" does not exist</code> (hint citando <code>subscriptions.user_id</code>).
|
|
</div>
|
|
|
|
<h3>4.2 Causa raiz #2 — View de entitlements agregados não filtrava status</h3>
|
|
<p>
|
|
A view <code>owner_feature_entitlements</code> agregava a fonte “plan” sem filtrar <code>subscriptions.status</code>, permitindo que uma subscription
|
|
<em>inactive</em> com <code>user_id NULL</code> continuasse “vazando” entitlements com <code>owner_id NULL</code> para o sistema.
|
|
</p>
|
|
|
|
<h3>4.3 Causa raiz #3 — Dado inválido</h3>
|
|
<p>
|
|
Foi identificado um registro em <code>subscriptions</code> com <code>user_id NULL</code>. Mesmo após torná-lo <em>inactive</em>, ele continuava contaminando
|
|
a view (por ausência do filtro de status).
|
|
</p>
|
|
|
|
</section>
|
|
<section class="section" id="05">
|
|
<h2>5. SQLs usados no diagnóstico (playbook)</h2>
|
|
|
|
<p>Use este bloco para reproduzir o diagnóstico com segurança.</p>
|
|
|
|
<h3>5.1 Ver divergências e amostras</h3>
|
|
<pre><code>select mismatch_type, count(*) as qtd
|
|
from public.v_subscription_feature_mismatch
|
|
group by 1
|
|
order by 2 desc;
|
|
|
|
select owner_id, feature_key, mismatch_type
|
|
from public.v_subscription_feature_mismatch
|
|
order by owner_id nulls first, feature_key
|
|
limit 50;</code></pre>
|
|
|
|
<h3>5.2 Encontrar subscriptions inválidas (user_id nulo)</h3>
|
|
<pre><code>select id, user_id, plan_id, status, created_at
|
|
from public.subscriptions
|
|
where user_id is null
|
|
order by created_at desc;</code></pre>
|
|
|
|
<h3>5.3 Entender a origem dos entitlements agregados</h3>
|
|
<pre><code>select pg_get_viewdef('public.owner_feature_entitlements'::regclass, true) as view_sql;</code></pre>
|
|
|
|
<h3>5.4 Verificar tenant_modules inválidos (owner_id nulo)</h3>
|
|
<pre><code>select count(*) as qtd
|
|
from public.tenant_modules
|
|
where status = 'active' and owner_id is null;</code></pre>
|
|
|
|
</section>
|
|
<section class="section" id="06">
|
|
<h2>6. Correções aplicadas no banco (patches)</h2>
|
|
|
|
<h3>6.1 Patch: rebuild_owner_entitlements (owner = subscriptions.user_id)</h3>
|
|
<p>
|
|
Ajuste para buscar o plano ativo por <code>subscriptions.user_id</code> e reconstruir entitlements com base em <code>plan_features</code>.
|
|
</p>
|
|
<pre><code>create or replace function public.rebuild_owner_entitlements(p_owner_id uuid)
|
|
returns void
|
|
language plpgsql
|
|
security definer
|
|
as $$
|
|
declare
|
|
v_plan_id uuid;
|
|
begin
|
|
select s.plan_id
|
|
into v_plan_id
|
|
from public.subscriptions s
|
|
where s.user_id = p_owner_id
|
|
and s.status = 'active'
|
|
order by s.created_at desc
|
|
limit 1;
|
|
|
|
delete from public.owner_feature_entitlements e
|
|
where e.owner_id = p_owner_id;
|
|
|
|
if v_plan_id is null then
|
|
return;
|
|
end if;
|
|
|
|
insert into public.owner_feature_entitlements (owner_id, feature_key, sources, limits_list)
|
|
select
|
|
p_owner_id,
|
|
f.key,
|
|
array['plan'::text],
|
|
'{}'::jsonb
|
|
from public.plan_features pf
|
|
join public.features f on f.id = pf.feature_id
|
|
where pf.plan_id = v_plan_id;
|
|
end;
|
|
$$;</code></pre>
|
|
|
|
<div class="callout warn">
|
|
<strong>Importante:</strong> se <code>owner_feature_entitlements</code> for uma <em>VIEW</em> (como no ambiente desta sessão),
|
|
o <code>DELETE/INSERT</code> acima deve ser direcionado à <em>tabela base</em> real de entitlements, se existir.
|
|
Nesta sessão, a correção definitiva foi feita ajustando a view agregadora e limpando o dado inválido.
|
|
</div>
|
|
|
|
<h3>6.2 Patch: fix_all_subscription_mismatches (itera subscriptions.user_id)</h3>
|
|
<pre><code>create or replace function public.fix_all_subscription_mismatches()
|
|
returns void
|
|
language plpgsql
|
|
security definer
|
|
as $$
|
|
declare
|
|
r record;
|
|
begin
|
|
for r in
|
|
select distinct s.user_id as owner_id
|
|
from public.subscriptions s
|
|
where s.status = 'active'
|
|
and s.user_id is not null
|
|
loop
|
|
perform public.rebuild_owner_entitlements(r.owner_id);
|
|
end loop;
|
|
end;
|
|
$$;</code></pre>
|
|
|
|
<h3>6.3 Patch: owner_feature_entitlements (filtra status e NULLs)</h3>
|
|
<pre><code>create or replace view public.owner_feature_entitlements as
|
|
with base as (
|
|
select
|
|
s.user_id as owner_id,
|
|
f.key as feature_key,
|
|
pf.limits,
|
|
'plan'::text as source
|
|
from public.subscriptions s
|
|
join public.plan_features pf
|
|
on pf.plan_id = s.plan_id
|
|
and pf.enabled = true
|
|
join public.features f
|
|
on f.id = pf.feature_id
|
|
where s.status = 'active'
|
|
and s.user_id is not null
|
|
|
|
union all
|
|
|
|
select
|
|
tm.owner_id,
|
|
f.key as feature_key,
|
|
mf.limits,
|
|
'module'::text as source
|
|
from public.tenant_modules tm
|
|
join public.modules m
|
|
on m.id = tm.module_id
|
|
and m.is_active = true
|
|
join public.module_features mf
|
|
on mf.module_id = m.id
|
|
and mf.enabled = true
|
|
join public.features f
|
|
on f.id = mf.feature_id
|
|
where tm.status = 'active'
|
|
and tm.owner_id is not null
|
|
)
|
|
select
|
|
owner_id,
|
|
feature_key,
|
|
array_agg(distinct source) as sources,
|
|
jsonb_agg(limits) filter (where limits is not null) as limits_list
|
|
from base
|
|
group by owner_id, feature_key;</code></pre>
|
|
|
|
<h3>6.4 Limpeza do dado inválido (subscription com user_id NULL)</h3>
|
|
<pre><code>-- se for lixo de seed/teste (recomendado remover):
|
|
delete from public.subscriptions
|
|
where user_id is null;</code></pre>
|
|
|
|
</section>
|
|
<section class="section" id="07">
|
|
<h2>7. Hardening (constraints e índices recomendados)</h2>
|
|
|
|
<p>Após corrigir dados e views, aplique hardening para impedir regressões.</p>
|
|
|
|
<h3>7.1 subscriptions.user_id NOT NULL</h3>
|
|
<pre><code>alter table public.subscriptions
|
|
alter column user_id set not null;</code></pre>
|
|
|
|
<h3>7.2 Uma assinatura ativa por usuário</h3>
|
|
<pre><code>create unique index if not exists subscriptions_one_active_per_user
|
|
on public.subscriptions (user_id)
|
|
where status = 'active';</code></pre>
|
|
|
|
<h3>7.3 Índice de performance para consultas por owner/status</h3>
|
|
<pre><code>create index if not exists subscriptions_user_status_idx
|
|
on public.subscriptions (user_id, status, created_at desc);</code></pre>
|
|
|
|
<h3>7.4 tenant_modules.owner_id NOT NULL (decisão tomada nesta sessão)</h3>
|
|
<pre><code>alter table public.tenant_modules
|
|
alter column owner_id set not null;</code></pre>
|
|
|
|
<h3>7.5 Uniqueness e performance em plan_features / module_features</h3>
|
|
<pre><code>create unique index if not exists plan_features_plan_feature_ux
|
|
on public.plan_features (plan_id, feature_id);
|
|
|
|
create index if not exists plan_features_plan_enabled_idx
|
|
on public.plan_features (plan_id, enabled);
|
|
|
|
create unique index if not exists module_features_module_feature_ux
|
|
on public.module_features (module_id, feature_id);</code></pre>
|
|
|
|
<div class="callout info">
|
|
<strong>Regra prática:</strong> dados inválidos (NULL em owner) devem ser bloqueados na borda (constraints), não “corrigidos” no front.
|
|
</div>
|
|
|
|
</section>
|
|
<section class="section" id="08">
|
|
<h2>8. Verificação pós-correção (checklist)</h2>
|
|
|
|
<h3>8.1 Saúde deve zerar</h3>
|
|
<pre><code>select mismatch_type, count(*) as qtd
|
|
from public.v_subscription_feature_mismatch
|
|
group by 1
|
|
order by 2 desc;</code></pre>
|
|
|
|
<h3>8.2 Não pode haver owner nulo em subscriptions / tenant_modules ativos</h3>
|
|
<pre><code>select count(*) as subs_user_null
|
|
from public.subscriptions
|
|
where user_id is null;
|
|
|
|
select count(*) as tenant_modules_active_owner_null
|
|
from public.tenant_modules
|
|
where status='active' and owner_id is null;</code></pre>
|
|
|
|
<h3>8.3 Entitlements agregados não devem conter owner null</h3>
|
|
<pre><code>select owner_id, feature_key
|
|
from public.owner_feature_entitlements
|
|
where owner_id is null
|
|
limit 20;</code></pre>
|
|
|
|
<div class="callout ok">
|
|
<strong>OK final:</strong> todas as queries acima retornam 0 linhas (ou contagens zero).
|
|
</div>
|
|
|
|
</section>
|
|
<section class="section" id="09">
|
|
<h2>9. Notas de implementação no front-end (contexto)</h2>
|
|
|
|
<p>Durante a sessão, a UI foi ajustada para:</p>
|
|
<ul>
|
|
<li>Traduzir telas para PT-BR, melhorar títulos, descrições e mensagens.</li>
|
|
<li>Padronizar inputs com <code>FloatLabel</code> + <code>IconField</code> + <code>InputIcon</code>.</li>
|
|
<li>Adicionar confirmações e “alterações pendentes” em ações em massa (plan_features), evitando salvar por clique acidental.</li>
|
|
<li>Garantir que ações de correção (Fix/FixAll) reflitam erros reais (RPC quebrada vs dados inválidos).</li>
|
|
</ul>
|
|
|
|
<div class="callout warn">
|
|
<strong>Regra operacional:</strong> se a coluna Owner aparecer vazia, corrija no banco primeiro (dados/view),
|
|
antes de mexer no front.
|
|
</div>
|
|
|
|
</section>
|
|
<section class="section" id="10">
|
|
<h2>10. Linha do tempo da sessão (resumo)</h2>
|
|
|
|
<ul>
|
|
<li><strong>Detecção:</strong> tela “Saúde das Assinaturas” exibindo Owner vazio e divergências.</li>
|
|
<li><strong>Inspeção:</strong> <code>v_subscription_feature_mismatch</code> mostrava <code>owner_id NULL</code> em missing/unexpected.</li>
|
|
<li><strong>Erro crítico:</strong> FixAll falhava com <code>subscriptions.owner_id</code> inexistente.</li>
|
|
<li><strong>Correção #1:</strong> alinhar RPCs ao schema atual (<code>subscriptions.user_id</code>).</li>
|
|
<li><strong>Correção #2:</strong> identificar que <code>owner_feature_entitlements</code> é VIEW e filtrar <code>status='active'</code>.</li>
|
|
<li><strong>Correção #3:</strong> remover subscription inválida com <code>user_id NULL</code>.</li>
|
|
<li><strong>Hardening:</strong> constraints e índices para prevenir regressões.</li>
|
|
</ul>
|
|
|
|
<div class="callout info">
|
|
<strong>Atualizado:</strong> 2026-03-01 11:46:44 UTC
|
|
</div>
|
|
|
|
</section>
|
|
</div>
|
|
<footer>
|
|
<div><strong>Agência PSI — Relatório Técnico (Billing Health)</strong></div>
|
|
<div class="small">Documento operacional inspirado no “Documento Mestre Billing v2.0”. Atualizado em 2026-03-01 11:46:44 UTC.</div>
|
|
</footer>
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>
|