Menuabrir
EstávelAtualizado em 14 de mai. de 2026, 00:06

Este módulo depende de

3
  • platform_adminApproval workflow cross-DB — super-admin aprova credenciais antes de ativar
  • settingsLê tenant_provider_credentials cifradas via bridge de integrations
  • membersSync de plano após payment paid — atualiza member.plan_type

Módulos que dependem deste

2
  • councilcross_module.read_revenue_summary (MRR, overdue, custo retenção)
  • connectionsAgrega status de credenciais Asaas (tenant_provider_credentials)

Billing

Status: 🟢 Estável (multi-tenant Asaas com envelope cifragem) Code: backend/app/modules/billing UI: frontend/src/app/(admin)/[slug]/admin/billing Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: platform_admin (approval workflow cross-DB), settings (integrations bridge), members (sync de plano)


1. Identidade

O que faz (uma frase)

Cobranças recorrentes + invoices avulsas via Asaas multi-tenant (cada tenant tem credenciais cifradas isoladas, KEK por tenant via HMAC), com multi-seat plans, webhook signature timing-safe + IP allowlist, e approval workflow central (super-admin aprova antes de credencial ficar active).

Por que existe (negócio)

Sem este módulo, Mandir teria que integrar Asaas externamente por tenant — sem isolamento + sem approval workflow + sem PIX QR + sem multi-seat. Com Billing:

  • Mais Consciente cobra clientes via PIX/cartão.
  • Mentoria família = 1 subscription + N seats convidados.
  • Invoice avulsa pra venda 1x.
  • Approval workflow garante que só admin Mandir libera credenciais novas (anti-fraude).

Por que existe (técnico)

  • Crypto envelope per-tenant (memória [[mandir crypto master key]]): KEK = HMAC-SHA256(MASTER_KEY, tenant_id.bytes). Determinística, sem state.
  • Adapter Asaas isolado (adapters/asaas.py): retry tenacity 3x exponencial só em 429/5xx/timeout.
  • Webhook idempotency via (credential_id, provider_event_id) UNIQUE.
  • Approval workflow cross-DB documentado em cross-db-writes.md.

Status atual

Em prod, Mais Consciente operando com Asaas Production credenciada.


2. Cases de uso reais

Case 1: Cliente compra Mentoria Premium R$ 2.997

  1. Felipe cria invoice via UI → backend chama _adapter_for(tenant_id) → decifra api_key Asaas → POST Asaas.
  2. Cliente recebe email com link Asaas → paga PIX.
  3. Asaas dispara webhook → mark_paid_by_gatewaybilling_invoice.status=paid + emit event billing.invoice.paid.

Case 2: Multi-seat (família 4 alunos)

Subscription "Mentoria Família R$ 4.997 mensal max 4 seats" → invite_token enviado por email → cada seat aceita via /seats/accept → vincula member_id.

Case 3: Approval workflow

Tenant adiciona credencial Asaas via wizard /admin/settings/integrations/asaas → status=draft → auto-test passes → status=tested → submit pra approval → cria mandir_platform.provider_approval_request → super-admin Mandir aprova em /admin/platform/billing/approvals → cross-DB write atualiza credencial pra status=active.


3. Oportunidades de negócio

  • Multi-gateway: adicionar Stripe + MercadoPago atrás do mesmo adapter pattern.
  • Subscription analytics: churn cohort + LTV — dashboard premium.
  • Smart dunning: retry inteligente pré-overdue (intelligence sinaliza risco antes do D+1).
  • Marketplace fees: cobrar % sobre invoices criadas (revenue model Mandir).

4. Arquitetura interna

Arquivos

  • models.py — 5 tabelas.
  • service.py (~570 linhas) — _adapter_for, create_invoice, create_subscription, mark_paid_by_gateway, multi-seat lifecycle.
  • adapters/asaas.pyAsaasAdapter.
  • routes_credentials.py — wizard 3 passos.
  • routes_webhook_events.py — debug.

Tasks

Sem Celery próprio — dunning delegado a platform_admin/dunning.py.


5. Tabelas (5)

billing_customer

ColunaNotas
tenant_id / emailUNIQUE
name / cpf_cnpj / phone
gateway_customer_idID no Asaas

billing_invoice

ColunaNotas
customer_idFK
credential_idSoft-FK → tenant_provider_credentials.id
amount / due_date / description
billing_typePIX / BOLETO / CREDIT_CARD
statuspending / paid / overdue / refunded / failed
gateway_invoice_id
provider_payment_urlLazy cached

billing_subscription

ColunaNotas
customer_idFK
gateway_subscription_id
plan_name / amount / cycle (MONTHLY/YEARLY)
max_seatsPra multi-seat
statusactive / paused / canceled

billing_subscription_seat

ColunaNotas
subscription_idFK CASCADE
invite_email / invite_name / invite_token (UNIQUE)
member_idVinculado após accept
accepted_at / removed_at

billing_webhook_event

ColunaNotas
credential_id
provider_event_idUNIQUE com credential_id (idempotency)
event_type / payload (JSONB)
processing_statusqueued / ok / ignored / failed / failed_terminal / manually_resolved / dedupe

Relacionamentos cross-módulo

DireçãoOutro móduloComo
↗ Lêtenant_provider_credentialsDecifra api_key Asaas
↘ EscrevemembersSync plano após payment paid
↘ Escreve cross-DBmandir_platform.provider_approval_requestApproval workflow

6. API / Endpoints (18)

Prefixo /api/billing.

Charges (5)

GET/POST /charges, GET /charges/{id}, GET /charges/{id}/payment-link (lazy), GET /charges/{id}/pix-qr.

Subscriptions (3)

GET/POST /subscriptions, DELETE /subscriptions/{id} (cancel).

Seats (4)

GET /subscriptions/{id}/seats, POST /subscriptions/{id}/seats (invite), POST /subscriptions/seats/accept (token), DELETE /subscriptions/{id}/seats/{seat_id}.

Member self-service (3)

GET /me/subscription, GET /me/seats, GET /me/invoices.

Outros

GET /stats, GET/POST /invoices (deprecated alias), credentials wizard (15 endpoints), webhook events (3).


7. Configuração

Env vars

VarPropósito
MANDIR_CRYPTO_MASTER_KEYFail-closed startup (hex 64)
MANDIR_ROOT_TENANT_IDSelf-onboarding wizard
ASAAS_WEBHOOK_TOKENHeader webhook
ASAAS_WEBHOOK_IP_ALLOWLISTCIDR allowlist (Asaas ranges)

8. Operações + Troubleshooting

Sintoma: instance_key_required em endpoint

Causa: Tenant tem múltiplas credenciais Asaas active no mesmo environment, sem instance_key na request. Fix: Passar instance_key explícito ou desativar credenciais redundantes.

Sintoma: PIX QR retorna 404

Causa: Invoice billing_type ≠ PIX. Fix: Recriar invoice com billing_type=PIX.

Sintoma: Approval workflow trava

Causa: Cross-DB write falhou (apply_error preenchido). Fix: POST /platform/billing/approval-requests/{id}/retry-apply.


9. Limitações e débitos técnicos

#Item
1Soft-FK credential_id cross-DB — não enforced
2PIX QR caching — link expirado não atualiza auto
3Invite_token long-lived — sem TTL
4Sem multi-gateway

10. Histórico

ADR 0009 cravou crypto + multi-tenant Asaas. Em prod desde wizard self-onboarding. Memória [[mandir crypto master key]] documenta runbook.