# Arquitetura Técnica Completa — Billing & Assinaturas

Projeto: Agência PSI • Documento estrutural definitivo do domínio de Billing.

## 1. Visão Arquitetural Geral

USUÁRIO │ ▼ subscription_intents (VIEW unificada) │ ▼ (RPC activate_subscription_from_intent) subscriptions │ ▼ subscription_events (auditoria) │ ▼ entitlements (derivados do plano)

Separação estrutural:

- plans → catálogo
- plan_prices → preço versionado
- subscription_intents_* → intenção pré-pagamento
- subscriptions → assinatura ativa
- subscription_events → histórico

## 2. Estados Oficiais da Assinatura

    pending
    active
    past_due
    suspended
    cancelled
    expired

## 3. Índice de Integridade de Preços

    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;

## 4. Função Completa — activate_subscription_from_intent

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

## 5. Função Completa — transition_subscription (segura)

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

## 6. Máquina de Estados Recomendada

pending → active → past_due → suspended → cancelled ↓ expired

Recomendação futura: validar allowed_transitions em tabela dedicada.

## 7. Checklist de Validação Estrutural

- Intent paga gera subscription ativa
- subscription_id vinculado corretamente
- Cancelamento gera evento
- Reativação preserva histórico
- Tenant isolation validado

## 8. Roadmap Estrutural Futuro

- State machine formal com allowed_transitions
- Automação de expiração por cron
- Integração Stripe mantendo arquitetura
- Health monitor automatizado

Documento técnico estrutural consolidado após implementação real validada.