ZERADO
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Arquitetura Técnica Completa — Billing & Assinaturas | Agência PSI</title>
|
||||
<style>
|
||||
body{font-family:Arial,Helvetica,sans-serif;margin:40px;line-height:1.65;color:#111}
|
||||
h1,h2,h3{margin-top:36px}
|
||||
code,pre{background:#f4f4f4;padding:12px;border-radius:8px;display:block;overflow:auto;font-size:13px}
|
||||
.section{margin-bottom:40px}
|
||||
.small{font-size:13px;color:#555}
|
||||
ul{margin-left:20px}
|
||||
.diagram{background:#fafafa;border:1px solid #ddd;padding:16px;border-radius:8px;font-family:monospace;font-size:13px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Arquitetura Técnica Completa — Billing & Assinaturas</h1>
|
||||
<p class="small">Projeto: Agência PSI • Documento estrutural definitivo do domínio de Billing.</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Visão Arquitetural Geral</h2>
|
||||
|
||||
<div class="diagram">
|
||||
USUÁRIO
|
||||
│
|
||||
▼
|
||||
subscription_intents (VIEW unificada)
|
||||
│
|
||||
▼ (RPC activate_subscription_from_intent)
|
||||
subscriptions
|
||||
│
|
||||
▼
|
||||
subscription_events (auditoria)
|
||||
│
|
||||
▼
|
||||
entitlements (derivados do plano)
|
||||
</div>
|
||||
|
||||
<p>Separação estrutural:</p>
|
||||
<ul>
|
||||
<li><strong>plans</strong> → catálogo</li>
|
||||
<li><strong>plan_prices</strong> → preço versionado</li>
|
||||
<li><strong>subscription_intents_*</strong> → intenção pré-pagamento</li>
|
||||
<li><strong>subscriptions</strong> → assinatura ativa</li>
|
||||
<li><strong>subscription_events</strong> → histórico</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Estados Oficiais da Assinatura</h2>
|
||||
<pre>
|
||||
pending
|
||||
active
|
||||
past_due
|
||||
suspended
|
||||
cancelled
|
||||
expired
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>3. Índice de Integridade de Preços</h2>
|
||||
<pre>
|
||||
create unique index if not exists uq_plan_price_active
|
||||
on plan_prices (plan_id, interval, currency)
|
||||
where is_active = true and active_to is null;
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>4. Função Completa — activate_subscription_from_intent</h2>
|
||||
<pre>
|
||||
CREATE OR REPLACE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid)
|
||||
RETURNS subscriptions
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
declare
|
||||
v_intent record;
|
||||
v_sub public.subscriptions;
|
||||
v_days int;
|
||||
v_user_id uuid;
|
||||
v_plan_id uuid;
|
||||
v_target text;
|
||||
begin
|
||||
|
||||
select * into v_intent
|
||||
from public.subscription_intents
|
||||
where id = p_intent_id;
|
||||
|
||||
if not found then
|
||||
raise exception 'Intent não encontrada';
|
||||
end if;
|
||||
|
||||
if v_intent.status <> 'paid' then
|
||||
raise exception 'Intent precisa estar paid';
|
||||
end if;
|
||||
|
||||
select p.id, p.target
|
||||
into v_plan_id, v_target
|
||||
from public.plans p
|
||||
where p.key = v_intent.plan_key
|
||||
limit 1;
|
||||
|
||||
if v_plan_id is null then
|
||||
raise exception 'Plano não encontrado';
|
||||
end if;
|
||||
|
||||
v_target := lower(coalesce(v_target,''));
|
||||
|
||||
if v_target = 'clinic' and v_intent.tenant_id is null then
|
||||
raise exception 'Intent clinic exige tenant_id';
|
||||
end if;
|
||||
|
||||
if v_target = 'therapist' and v_intent.tenant_id is not null then
|
||||
raise exception 'Intent therapist não deve ter tenant_id';
|
||||
end if;
|
||||
|
||||
v_days := case when v_intent.interval = 'year' then 365 else 30 end;
|
||||
|
||||
v_user_id := coalesce(v_intent.created_by_user_id, v_intent.user_id);
|
||||
|
||||
if v_user_id is null then
|
||||
raise exception 'user_id obrigatório';
|
||||
end if;
|
||||
|
||||
if v_target = 'clinic' then
|
||||
update subscriptions
|
||||
set status = 'cancelled',
|
||||
cancelled_at = now()
|
||||
where tenant_id = v_intent.tenant_id
|
||||
and status = 'active';
|
||||
else
|
||||
update subscriptions
|
||||
set status = 'cancelled',
|
||||
cancelled_at = now()
|
||||
where user_id = v_user_id
|
||||
and tenant_id is null
|
||||
and status = 'active';
|
||||
end if;
|
||||
|
||||
insert into subscriptions (
|
||||
user_id,
|
||||
plan_id,
|
||||
status,
|
||||
current_period_start,
|
||||
current_period_end,
|
||||
tenant_id,
|
||||
plan_key,
|
||||
interval,
|
||||
provider,
|
||||
started_at,
|
||||
activated_at
|
||||
)
|
||||
values (
|
||||
v_user_id,
|
||||
v_plan_id,
|
||||
'active',
|
||||
now(),
|
||||
now() + make_interval(days => v_days),
|
||||
case when v_target='clinic' then v_intent.tenant_id else null end,
|
||||
v_intent.plan_key,
|
||||
v_intent.interval,
|
||||
'manual',
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
returning * into v_sub;
|
||||
|
||||
return v_sub;
|
||||
|
||||
end;
|
||||
$function$;
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>5. Função Completa — transition_subscription (segura)</h2>
|
||||
<pre>
|
||||
create or replace function public.transition_subscription(
|
||||
p_subscription_id uuid,
|
||||
p_to_status text,
|
||||
p_reason text default null,
|
||||
p_metadata jsonb default null
|
||||
)
|
||||
returns subscriptions
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_sub subscriptions;
|
||||
v_uid uuid;
|
||||
v_allowed boolean := false;
|
||||
begin
|
||||
|
||||
v_uid := auth.uid();
|
||||
|
||||
select * into v_sub
|
||||
from subscriptions
|
||||
where id = p_subscription_id;
|
||||
|
||||
if not found then
|
||||
raise exception 'Assinatura não encontrada';
|
||||
end if;
|
||||
|
||||
if is_saas_admin() then
|
||||
v_allowed := true;
|
||||
end if;
|
||||
|
||||
if not v_allowed
|
||||
and v_sub.tenant_id is null
|
||||
and v_sub.user_id = v_uid then
|
||||
v_allowed := true;
|
||||
end if;
|
||||
|
||||
if not v_allowed
|
||||
and v_sub.tenant_id is not null then
|
||||
|
||||
if exists (
|
||||
select 1 from tenant_members tm
|
||||
where tm.tenant_id = v_sub.tenant_id
|
||||
and tm.user_id = v_uid
|
||||
and tm.status = 'active'
|
||||
and tm.role = 'tenant_admin'
|
||||
) then
|
||||
v_allowed := true;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
if not v_allowed then
|
||||
raise exception 'Sem permissão';
|
||||
end if;
|
||||
|
||||
update subscriptions
|
||||
set status = p_to_status,
|
||||
updated_at = now()
|
||||
where id = p_subscription_id
|
||||
returning * into v_sub;
|
||||
|
||||
insert into subscription_events (
|
||||
subscription_id,
|
||||
owner_id,
|
||||
event_type,
|
||||
created_at,
|
||||
created_by,
|
||||
source,
|
||||
reason,
|
||||
metadata
|
||||
)
|
||||
values (
|
||||
v_sub.id,
|
||||
coalesce(v_sub.tenant_id, v_sub.user_id),
|
||||
'status_changed',
|
||||
now(),
|
||||
v_uid,
|
||||
'manual_transition',
|
||||
p_reason,
|
||||
p_metadata
|
||||
);
|
||||
|
||||
return v_sub;
|
||||
|
||||
end;
|
||||
$$;
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>6. Máquina de Estados Recomendada</h2>
|
||||
|
||||
<div class="diagram">
|
||||
pending → active → past_due → suspended → cancelled
|
||||
↓
|
||||
expired
|
||||
</div>
|
||||
|
||||
<p>Recomendação futura: validar allowed_transitions em tabela dedicada.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>7. Checklist de Validação Estrutural</h2>
|
||||
<ul>
|
||||
<li>Intent paga gera subscription ativa</li>
|
||||
<li>subscription_id vinculado corretamente</li>
|
||||
<li>Cancelamento gera evento</li>
|
||||
<li>Reativação preserva histórico</li>
|
||||
<li>Tenant isolation validado</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>8. Roadmap Estrutural Futuro</h2>
|
||||
<ul>
|
||||
<li>State machine formal com allowed_transitions</li>
|
||||
<li>Automação de expiração por cron</li>
|
||||
<li>Integração Stripe mantendo arquitetura</li>
|
||||
<li>Health monitor automatizado</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<p class="small">Documento técnico estrutural consolidado após implementação real validada.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user