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:

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