Files
agenciapsilmno/docs/billing/Agencia_PSI_Billing_Subscriptions_v1_2.html

207 lines
5.4 KiB
HTML

<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Agência PSI — Billing & Subscriptions v1.2</title>
<style>
body{
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
margin:0;
padding:40px;
background:#f6f8fc;
color:#0f172a;
}
h1{font-size:28px;margin-bottom:8px;}
h2{margin-top:40px;font-size:20px;}
h3{margin-top:24px;font-size:16px;}
p{line-height:1.6;color:#334155;}
pre{
background:#0f172a;
color:#e2e8f0;
padding:16px;
border-radius:12px;
overflow:auto;
}
code{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;}
.section{margin-bottom:40px;}
.badge{
display:inline-block;
padding:4px 10px;
border-radius:999px;
background:#e2e8f0;
font-size:12px;
margin-right:6px;
}
.rule{
background:#e0f2fe;
padding:14px;
border-left:4px solid #0284c7;
border-radius:10px;
margin-top:12px;
}
.warn{
background:#fef3c7;
padding:14px;
border-left:4px solid #d97706;
border-radius:10px;
margin-top:12px;
}
.danger{
background:#fee2e2;
padding:14px;
border-left:4px solid #dc2626;
border-radius:10px;
margin-top:12px;
}
.ok{
background:#dcfce7;
padding:14px;
border-left:4px solid #16a34a;
border-radius:10px;
margin-top:12px;
}
footer{
margin-top:60px;
font-size:12px;
color:#64748b;
}
</style>
</head>
<body>
<h1>Billing & Subscriptions — v1.2</h1>
<p><strong>Agência PSI</strong> — Documento consolidado da sessão técnica sobre Subscriptions, Guardrails e Seeder.</p>
<div class="section">
<h2>1. Escopo desta versão</h2>
<p>Este documento consolida tudo o que foi tratado nesta sessão:</p>
<ul>
<li>Modelagem real da tabela <code>subscriptions</code></li>
<li>Histórico via <code>subscription_events</code></li>
<li>Triggers (guardrails) de proteção</li>
<li>Views oficiais</li>
<li>Seeder completo (planos + preços + metadata pública)</li>
<li>Erros reais encontrados e solução aplicada</li>
</ul>
</div>
<div class="section">
<h2>2. Estrutura confirmada — subscriptions</h2>
<pre><code>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()</code></pre>
<div class="rule">
Assinatura de clínica exige <strong>tenant_id</strong>.
Assinatura de terapeuta pode usar <strong>user_id</strong>.
</div>
</div>
<div class="section">
<h2>3. Guardrails (Proteções de Integridade)</h2>
<h3>3.1 Impedir deletar planos core</h3>
<pre><code>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 $$;</code></pre>
<h3>3.2 Impedir alterar target</h3>
<pre><code>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 $$;</code></pre>
<h3>3.3 Impedir alterar key core</h3>
<pre><code>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 $$;</code></pre>
<div class="warn">
Esses guardrails impediram alterações indevidas quando tentamos renomear planos core.
</div>
</div>
<div class="section">
<h2>4. Views Oficiais</h2>
<p><strong>v_public_pricing</strong> — Tela pública de preços.</p>
<p><strong>v_tenant_active_subscription</strong> — Plano vigente do tenant.</p>
<p><strong>v_subscription_health</strong> — Diagnóstico de inconsistências.</p>
</div>
<div class="section">
<h2>5. Seeder Oficial (MVP)</h2>
<pre><code>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;</code></pre>
<div class="ok">
Seeder é idempotente. Pode rodar múltiplas vezes sem duplicar.
</div>
</div>
<div class="section">
<h2>6. Incidentes reais resolvidos</h2>
<h3>6.1 Pricing retornando null</h3>
<p>Causa: não havia preço vigente (is_active=true e active_to is null).</p>
<h3>6.2 Erro ao alterar plano padrão</h3>
<p>Causa: trigger guard_no_change_core_plan_key bloqueando alteração.</p>
<h3>6.3 Assinatura sem tenant_id</h3>
<p>Causa: regra de negócio no banco impedindo clinic sem tenant.</p>
</div>
<div class="section">
<h2>7. Diretrizes finais</h2>
<ul>
<li>Plano nunca deve ser inferido do role.</li>
<li>UI deve consumir apenas views oficiais.</li>
<li>Plano core nunca deve ser renomeado.</li>
<li>Preço sempre deve ter vigência ativa.</li>
</ul>
</div>
<footer>
Agência PSI — Billing & Subscriptions v1.2<br>
Documento gerado automaticamente.
</footer>
</body>
</html>