# Billing & Subscriptions — v1.2

Agência PSI — Documento consolidado da sessão técnica sobre Subscriptions, Guardrails e Seeder.

## 1. Escopo desta versão

Este documento consolida tudo o que foi tratado nesta sessão:

- Modelagem real da tabela `subscriptions`
- Histórico via `subscription_events`
- Triggers (guardrails) de proteção
- Views oficiais
- Seeder completo (planos + preços + metadata pública)
- Erros reais encontrados e solução aplicada

## 2. Estrutura confirmada — subscriptions

    id uuid PK
    tenant_id uuid NULL
    user_id uuid NULL
    plan_id uuid NOT NULL
    plan_key text NULL
    interval text CHECK ('month','year')
    status text DEFAULT 'active'
    current_period_start timestamptz
    current_period_end timestamptz
    provider text DEFAULT 'manual'
    cancel_at_period_end boolean DEFAULT false
    created_at timestamptz DEFAULT now()
    updated_at timestamptz DEFAULT now()

Assinatura de clínica exige tenant_id. Assinatura de terapeuta pode usar user_id.

## 3. Guardrails (Proteções de Integridade)

### 3.1 Impedir deletar planos core

    create or replace function guard_no_delete_core_plans()
    returns trigger language plpgsql as $$
    begin
      if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro') then
        raise exception 'Plano padrão (%) não pode ser removido.', old.key;
      end if;
      return old;
    end $$;

### 3.2 Impedir alterar target

    create or replace function guard_no_change_plan_target()
    returns trigger language plpgsql as $$
    begin
      if new.target is distinct from old.target then
        raise exception 'Não é permitido alterar target do plano.';
      end if;
      return new;
    end $$;

### 3.3 Impedir alterar key core

    create or replace function guard_no_change_core_plan_key()
    returns trigger language plpgsql as $$
    begin
      if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro')
         and new.key is distinct from old.key then
        raise exception 'Não é permitido alterar a key do plano padrão.';
      end if;
      return new;
    end $$;

Esses guardrails impediram alterações indevidas quando tentamos renomear planos core.

## 4. Views Oficiais

v_public_pricing — Tela pública de preços.

v_tenant_active_subscription — Plano vigente do tenant.

v_subscription_health — Diagnóstico de inconsistências.

## 5. Seeder Oficial (MVP)

    insert into plans (key,name,target,is_active)
    values
    ('clinic_free','Clinic Free','clinic',true),
    ('clinic_pro','Clinic Pro','clinic',true),
    ('therapist_free','Therapist Free','therapist',true),
    ('therapist_pro','Therapist Pro','therapist',true)
    on conflict (key) do update set
    name=excluded.name,
    target=excluded.target,
    is_active=excluded.is_active;

Seeder é idempotente. Pode rodar múltiplas vezes sem duplicar.

## 6. Incidentes reais resolvidos

### 6.1 Pricing retornando null

Causa: não havia preço vigente (is_active=true e active_to is null).

### 6.2 Erro ao alterar plano padrão

Causa: trigger guard_no_change_core_plan_key bloqueando alteração.

### 6.3 Assinatura sem tenant_id

Causa: regra de negócio no banco impedindo clinic sem tenant.

## 7. Diretrizes finais

- Plano nunca deve ser inferido do role.
- UI deve consumir apenas views oficiais.
- Plano core nunca deve ser renomeado.
- Preço sempre deve ter vigência ativa.