1. Resumo executivo
Sintoma principal: a tela Saúde das Assinaturas exibia divergências e a coluna Owner aparecia vazia
(linhas com owner_id = NULL). Além disso, os botões Fix e Fix All falhavam.
subscriptions.status = 'active' e exclui NULL),
funções RPC alinhadas ao schema atual (subscriptions.user_id), dados inválidos removidos e constraints/índices adicionados
para impedir regressões.
2. Escopo e componentes envolvidos
public.v_subscription_feature_mismatch — compara o esperado (plan_features do plano ativo) com o atual (entitlements).
public.owner_feature_entitlements — VIEW agregada (sources + limits_list), derivada de subscriptions e tenant_modules.
public.rebuild_owner_entitlements(uuid) e public.fix_all_subscription_mismatches().
plans, features, plan_features, module_features, tenant_modules, subscriptions.
3. Sintomas observados e evidências
Foram observados os seguintes indícios no banco:
- View
v_subscription_feature_mismatchretornandoowner_id = NULLtanto em missing quanto em unexpected. - Contagem estável em 4/4 divergências, mesmo após tentativa de reparo.
- Existência de uma
subscriptioncomstatus='active'euser_id = NULL(dado inválido). - Falha de execução do FixAll com erro de coluna inexistente:
subscriptions.owner_id(schema drift).
owner_id = NULL 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 integridade.
4. Diagnóstico e causa raiz
4.1 Causa raiz #1 — Schema drift nas funções RPC
As funções de reparo estavam escritas para um schema anterior, usando subscriptions.owner_id. No schema atual, o owner do contexto
“terapeuta” é subscriptions.user_id. Isso quebrou:
- Fix owner: falha ao buscar o plano ativo do owner.
- Fix all: falha ao iterar owners e chamar o rebuild.
column "owner_id" does not exist (hint citando subscriptions.user_id).
4.2 Causa raiz #2 — View de entitlements agregados não filtrava status
A view owner_feature_entitlements agregava a fonte “plan” sem filtrar subscriptions.status, permitindo que uma subscription
inactive com user_id NULL continuasse “vazando” entitlements com owner_id NULL para o sistema.
4.3 Causa raiz #3 — Dado inválido
Foi identificado um registro em subscriptions com user_id NULL. Mesmo após torná-lo inactive, ele continuava contaminando
a view (por ausência do filtro de status).
5. SQLs usados no diagnóstico (playbook)
Use este bloco para reproduzir o diagnóstico com segurança.
5.1 Ver divergências e amostras
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;
5.2 Encontrar subscriptions inválidas (user_id nulo)
select id, user_id, plan_id, status, created_at
from public.subscriptions
where user_id is null
order by created_at desc;
5.3 Entender a origem dos entitlements agregados
select pg_get_viewdef('public.owner_feature_entitlements'::regclass, true) as view_sql;
5.4 Verificar tenant_modules inválidos (owner_id nulo)
select count(*) as qtd
from public.tenant_modules
where status = 'active' and owner_id is null;
6. Correções aplicadas no banco (patches)
6.1 Patch: rebuild_owner_entitlements (owner = subscriptions.user_id)
Ajuste para buscar o plano ativo por subscriptions.user_id e reconstruir entitlements com base em plan_features.
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;
$$;
owner_feature_entitlements for uma VIEW (como no ambiente desta sessão),
o DELETE/INSERT acima deve ser direcionado à tabela base real de entitlements, se existir.
Nesta sessão, a correção definitiva foi feita ajustando a view agregadora e limpando o dado inválido.
6.2 Patch: fix_all_subscription_mismatches (itera subscriptions.user_id)
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;
$$;
6.3 Patch: owner_feature_entitlements (filtra status e NULLs)
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;
6.4 Limpeza do dado inválido (subscription com user_id NULL)
-- se for lixo de seed/teste (recomendado remover):
delete from public.subscriptions
where user_id is null;
7. Hardening (constraints e índices recomendados)
Após corrigir dados e views, aplique hardening para impedir regressões.
7.1 subscriptions.user_id NOT NULL
alter table public.subscriptions
alter column user_id set not null;
7.2 Uma assinatura ativa por usuário
create unique index if not exists subscriptions_one_active_per_user
on public.subscriptions (user_id)
where status = 'active';
7.3 Índice de performance para consultas por owner/status
create index if not exists subscriptions_user_status_idx
on public.subscriptions (user_id, status, created_at desc);
7.4 tenant_modules.owner_id NOT NULL (decisão tomada nesta sessão)
alter table public.tenant_modules
alter column owner_id set not null;
7.5 Uniqueness e performance em plan_features / module_features
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);
8. Verificação pós-correção (checklist)
8.1 Saúde deve zerar
select mismatch_type, count(*) as qtd
from public.v_subscription_feature_mismatch
group by 1
order by 2 desc;
8.2 Não pode haver owner nulo em subscriptions / tenant_modules ativos
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;
8.3 Entitlements agregados não devem conter owner null
select owner_id, feature_key
from public.owner_feature_entitlements
where owner_id is null
limit 20;
9. Notas de implementação no front-end (contexto)
Durante a sessão, a UI foi ajustada para:
- Traduzir telas para PT-BR, melhorar títulos, descrições e mensagens.
- Padronizar inputs com
FloatLabel+IconField+InputIcon. - Adicionar confirmações e “alterações pendentes” em ações em massa (plan_features), evitando salvar por clique acidental.
- Garantir que ações de correção (Fix/FixAll) reflitam erros reais (RPC quebrada vs dados inválidos).
10. Linha do tempo da sessão (resumo)
- Detecção: tela “Saúde das Assinaturas” exibindo Owner vazio e divergências.
- Inspeção:
v_subscription_feature_mismatchmostravaowner_id NULLem missing/unexpected. - Erro crítico: FixAll falhava com
subscriptions.owner_idinexistente. - Correção #1: alinhar RPCs ao schema atual (
subscriptions.user_id). - Correção #2: identificar que
owner_feature_entitlementsé VIEW e filtrarstatus='active'. - Correção #3: remover subscription inválida com
user_id NULL. - Hardening: constraints e índices para prevenir regressões.