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:
@@ -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>
|
||||
Reference in New Issue
Block a user