## 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.

Impacto: 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.
Resultado final: view de entitlements corrigida (filtra `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

View de saúde

`public.v_subscription_feature_mismatch` — compara o esperado (plan_features do plano ativo) com o atual (entitlements).

Entitlements agregados

`public.owner_feature_entitlements` — VIEW agregada (sources + limits_list), derivada de `subscriptions` e `tenant_modules`.

Rotinas de reparo

`public.rebuild_owner_entitlements(uuid)` e `public.fix_all_subscription_mismatches()`.

Tabelas de configuração

`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_mismatch` retornando `owner_id = NULL` tanto em missing quanto em unexpected.
- Contagem estável em 4/4 divergências, mesmo após tentativa de reparo.
- Existência de uma `subscription` com `status='active'` e `user_id = NULL` (dado inválido).
- Falha de execução do FixAll com erro de coluna inexistente: `subscriptions.owner_id` (schema drift).

Nota de leitura: ao ver `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.

Erro observado: `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;
    $$;

Importante: se `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);

Regra prática: dados inválidos (NULL em owner) devem ser bloqueados na borda (constraints), não “corrigidos” no front.

## 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;

OK final: todas as queries acima retornam 0 linhas (ou contagens zero).

## 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).

Regra operacional: se a coluna Owner aparecer vazia, corrija no banco primeiro (dados/view), antes de mexer no front.

## 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_mismatch` mostrava `owner_id NULL` em missing/unexpected.
- Erro crítico: FixAll falhava com `subscriptions.owner_id` inexistente.
- Correção #1: alinhar RPCs ao schema atual (`subscriptions.user_id`).
- Correção #2: identificar que `owner_feature_entitlements` é VIEW e filtrar `status='active'`.
- Correção #3: remover subscription inválida com `user_id NULL`.
- Hardening: constraints e índices para prevenir regressões.

Atualizado: 2026-03-01 11:46:44 UTC