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
- Felipe cria invoice via UI → backend chama
_adapter_for(tenant_id)→ decifra api_key Asaas → POST Asaas. - Cliente recebe email com link Asaas → paga PIX.
- Asaas dispara webhook →
mark_paid_by_gateway→billing_invoice.status=paid+ emit eventbilling.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.py—AsaasAdapter.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
| Coluna | Notas |
|---|---|
tenant_id / email | UNIQUE |
name / cpf_cnpj / phone | |
gateway_customer_id | ID no Asaas |
billing_invoice
| Coluna | Notas |
|---|---|
customer_id | FK |
credential_id | Soft-FK → tenant_provider_credentials.id |
amount / due_date / description | |
billing_type | PIX / BOLETO / CREDIT_CARD |
status | pending / paid / overdue / refunded / failed |
gateway_invoice_id | |
provider_payment_url | Lazy cached |
billing_subscription
| Coluna | Notas |
|---|---|
customer_id | FK |
gateway_subscription_id | |
plan_name / amount / cycle (MONTHLY/YEARLY) | |
max_seats | Pra multi-seat |
status | active / paused / canceled |
billing_subscription_seat
| Coluna | Notas |
|---|---|
subscription_id | FK CASCADE |
invite_email / invite_name / invite_token (UNIQUE) | |
member_id | Vinculado após accept |
accepted_at / removed_at |
billing_webhook_event
| Coluna | Notas |
|---|---|
credential_id | |
provider_event_id | UNIQUE com credential_id (idempotency) |
event_type / payload (JSONB) | |
processing_status | queued / ok / ignored / failed / failed_terminal / manually_resolved / dedupe |
Relacionamentos cross-módulo
| Direção | Outro módulo | Como |
|---|---|---|
| ↗ Lê | tenant_provider_credentials | Decifra api_key Asaas |
| ↘ Escreve | members | Sync plano após payment paid |
| ↘ Escreve cross-DB | mandir_platform.provider_approval_request | Approval 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
| Var | Propósito |
|---|---|
MANDIR_CRYPTO_MASTER_KEY | Fail-closed startup (hex 64) |
MANDIR_ROOT_TENANT_ID | Self-onboarding wizard |
ASAAS_WEBHOOK_TOKEN | Header webhook |
ASAAS_WEBHOOK_IP_ALLOWLIST | CIDR 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 |
|---|---|
| 1 | Soft-FK credential_id cross-DB — não enforced |
| 2 | PIX QR caching — link expirado não atualiza auto |
| 3 | Invite_token long-lived — sem TTL |
| 4 | Sem 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.