Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions

View File

@@ -0,0 +1,612 @@
<!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>