Todo mundo que vende faturamento por assinatura cobra um percentual do seu faturamento. O Billing da Catalisa mede o uso, monta a fatura e controla a assinatura sem ficar com um pedaço da sua receita.
- Fintechs e financeiras que cobram por operação processada, não por licença de usuário
- Plataformas B2B que vendem plano com franquia incluída e excedente medido
- Times de produto que já usam gateway de pagamento e só precisam da camada de faturamento
- Assinatura de plataforma de subscription billing cobrada como percentual do faturamento (Stripe Billing, Chargebee, Recurly)
- Planilha de controle de franquia e excedente mantida pelo time financeiro
- Rotina caseira de "somar o uso do mês e mandar a fatura" espalhada pelo backend
- Um gateway de pagamento — quem captura cartão, Pix e boleto é o building block Payments
- Um emissor de nota fiscal (NF-e/NFS-e) ou motor de cálculo de tributos
- Um ERP financeiro com contas a pagar, conciliação bancária e razão contábil
01Resumo executivo
O Billing responde a uma pergunta que toda empresa que cobra recorrente precisa responder todo mês: quanto este cliente deve, por quê, e o que já foi pago. Ele guarda o plano, mede o consumo, monta a fatura linha a linha, registra o pagamento e mantém o ciclo da assinatura — trial, ativa, pausada, cancelada.
Na prática, uma financeira que cobra R$ 2.000 de mensalidade mais R$ 0,80 por consulta de crédito acima de 5.000 consultas para de manter isso em planilha. Ela cria um plano com franquia de 5.000 unidades, manda um evento de uso a cada consulta e, no fim do ciclo, uma chamada gera a fatura já com a assinatura e o excedente separados em linhas distintas.
Está em staging e roda em modo standalone na porta 3010. O status é beta, e isso é deliberado: a API de faturamento está completa e coberta por testes de unidade e integração, mas o agendador que fecharia o ciclo sozinho ainda não existe — hoje quem dispara o fechamento é o integrador. A seção 15 é explícita sobre isso e você deve lê-la antes de vender o módulo.
| Atributo | Valor |
|---|---|
| Identificador | billing |
| Categoria | Financeiro |
| Escopo | Tenant (exige organizationId no token em todas as 46 rotas) |
| Porta (standalone) | 3010 |
| Path alias | @billing |
| Prefixo HTTP | /billing |
| Schema no banco | billing |
| Status | Beta desde 2025-11 |
| Depende de | PostgreSQL, Redis, IAM |
02O problemanegócio
O cenário. Uma empresa vende software ou serviço com cobrança recorrente. No começo são dez clientes com o mesmo plano, e a cobrança cabe numa planilha. Depois vem o cliente que negociou franquia maior, o que pediu cobrança trimestral, o que quer pagar por consumo. Aí a planilha vira o sistema financeiro da empresa — e ninguém percebeu quando isso aconteceu.
O que trava hoje.
Medir consumo é a parte difícil, e é a parte que ninguém faz direito. Somar quantas chamadas de API o cliente fez parece trivial até o dia em que o mesmo evento é contado duas vezes porque a integração fez retry. Cobrança duplicada não é bug de software, é ligação do cliente para o diretor.
A franquia mora na cabeça de alguém. "Esse cliente tem 5 mil consultas inclusas" é informação que existe no contrato e no Slack, não no sistema. Quem fecha a fatura confere na mão.
A plataforma de billing cobra percentual do seu faturamento. Stripe Billing cobra 0,7% do volume faturado, Chargebee 0,80% no pay-as-you-go, Metronome 0,8% mais os eventos ingeridos, Recurly US$ 249/mês mais 0,9% (páginas públicas consultadas em 2026-08-16). Isso significa que o fornecedor cresce exatamente na mesma curva que você — e a linha de custo que mais dói é a que sobe quando o negócio vai bem. A página da Stripe é explícita ao dizer que o percentual "inclui transações do Billing processadas dentro e fora da Stripe": o percentual incide até sobre receita que não passou por eles.
Cobrar por consumo virou a norma, e a maioria das ferramentas cobra caro por isso. A adoção de precificação por uso em SaaS saiu de 34% em 2020 para 45% em 2021 e 61% em 2022, segundo as pesquisas State of Usage-Based Pricing da OpenView com cerca de 600 empresas (press release, 2021; TechCrunch sobre a 2ª edição, 2023). A mesma pesquisa de 2021 mediu retenção líquida de receita de 120% nas empresas com cobrança por uso, contra 110% nas de assinatura tradicional.
A base de assinaturas sai do seu banco. Quando o plano, o cliente e a fatura vivem no SaaS do fornecedor, toda pergunta cruzada ("quais clientes do segmento X estouraram franquia?") vira integração, não consulta.
Cada serviço novo recomeça a cobrança do zero. O módulo de crédito cobra de um jeito, o de assinatura eletrônica de outro, e a conta consolidada do cliente não existe em lugar nenhum.
O custo de não resolver. O custo direto é o percentual: numa operação que fatura R$ 5 milhões por mês, 0,7% são R$ 35 mil mensais — R$ 420 mil por ano, pagos para emitir faturas que você mesmo calculou. O custo indireto é pior e menos visível: fatura errada é a forma mais rápida de perder um cliente B2B que estava satisfeito com o produto, e a correção sempre custa mais que a diferença cobrada.
03Proposta de valornegócio
| Antes | Depois |
|---|---|
| A franquia do cliente está no contrato e na memória de alguém | includedUnits é campo do item de plano, aplicado no cálculo e na ingestão |
| Retry da integração vira cobrança em duplicidade | idempotencyKey é obrigatório em todo evento de uso; repetição retorna o evento original |
| Fechar o mês é conferência manual linha a linha | Uma chamada gera a fatura com assinatura e excedente separados por chargeType |
| O custo do faturamento é percentual da sua receita | O custo não acompanha o seu faturamento |
| Assinatura, cliente e permissão em três bases diferentes | Tudo no mesmo banco, sob o mesmo organizationId do token |
A medição é idempotente por contrato, não por disciplina. O campo idempotencyKey não é opcional no schema Zod. O par (organizationId, idempotencyKey) é único no banco. Reenviar o mesmo evento devolve isDuplicate: true e o evento original — não um segundo lançamento.
Franquia e excedente são do modelo, não do relatório. SubscriptionPlanItem.includedUnits define quantas unidades entram no plano. Na geração da fatura, a quantidade cobrada é max(0, consumo − franquia). Na ingestão, quando o item tem franquia e não tem preço de excedente, o evento que estouraria o limite é recusado com 422 — o limite vira barreira, não surpresa no fim do mês.
A fatura é auditável linha a linha. Cada InvoiceLineItem carrega chargeType, quantidade, preço unitário, período e metadados. Quando o cliente pergunta "de onde saiu esse valor", a resposta está na própria fatura.
A fatura finalizada é imutável. Só rascunho aceita alteração e novos itens. Depois de finalize, o documento congela: o caminho para corrigir é anular e emitir outra, que é como faturamento precisa se comportar para sobreviver a auditoria.
O custo não é percentual da sua receita. É a diferença estrutural em relação a Stripe Billing, Chargebee, Recurly e Metronome, e é o argumento que mais cresce de valor conforme o cliente cresce.
04Casos de uso reaisnegócio
Caso 1 — Uma financeira cobra por consulta de crédito sem planilha no fim do mês Cenário ilustrativo
Contexto. Financeira de crédito que vende análise de risco para 40 correspondentes. O contrato padrão é R$ 2.000 de mensalidade com 5.000 consultas inclusas e R$ 0,80 por consulta excedente.
A dor. A esteira de crédito registrava as consultas num log. No dia 1º de cada mês, alguém do financeiro exportava o log, cruzava com a lista de contratos numa planilha e digitava as faturas. Levava dois dias, e cada divergência com o cliente virava uma investigação de horas — porque a fonte de verdade era um CSV que já tinha sido editado.
A solução com o BB. Um BillingProduct "Consulta de crédito" com pricingModel: PER_UNIT e unitPrice: 0.80. Um UsageMeter com eventName: "credito.consulta" apontando para esse produto. Um SubscriptionPlan de R$ 2.000/mês com um item que liga o produto e define includedUnits: 5000. A esteira passa a chamar POST /billing/api/v1/usage a cada consulta, usando o id da consulta como idempotencyKey. No fechamento, POST /billing/api/v1/invoices/generate monta a fatura com a mensalidade como linha SUBSCRIPTION e o excedente como linha USAGE.
O resultado. O fechamento deixa de ser um projeto de dois dias e vira uma chamada por assinatura. A pergunta "por que R$ 3.240 neste mês?" é respondida pelas linhas da própria fatura, e o retry da esteira não gera consulta contada duas vezes.
Caso 2 — Um SaaS B2B trava o excedente em vez de mandar a conta surpresa Cenário ilustrativo
Contexto. Plataforma de assinatura eletrônica que vende pacotes de documentos. O plano Starter inclui 200 documentos por mês.
A dor. O modelo anterior deixava o cliente assinar à vontade e cobrava o excedente na fatura seguinte. Funcionava até chegar o cliente que assinou 3.000 documentos num mês, recebeu uma fatura quinze vezes maior que a esperada e abriu disputa. O time comercial passou a temer o próprio produto.
A solução com o BB. O item de plano recebe includedUnits: 200 e não recebe priceOverride. Com essa combinação, UsageService.checkUsageLimit agrega o consumo do período atual antes de gravar e recusa o evento que ultrapassaria a franquia, devolvendo 422 com currentUsage, limit e attempted no corpo. A aplicação usa esses campos para mostrar "você usou 200 de 200 documentos" e oferecer o upgrade. Quando o cliente aceita excedente, o mesmo item de plano ganha priceOverride e o bloqueio deixa de ser aplicado — o consumo passa a ser cobrado.
O resultado. A franquia vira um limite que o cliente enxerga durante o uso, e não uma surpresa na fatura. O mesmo campo de dados controla os dois comportamentos: franquia dura ou franquia com excedente cobrado.
Caso 3 — Uma operação com 20 mil assinantes deixa de pagar percentual sobre a própria receita Cenário ilustrativo
Contexto. Plataforma que fatura R$ 4 milhões por mês em assinaturas B2B, hoje operando em uma plataforma de subscription billing de mercado.
A dor. A linha de custo do faturamento é percentual do faturamento. Com as tabelas públicas consultadas em 2026-08-16, R$ 4 milhões por mês custariam cerca de R$ 28 mil no Stripe Billing (0,7%), R$ 32 mil no Chargebee pay-as-you-go (0,80%) ou R$ 32 mil mais mensalidade no Metronome (0,8% mais eventos). E essa conta cresce sozinha: dobrar o faturamento dobra o custo de emitir as faturas, ainda que o número de faturas e a complexidade do cálculo sejam os mesmos.
A solução com o BB. O catálogo, os planos e as assinaturas passam a viver no schema billing do próprio banco. As faturas são geradas por chamada de API, e o pagamento efetivo continua no gateway de sempre — o Billing registra o fato do pagamento via POST /billing/api/v1/payments, com externalRef apontando para a transação do gateway.
O resultado. A conta de faturamento deixa de ser função da receita. O custo do gateway continua existindo, porque alguém precisa capturar o dinheiro; o que sai da conta é o percentual pago para calcular e emitir a cobrança.
Caso 4 — O mercado inteiro precifica billing como percentual da sua receita Referência de mercado
Contexto. As páginas públicas de preço dos principais fornecedores de subscription e usage-based billing, consultadas em 2026-08-16, convergem no mesmo modelo: Stripe Billing cobra 0,7% do volume faturado; Chargebee cobra 0,80% do faturamento mensal no pay-as-you-go ou US$ 99/mês mais 0,65%; Recurly cobra US$ 249/mês mais 0,9% acima dos primeiros US$ 40 mil mensais; Metronome — hoje um produto da Stripe — cobra 0,8% do volume faturado mais US$ 0,04 por mil eventos ingeridos, ou seja, mais caro que o próprio Stripe Billing. Paddle leva o modelo ao limite ao operar como merchant of record por 5% mais US$ 0,50 por transação, taxa que já embute processamento e responsabilidade fiscal.
A dor do mercado. O percentual é conveniente para quem começa — custo zero antes da primeira venda — e é exatamente onde ele deixa de fazer sentido depois. O trabalho de emitir uma fatura de R$ 100 e o de emitir uma de R$ 100 mil é o mesmo trabalho computacional, mas o preço difere em mil vezes.
Como a Catalisa endereça. O Billing é módulo da plataforma, e a precificação da Catalisa não usa percentual sobre o faturamento do cliente. Os direcionadores de custo são operacionais — assinaturas ativas, eventos de uso ingeridos, faturas emitidas —, todos com relação direta com o trabalho efetivamente feito.
O resultado. O ponto de virada é matemático: quanto maior o ticket médio e o volume faturado, maior a diferença. Para operação pequena, o percentual dos fornecedores de mercado é competitivo e a §5 diz isso com todas as letras.
Caso 5 — As três ferramentas independentes de medição de uso deixaram de existir em seis meses Referência de mercado
Contexto. Entre janeiro e julho de 2026, a categoria de metering independente foi absorvida por processadores de pagamento e por um CRM. A Stripe concluiu a compra da Metronome em janeiro de 2026 (newsroom da Stripe), cujos clientes incluíam OpenAI, Anthropic e NVIDIA. A Adyen anunciou a compra do Orb por US$ 335 milhões em junho de 2026, com fechamento previsto para julho (press release da Adyen) — o Orb atendia Vercel, Supabase, Replit e Glean. E a Salesforce assinou acordo definitivo pela m3ter também em junho de 2026 (Salesforce).
A dor do mercado. Patrick Collison, da Stripe, justificou a compra dizendo que "a virada para modelos baseados em uso será uma característica definidora da próxima década para a nossa indústria". O co-CEO da Adyen foi na mesma direção, falando de "uma mudança estrutural impulsionada por IA, à medida que empresas migram para modelos de precificação por uso que exigem infraestrutura capaz de processar milhões de eventos em tempo real". Os dois têm razão sobre a tendência — e o efeito prático para quem compra é que a medição de uso, que era produto de empresa independente, virou funcionalidade de quem cobra percentual da transação.
Como a Catalisa endereça. UsageEvent é entidade de primeira classe do módulo: unicidade em (organizationId, idempotencyKey), índice em (organizationId, billingAccountId, eventTimestamp), lote de até mil eventos por requisição e franquia aplicada já na ingestão. O evento fica no seu banco, consultável por SQL, sem passar por API de terceiro e sem que o fornecedor da medição seja o mesmo que processa o seu pagamento.
O resultado. A medição de uso é parte do módulo, não plano superior nem produto de um adquirente. As limitações reais dessa implementação — agregação apenas por soma, precificação por faixas não aplicada — estão na §15, não escondidas aqui.
05Mercado e diferenciaisnegócio
Panorama. O mercado de billing se organizou em três camadas que os compradores costumam confundir. Na primeira estão os gateways com recorrência — no Brasil, Vindi, Iugu, Asaas, Pagar.me —, que capturam o dinheiro e sabem repetir uma cobrança de valor fixo; medição de consumo com agregação e franquia não é o problema que eles resolvem, e nenhum deles anuncia ingestão de eventos de uso em página pública. Na segunda estão as plataformas de subscription billing — Stripe Billing, Chargebee, Recurly, Zuora —, que fazem gestão de assinatura, cobrança de inadimplência e reconhecimento de receita, e cobram percentual do faturamento. Na terceira estavam os especialistas em usage-based — Orb, Metronome, m3ter.
Escrevemos "estavam" de propósito: em 2026 essa terceira camada foi comprada inteira. A Metronome virou produto da Stripe em janeiro, o Orb foi para a Adyen em junho por US$ 335 milhões e a m3ter foi para a Salesforce no mesmo mês (§4, Caso 5). Sobraram como independentes os dois projetos de código aberto — Lago (self-hosted gratuito; a nuvem não publica preço) e Kill Bill (Apache 2.0, na JVM). O posicionamento público do Kill Bill é, literalmente, "billing e infraestrutura de pagamento de código aberto que você mesmo roda, sem lock-in de fornecedor e sem preço baseado em receita".
Isso desenha bem onde o Billing da Catalisa se encaixa: ele oferece o que os dois projetos abertos oferecem — faturamento sem percentual sobre a sua receita — sem que você precise operar a infraestrutura, e dentro de uma plataforma onde identidade, clientes e catálogo já existem.
| Critério | Catalisa Billing | Stripe Billing | Chargebee Flow | Recurly Starter | Metronome (Stripe) | Kill Bill | Vindi |
|---|---|---|---|---|---|---|---|
| Modelo de preço | Sem percentual sobre o faturamento | 0,7% do volume faturado | 0,80%, ou US$ 99/mês + 0,65% | US$ 249/mês + 0,9% | 0,8% + US$ 0,04/mil eventos | Licença zero, você opera | R$ 299–1.119/mês + taxas por transação |
| Cobra percentual da sua receita | Não | Sim | Sim | Sim | Sim | Não | Não (cobra por transação) |
| Franquia de receita isenta | — | Não publicada | Não publicada | US$ 40 mil/mês | Não publicada | — | — |
| Medição de uso (metering) | Sim, com idempotência e lote | Sim (via Metronome) | Sim | Parcial | Sim, é o foco | Parcial | Não é o foco |
| Franquia por item de plano | Sim (includedUnits) | Sim | Sim | Sim | Sim | Sim | Não |
| Bloqueio na ingestão ao estourar franquia | Sim | Não | Não | Não | Não | Não | — |
| Precificação por faixas aplicada no cálculo | Não (§15) | Sim | Sim | Sim | Sim | Sim | — |
| Cálculo de tributos na fatura | Não (§15) | Sim (Stripe Tax) | Sim | Sim | Parcial | Via plugin | Sim, no ecossistema |
| Emissão de NF-e / NFS-e | Não | Não (Brasil) | Não | Não | Não | Não | Sim |
| Cobrança automática por agendador | Não (§15) | Sim | Sim | Sim | Sim | Sim | Sim |
| Portal do assinante pronto | Não | Sim | Sim | Sim | Não | Não | Sim |
| Dunning / recuperação de inadimplência | Não | Sim | Sim | Sim, é ponto forte | Não | Sim | Parcial |
| Captura de pagamento | Não — é o BB Payments | Sim | Via gateway | Via gateway | Via gateway | Via plugin | Sim, é gateway |
| Operação por sua conta | Já vem operado | Não | Não | Não | Não | Sim, integral | Não |
| Dado de faturamento no seu banco | Sim | Não | Não | Não | Não | Sim | Não |
Preços conforme páginas públicas dos fornecedores consultadas em 2026-08-16: Stripe Billing, Chargebee, Recurly, Metronome, Kill Bill, Vindi. Tabelas mudam e variam por região, plano e negociação — confira na data da sua análise. Iugu e Pagar.me não publicam tabela de preços, então não entram na comparação.
Nossos diferenciais
- O custo não é função da sua receita. Stripe Billing, Chargebee, Recurly e Metronome cobram percentual do volume faturado — no caso da Stripe, inclusive sobre transações processadas fora dela. Isso é difícil de copiar não por barreira técnica, mas porque é o modelo de negócio deles: um fornecedor que vive de percentual não tem como oferecer o contrário sem destruir a própria receita. Vale dizer com todas as letras que não somos os únicos: Lago e Kill Bill também não cobram percentual. A diferença nossa em relação a eles é que você não opera a infraestrutura.
- O faturamento mora no mesmo banco que o cliente, o catálogo e a permissão.
BillingAccountreferenciaPersondo Customers;BillingProductguardaexternalProductIdapontando para o Products; a permissão vem do token do IAM. Não existe job de sincronização entre a sua base e a base do fornecedor, porque não existem duas bases. - A franquia é uma barreira na ingestão, não só uma linha no cálculo. Quando o item de plano tem
includedUnitse não tempriceOverride, o evento que estouraria o limite é recusado com422antes de virar dívida. Nenhum dos concorrentes da tabela faz isso — todos assumem que o excedente sempre pode ser cobrado depois. - Idempotência obrigatória, não opcional.
idempotencyKeyé campo exigido pelo schema, com unicidade garantida no banco. Não há caminho em que uma integração com retry gere cobrança dupla por descuido do integrador.
Quando escolher o concorrente. Escolha o Stripe Billing ou o Chargebee se você precisa hoje de precificação por faixas de volume aplicada no cálculo, cálculo de tributos na fatura, portal de autoatendimento do assinante ou recuperação automática de pagamento recusado — o Billing não faz nada disso, e a §15 lista cada um. Se você fatura pouco, o percentual deles também é simplesmente mais barato: 0,7% sobre R$ 50 mil por mês são R$ 350, e nenhuma economia de percentual justifica trocar de plataforma nessa faixa. Escolha o Recurly se a sua dor principal é inadimplência de cartão, porque o motor de retry deles é maduro e o nosso não existe — e note que os primeiros US$ 40 mil de faturamento mensal são isentos do percentual lá. Escolha o Paddle se você vende para o exterior e quer que alguém assuma a responsabilidade fiscal como merchant of record por 5% mais US$ 0,50 por transação; isso é decisão jurídica, não técnica, e não é algo que um módulo resolve. Escolha Vindi, Iugu ou Asaas se o que você precisa é cobrar recorrência de valor fixo no Brasil com boleto, Pix e nota fiscal saindo do mesmo lugar — nesse cenário o Billing seria uma peça a mais sem ganho claro. E escolha Lago ou Kill Bill se a exigência é código aberto sob seu controle: eles resolvem o mesmo problema de modelo de preço que nós, sem custo de licença, ao preço de você operar a infraestrutura — o Kill Bill roda na JVM e a conta de engenharia de plantão é sua.
O Billing ganha quando o problema é cobrar por consumo medido, dentro de uma plataforma que o cliente já usa, sem entregar um percentual da receita ao fornecedor de faturamento — e quando o ciclo de fechamento pode ser disparado pela sua aplicação, porque é assim que ele funciona hoje.
06Modelo de cobrança e ROInegócio
Unidade de cobrança. Precificação em definição. O compromisso de desenho já está tomado e é o argumento comercial central: a Catalisa não cobra percentual sobre o faturamento processado pelo Billing. Não invente número em proposta enquanto a tabela não sair; use a comparação de modelo, que é o que de fato decide a conversa.
O que dispara custo. Assinaturas ativas, eventos de uso ingeridos e faturas emitidas. Os três têm relação direta com trabalho computacional real: uma fatura de R$ 100 mil custa o mesmo que uma de R$ 100 para ser calculada e emitida, e o modelo de cobrança precisa refletir isso.
Por que essa unidade é justa. O percentual sobre faturamento cobra pelo valor da transação, não pelo trabalho de processá-la. É excelente para quem está começando — sem receita, sem custo — e vira penalidade ao crescimento depois. A empresa que dobra o ticket médio sem mudar nada na operação de cobrança passa a pagar o dobro pelo mesmo serviço.
Vale conhecer um detalhe do modelo da Stripe antes de qualquer comparação: a página oficial do Billing diz que os 0,7% incidem sobre "transações do Billing processadas dentro e fora da Stripe". Ou seja, o percentual pode alcançar receita que você recebeu por outro meio, só porque a fatura foi emitida ali. E ele é adicional ao processamento: no Brasil, a mesma página lista 3,99% mais R$ 0,50 por cobrança de cartão bem-sucedida. Somando os dois, uma cobrança de cartão passa de 4,6%.
Comparação de custo — cenário nomeado: operação B2B que fatura R$ 4 milhões por mês, com 800 assinaturas ativas e 6 milhões de eventos de uso medidos por mês.
| Catalisa Billing | Stripe Billing | Chargebee (pay-as-you-go) | Recurly (Starter) | Metronome (Starter) | |
|---|---|---|---|---|---|
| Base de cálculo | Sem percentual sobre faturamento | 0,7% do volume faturado | 0,80% do faturamento mensal | US$ 249/mês + 0,9% acima de US$ 40 mil | 0,8% do faturado + US$ 0,04/mil eventos |
| Ordem de grandeza mensal | Precificação em definição | ~R$ 28.000 | ~R$ 32.000 | ~R$ 36.000 + US$ 249 | ~R$ 32.000 + ~US$ 240 |
| Custo ao dobrar o faturamento | Sem relação direta | Dobra | Dobra | Praticamente dobra | Dobra |
| Dado de faturamento | No seu banco | No fornecedor | No fornecedor | No fornecedor | No fornecedor |
Estimativa. Percentuais retirados das páginas públicas dos fornecedores em 2026-08-16, aplicados diretamente sobre R$ 4 milhões, sem conversão cambial nos itens em dólar e sem considerar descontos por volume, plano anual ou negociação — que existem e reduzem os valores. Serve para comparar modelos, não para cotar. Nenhum destes números é proposta comercial da Catalisa nem do fornecedor citado.
ROI. A conta de guardanapo tem duas parcelas.
A primeira é o percentual que deixa de ser pago. No cenário acima, a ordem de grandeza é de R$ 28 mil a R$ 36 mil por mês — algo entre R$ 336 mil e R$ 432 mil por ano — e essa parcela cresce sozinha conforme o faturamento cresce, o que é o oposto do que se espera de um custo de infraestrutura.
A segunda é a engenharia que não é gasta. Construir medição idempotente, franquia por plano, geração de fatura com itens tipados e ciclo de assinatura com transições válidas é trabalho de meses, não de semanas — e é o tipo de código em que o bug aparece na fatura do cliente, não no teste. O Billing entrega essa base pronta e coberta por testes de unidade e integração.
Contra isso pesa o que ainda não está pronto e precisa entrar na conta honestamente: sem agendador, alguém do seu lado precisa disparar o fechamento do ciclo; sem cálculo de tributo e sem emissão fiscal, a nota continua saindo de outro sistema; sem dunning, a régua de cobrança de inadimplente é sua. Em operação que depende dessas três coisas hoje, o ROI não fecha — e é melhor descobrir isso na proposta do que na implantação.
07Arquitetura
HTTP
│
┌─────────────────────────────────┴───────────────────────────────────────┐
│ Hono app basePath('/billing') + applyCommonMiddleware │
│ bodyLimit 1MB · CORS · security headers · rate limit global │
│ │
│ /api/v1/billing-accounts billingAccountsRouter 5 rotas │
│ /api/v1/products billingProductsRouter 5 rotas │
│ /api/v1/plans plansRouter 7 rotas │
│ /api/v1/config configRouter 2 rotas │
│ /api/v1/subscriptions subscriptionsRouter 7 rotas │
│ /api/v1/meters metersRouter 5 rotas │
│ /api/v1/usage usageRouter 3 rotas │
│ /api/v1/invoices invoicesRouter 8 rotas │
│ /api/v1/payments paymentsRouter 4 rotas │
│ /health sonda simples (sem toque no banco) │
└─────────────────────────────────┬───────────────────────────────────────┘
│
authMiddleware → requirePermission(P) → requireOrganization
│ (nas 46 rotas, sem exceção)
│ Zod parse → ResultAsync<T, AppError>
┌─────────────────────────────────┴───────────────────────────────────────┐
│ services/ │
│ BillingAccountService conta de cobrança, saldo, exclusão lógica │
│ BillingProductService catálogo de itens cobráveis │
│ SubscriptionPlanService planos e itens de plano (franquia) │
│ SubscriptionService ciclo de vida, transições validadas │
│ UsageMeterService definição dos medidores │
│ UsageService ingestão idempotente, limite, resumo │
│ BillingEngineService monta a fatura a partir da assinatura │
│ InvoiceService rascunho, itens, finalizar, anular, pagar │
│ PaymentService registro de pagamento e estorno │
│ ProrationService rateio por dias (cálculo puro) │
│ OrganizationBillingConfigService prazo, moeda, ciclo por organização │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────┴───────────────────────────────────────┐
│ repositories/ (Prisma) → PostgreSQL, schema "billing", 12 modelos │
└─────────────────────────────────┬───────────────────────────────────────┘
│ EventPublisher
▼
Redis Stream "iam-events" → webhooks-engine
O caminho do dinheiro, do consumo à fatura:
aplicação do cliente
│ POST /usage (idempotencyKey obrigatório)
▼
┌───────────────┐ resolve medidor por meterId ou eventName
│ UsageEvent │ verifica franquia do item de plano ──► 422 se estourar
│ (bruto) │ unicidade (organizationId, idempotencyKey)
└───────┬───────┘
│ UsageService.aggregateUsageForPeriod()
│ ⚠ NÃO existe job que chame isto — ver §11 e §15
▼
┌───────────────┐ soma por medidor e período, upsert incremental
│ UsageRecord │ billed = false
│ (agregado) │
└───────┬───────┘
│ POST /invoices/generate (includeUsage: true)
▼
┌───────────────────────────────────────────────────────────┐
│ BillingEngineService │
│ linha SUBSCRIPTION ← basePrice × quantity do plano │
│ linha SUBSCRIPTION ← cada item de plano (produto) │
│ linha USAGE ← max(0, consumo − includedUnits) │
│ × (priceOverride ou unitPrice) │
│ marca os UsageRecord consumidos como billed = true │
└───────────────────────────┬───────────────────────────────┘
▼
Invoice em DRAFT
Decisões não óbvias.
UsageEventeUsageRecordsão tabelas separadas, de propósito. O evento bruto é volume alto e imutável; o registro agregado é o que a fatura consome. Separar permite que a ingestão seja umINSERTbarato, sem contenção com o cálculo, e que a reagregação de um período seja idempotente peloupsertem(organizationId, billingAccountId, meterId, periodStart). O preço dessa escolha é o passo intermediário de agregação — que hoje precisa ser disparado explicitamente, e é a armadilha número um de integração deste módulo (§11).- A fatura só existe a partir do que já foi agregado.
BillingEngineService.addUsageChargeslêUsageRecord, nãoUsageEvent. Gerar fatura sem ter agregado antes produz uma fatura sem nenhuma linha de uso, sem erro nenhum. É silencioso e é a causa mais provável de "a fatura veio sem o consumo". idempotencyKeyé obrigatório no schema Zod, não só único no banco. A unicidade no banco protege contra duplicata; exigir o campo protege contra o integrador que nunca pensou no assunto. A verificação acontece antes da escrita, então o retorno de duplicata é201commeta.isDuplicate: true, e não409— reenviar é seguro por construção.- O lote de uso é processado em sequência, não em paralelo.
recordUsageEventsBatchreduz os eventos um a um justamente para que a checagem de idempotência e a de franquia enxerguem o efeito dos eventos anteriores do mesmo lote. Paralelizar quebraria as duas. O custo é latência proporcional ao tamanho do lote; o limite é 1.000 eventos por requisição. - O lote falha por item, não por requisição. Um evento inválido no meio do lote entra em
errors[]com o índice, e os demais são gravados. Faturamento não pode perder 999 eventos válidos por causa de umbillingAccountIderrado. - Valores monetários são
Decimalno Postgres, convertidos com.toNumber()na borda.Decimal(15,2)para dinheiro,Decimal(15,4)para preço unitário e quantidade — quatro casas porque preço por unidade em cobrança por consumo é frequentemente fração de centavo. A conversão paranumberacontece só na serialização da resposta; o cálculo e o armazenamento não passam por ponto flutuante. - A transição de assinatura é uma tabela, não uma cadeia de
if.VALID_TRANSITIONSemsubscription.service.tsdeclara o grafo, e toda ação consultaisValidTransitionantes de agir. Isso é o que impede "retomar uma assinatura cancelada" de existir como caminho. - Exclusão é lógica em tudo que tem valor histórico.
BillingAccount,BillingProduct,SubscriptionPlan,Subscription,Invoice,PaymenteUsageMetertêmdeletedAt. Fatura emitida não desaparece porque alguém apagou o cliente. /healthnão toca o banco. Diferente do IAM, a sonda do Billing responde estático com nome do serviço e versão. Serve para o orquestrador saber que o processo está de pé, não para saber que o banco está acessível — se você monitora saúde de dependência, monitore pelo IAM ou por uma chamada real de listagem.
Monolito vs. standalone. Em monolito, o app é montado em src/app.ts junto com os demais e resolve serviços pelo container TypeDI. Em standalone — o modo usado em produção — main.ts sobe o Bun na porta de PORT (padrão da tabela: 3010) e define MODULE_SELF=billing. O applyCommonMiddleware existe justamente porque, em standalone, os middlewares globais do monolito não alcançam o módulo: sem ele, o Billing subiria sem limite de corpo, sem cabeçalhos de segurança e sem rate limit.
08Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| BillingAccount | A quem se cobra. Uma entidade de cobrança dentro da organização, opcionalmente ligada a uma Person do Customers. E-mail único por organização. |
| BillingProduct | Um item cobrável do catálogo interno de faturamento: "Mensalidade", "Consulta de crédito", "GB armazenado". Não é o produto comercial do BB Products — pode apontar para ele por externalProductId. |
| SubscriptionPlan | Modelo de assinatura: preço-base, intervalo, dias de teste. É um template, não uma assinatura. |
| SubscriptionPlanItem | Liga um produto ao plano, com quantidade, preço sobrescrito (priceOverride) e franquia (includedUnits). É aqui que mora o "5.000 consultas inclusas". |
| Subscription | A instância: esta conta de cobrança, neste plano, com este período corrente. Guarda unitPrice no momento da contratação, para que mudança de preço no plano não reescreva o histórico. |
| UsageMeter | Como medir. Liga um eventName (único por organização) a um produto e diz como agregar. |
| UsageEvent | Um fato de consumo bruto, com idempotencyKey, quantity e properties livres. Alto volume, imutável. |
| UsageRecord | O consumo já agregado por medidor e período. É o que a fatura lê. Carrega billed para não cobrar duas vezes. |
| Invoice | O documento de cobrança. Rascunho é editável; finalizada, não. |
| InvoiceLineItem | Uma linha da fatura, tipada por chargeType, com período próprio. É a resposta a "de onde saiu esse valor". |
| Payment | O registro de que o dinheiro entrou. O Billing não captura o pagamento; ele registra o fato, com externalRef para a transação do gateway. |
| OrganizationBillingConfig | Preferências de faturamento da organização: moeda padrão, prazo de pagamento, tipo de ciclo. Criada sob demanda no primeiro acesso. |
| includedUnits | A franquia. Unidades que entram no plano antes de começar a cobrar. |
| priceOverride | Preço do produto só para este plano. Sua presença também sinaliza que o excedente é cobrável — e portanto que a franquia não deve bloquear a ingestão. |
Modelo de dados — schema billing no PostgreSQL, 12 modelos.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
OrganizationBillingConfig | billing.organization_billing_configs | Preferências por organização | organizationId (único), defaultCurrency, paymentTermDays, billingCycleType, invoicePrefix, invoiceFooter |
BillingAccount | billing.billing_accounts | Entidade de cobrança | Único (organizationId, email), personId, personOrphaned, balance, status, taxId, deletedAt |
BillingProduct | billing.billing_products | Catálogo cobrável | Único (organizationId, name), pricingModel, unitPrice Decimal(15,4), tiers (JSONB), externalProductId, deletedAt |
SubscriptionPlan | billing.subscription_plans | Template de assinatura | Único (organizationId, name), billingInterval, billingCycleType, basePrice, trialDays, deletedAt |
SubscriptionPlanItem | billing.subscription_plan_items | Produto dentro do plano | Único (planId, productId), quantity, priceOverride, includedUnits |
Subscription | billing.subscriptions | Assinatura ativa | status, currentPeriodStart/End, trialStart/End, cancelAtPeriodEnd, pausedAt, unitPrice, deletedAt |
UsageMeter | billing.usage_meters | Definição de medição | Único (organizationId, eventName), aggregationType, propertyKey, category, productId, deletedAt |
UsageEvent | billing.usage_events | Consumo bruto | Único (organizationId, idempotencyKey), índice (organizationId, billingAccountId, eventTimestamp), quantity Decimal(15,4), properties JSONB |
UsageRecord | billing.usage_records | Consumo agregado | Único (organizationId, billingAccountId, meterId, periodStart), billed, invoiceId |
Invoice | billing.invoices | Fatura | Único (organizationId, invoiceNumber), status, subtotal/taxAmount/discountAmount/total/amountPaid/amountDue, dueDate, finalizedAt, paidAt, voidedAt, deletedAt |
InvoiceLineItem | billing.invoice_line_items | Linha da fatura | chargeType, quantity, unitPrice, amount, periodStart/End, metadata |
Payment | billing.payments | Pagamento registrado | status, amount, paymentMethod, externalRef, refundedAmount, refundedAt, deletedAt |
Enumerações
| Enum | Valores | Observação |
|---|---|---|
BillingAccountStatus | ACTIVE · SUSPENDED · CLOSED | Só ACTIVE aceita nova assinatura |
SubscriptionStatus | TRIAL · ACTIVE · PAUSED · CANCELED · EXPIRED | EXPIRED existe no enum mas nenhum caminho de código o grava (§15) |
InvoiceStatus | DRAFT · PENDING · PAID · OVERDUE · CANCELED · VOID | OVERDUE e CANCELED existem no enum mas nenhum caminho de código os grava (§15) |
PaymentStatusType | PENDING · PROCESSING · COMPLETED · FAILED · REFUNDED | Pagamento registrado pela API nasce COMPLETED |
BillingInterval | DAILY · WEEKLY · MONTHLY · QUARTERLY · YEARLY | |
BillingCycleType | CALENDAR_ALIGNED · ANNIVERSARY | Armazenado; o cálculo de período hoje é sempre aniversário (§15) |
PricingModelType | FLAT_FEE · TIERED · VOLUME · PER_UNIT · PACKAGE | Rótulo de catálogo; o cálculo aplica sempre quantidade × preço unitário (§15) |
ChargeType | SUBSCRIPTION · USAGE · ONE_TIME · PRORATION · CREDIT | O motor emite SUBSCRIPTION e USAGE; os demais só por lançamento manual |
UsageAggregationType | SUM · MAX · LAST · AVERAGE · COUNT_DISTINCT | Armazenado; a agregação implementada é sempre soma (§15) |
Máquina de estados da fatura
POST /invoices
POST /invoices/generate
│
▼
┌───────────────┐
PATCH ───────│ DRAFT │──── POST /:id/line-items
(notas, │ (rascunho) │ (só aqui aceita item novo)
vencimento) └───┬───────┬───┘
│ │
POST /:id/finalize│ │POST /:id/void
BILLING_INVOICES_ │ │BILLING_INVOICES_VOID
FINALIZE │ │
▼ │
┌───────────────┐
│ PENDING │──── POST /:id/void ────┐
│ (finalizada, │ │
│ imutável) │ │
└───────┬───────┘ ▼
│ ┌──────────────┐
POST /payments com │ │ VOID │
invoiceId cobrindo │ │ (terminal) │
o total │ └──────────────┘
▼ void recusa
┌───────────────┐ PAID e VOID
│ PAID │
│ (terminal) │◄── pagamento parcial mantém PENDING
└───────────────┘ e só reduz amountDue
┌────────────────────────────────────────────────────────────────────────┐
│ OVERDUE e CANCELED existem no enum e no banco, mas NENHUM caminho de │
│ código os grava hoje. A consulta InvoiceRepository.findOverdue está │
│ escrita e não tem chamador — falta o agendador. Não construa lógica │
│ de integração esperando receber uma fatura em OVERDUE. Ver §15. │
└────────────────────────────────────────────────────────────────────────┘
Regras aplicadas em InvoiceService:
PATCH e adicionar item → só em DRAFT, senão 400 VALIDATION
finalize → só de DRAFT, senão 400 VALIDATION
void → recusa se status é PAID ou VOID
pagamento → recusa se status é VOID
finalize NÃO recalcula totais → o total é o que os itens somaram
Máquina de estados da assinatura
POST /subscriptions
│
plano tem trialDays? e skipTrial != true
┌───────┴────────┐
sim não
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ TRIAL │─▶│ ACTIVE │◄──┐
└───┬───────┬───┘ └───┬───┬───┬───┘ │ POST /:id/resume
│ │ │ │ │ │ BILLING_SUBSCRIPTIONS_
│ │ ┌───────┘ │ └───────┤ UPDATE
│ │ │ │ │
│ │ │ POST /:id/pause │
│ │ │ │ ┌────┴──────┐
│ │ │ │ │ PAUSED │
│ │ │ │ └────┬──────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────────────────────────────────────────┐
│ CANCELED │ POST /:id/cancel
│ (terminal) │ BILLING_SUBSCRIPTIONS_
└──────────────────────────────────────────┘ CANCEL
┌──────────────────────────────────────────┐
│ EXPIRED │ declarado como destino
│ (terminal) │ válido de TRIAL e ACTIVE,
└──────────────────────────────────────────┘ mas nenhum código grava
Grafo declarado em VALID_TRANSITIONS (subscription.service.ts):
TRIAL → ACTIVE, CANCELED, EXPIRED
ACTIVE → PAUSED, CANCELED, EXPIRED
PAUSED → ACTIVE, CANCELED
CANCELED → (nada)
EXPIRED → (nada)
Duas armadilhas que o grafo não mostra:
1. TRIAL → ACTIVE não tem endpoint. A transição existe em
SubscriptionService.activateFromTrial(), que nenhuma rota expõe e
nenhum job chama. Assinatura em teste permanece em TRIAL.
2. cancel com cancelAtPeriodEnd = true NÃO muda o status. Ele apenas
grava a flag; a assinatura segue ACTIVE. E findDueForBilling exclui
quem tem a flag ligada, então essa assinatura também para de gerar
fatura. Fechar o ciclo dela é responsabilidade da sua aplicação.
09Referência da API
Prefixo: /billing. Em standalone (staging), a base é https://billing.bb.stg.catalisa.app.
Todas as 46 rotas aplicam, nesta ordem: authMiddleware → requirePermission(...) → requireOrganization. Não existe rota pública além de GET /billing/health, e não existe rota que aceite organizationId no corpo — o tenant vem sempre do token.
Todos os corpos de escrita seguem envelope JSON:API — { "data": { "type": "...", "attributes": { ... } } }. Paginação por page[number] e page[size] (padrão 20, máximo 100). Filtros por filter[campo].
Contas de cobrança — /billing/api/v1/billing-accounts
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/billing-accounts | Cria conta de cobrança | BILLING_ACCOUNTS_CREATE |
GET | /billing/api/v1/billing-accounts | Lista contas. Filtros: filter[status], filter[personId] | BILLING_ACCOUNTS_READ |
GET | /billing/api/v1/billing-accounts/:id | Busca conta | BILLING_ACCOUNTS_READ |
PATCH | /billing/api/v1/billing-accounts/:id | Atualiza conta | BILLING_ACCOUNTS_UPDATE |
DELETE | /billing/api/v1/billing-accounts/:id | Exclusão lógica. 204 | BILLING_ACCOUNTS_DELETE |
Produtos de faturamento — /billing/api/v1/products
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/products | Cria produto cobrável | BILLING_PRODUCTS_CREATE |
GET | /billing/api/v1/products | Lista. Filtros: filter[isActive], filter[pricingModel] | BILLING_PRODUCTS_READ |
GET | /billing/api/v1/products/:id | Busca produto | BILLING_PRODUCTS_READ |
PATCH | /billing/api/v1/products/:id | Atualiza produto | BILLING_PRODUCTS_UPDATE |
DELETE | /billing/api/v1/products/:id | Exclusão lógica. 204 | BILLING_PRODUCTS_DELETE |
Planos — /billing/api/v1/plans
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/plans | Cria plano | BILLING_PLANS_CREATE |
GET | /billing/api/v1/plans | Lista. Filtros: filter[isActive], filter[billingInterval] | BILLING_PLANS_READ |
GET | /billing/api/v1/plans/:id | Busca plano com itens | BILLING_PLANS_READ |
PATCH | /billing/api/v1/plans/:id | Atualiza plano | BILLING_PLANS_UPDATE |
DELETE | /billing/api/v1/plans/:id | Exclusão lógica. 204 | BILLING_PLANS_DELETE |
POST | /billing/api/v1/plans/:id/items | Adiciona produto ao plano. 201 | BILLING_PLANS_UPDATE |
DELETE | /billing/api/v1/plans/:planId/items/:itemId | Remove item do plano. 204 | BILLING_PLANS_UPDATE |
Configuração da organização — /billing/api/v1/config
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /billing/api/v1/config | Lê a configuração; cria com padrões se não existir | BILLING_CONFIG_MANAGE |
PATCH | /billing/api/v1/config | Atualiza moeda, ciclo, prazo, prefixo e rodapé | BILLING_CONFIG_MANAGE |
Assinaturas — /billing/api/v1/subscriptions
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/subscriptions | Cria assinatura. 201 | BILLING_SUBSCRIPTIONS_CREATE |
GET | /billing/api/v1/subscriptions | Lista. Filtros: filter[billingAccountId], filter[planId], filter[status] | BILLING_SUBSCRIPTIONS_READ |
GET | /billing/api/v1/subscriptions/:id | Busca assinatura | BILLING_SUBSCRIPTIONS_READ |
PATCH | /billing/api/v1/subscriptions/:id | Altera apenas quantity | BILLING_SUBSCRIPTIONS_UPDATE |
POST | /billing/api/v1/subscriptions/:id/cancel | Cancela agora ou ao fim do período | BILLING_SUBSCRIPTIONS_CANCEL |
POST | /billing/api/v1/subscriptions/:id/pause | Pausa (só de ACTIVE) | BILLING_SUBSCRIPTIONS_UPDATE |
POST | /billing/api/v1/subscriptions/:id/resume | Retoma (só de PAUSED) | BILLING_SUBSCRIPTIONS_UPDATE |
Medidores de uso — /billing/api/v1/meters
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/meters | Cria medidor. 201 | BILLING_METERS_CREATE |
GET | /billing/api/v1/meters | Lista. Filtros: filter[productId], filter[isActive] | BILLING_METERS_READ |
GET | /billing/api/v1/meters/:id | Busca medidor | BILLING_METERS_READ |
PATCH | /billing/api/v1/meters/:id | Atualiza medidor | BILLING_METERS_UPDATE |
DELETE | /billing/api/v1/meters/:id | Exclusão lógica. 204 | BILLING_METERS_DELETE |
Uso — /billing/api/v1/usage
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/usage | Registra um evento de uso. 201 | BILLING_USAGE_CREATE |
POST | /billing/api/v1/usage/batch | Registra lote de 1 a 1.000 eventos. 201 | BILLING_USAGE_CREATE |
GET | /billing/api/v1/usage/summary | Resumo por medidor no período | BILLING_USAGE_READ |
Faturas — /billing/api/v1/invoices
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/invoices | Cria fatura em branco (DRAFT). 201 | BILLING_INVOICES_CREATE |
POST | /billing/api/v1/invoices/generate | Gera fatura a partir de uma assinatura. 201 | BILLING_INVOICES_CREATE |
GET | /billing/api/v1/invoices | Lista. Filtros: filter[billingAccountId], filter[status], filter[startDate], filter[endDate] | BILLING_INVOICES_READ |
GET | /billing/api/v1/invoices/:id | Busca fatura com itens | BILLING_INVOICES_READ |
PATCH | /billing/api/v1/invoices/:id | Altera notes e dueDate. Só em DRAFT | BILLING_INVOICES_UPDATE |
POST | /billing/api/v1/invoices/:id/line-items | Adiciona linha. Só em DRAFT. 201 | BILLING_INVOICES_UPDATE |
POST | /billing/api/v1/invoices/:id/finalize | DRAFT → PENDING, congela a fatura | BILLING_INVOICES_FINALIZE |
POST | /billing/api/v1/invoices/:id/void | Anula. Recusa PAID e VOID | BILLING_INVOICES_VOID |
Pagamentos — /billing/api/v1/payments
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /billing/api/v1/payments | Registra pagamento. 201 | BILLING_PAYMENTS_READ ⚠ |
GET | /billing/api/v1/payments | Lista. Filtros: filter[billingAccountId], filter[invoiceId], filter[status] | BILLING_PAYMENTS_READ |
GET | /billing/api/v1/payments/:id | Busca pagamento | BILLING_PAYMENTS_READ |
POST | /billing/api/v1/payments/:id/refund | Estorna total ou parcial | BILLING_PAYMENTS_REFUND |
⚠ Atenção ao registrar pagamento. A rota de criação exige
BILLING_PAYMENTS_READ, nãoBILLING_PAYMENTS_RECORD. A permissãoBILLING_PAYMENTS_RECORDexiste no vocabulário do IAM, mas o router ainda não a usa — há um comentário no código dizendo que não haveria permissão de criação, o que deixou de ser verdade. Documentado aqui porque é o que o código faz hoje; consta como pendência na §15. Ao montar papéis, considere que quem consegue ler pagamentos também consegue registrar pagamento.
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /billing/health | Responde { status, service, version, timestamp }. Não toca banco nem Redis. |
POST /billing/api/v1/usage
Registra um evento de consumo. É o endpoint de maior volume do módulo.
Request
{
"data": {
"type": "usage-events",
"attributes": {
"billingAccountId": "0f1c1d3a-1111-4a2b-9c3d-000000000001",
"eventName": "credito.consulta",
"idempotencyKey": "consulta-8f3a2b19",
"quantity": 1,
"eventTimestamp": "2026-08-16T13:45:00Z",
"properties": { "cpf": "***", "produto": "consignado" }
}
}
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
billingAccountId | uuid | Sim | Conta de cobrança que consumiu |
meterId | uuid | Um dos dois | Medidor por id |
eventName | string | Um dos dois | Medidor pelo nome do evento. Resolve só medidor ativo |
idempotencyKey | string (1–255) | Sim | Chave de deduplicação. Use o id natural da operação |
quantity | number > 0 | Sim | Quantidade consumida |
eventTimestamp | date | Não | Padrão: agora. É a data usada na agregação por período |
properties | object | Não | Metadados livres, guardados em JSONB |
Resposta 201
{
"data": {
"type": "usage-events",
"id": "9a2f...",
"links": { "self": "/billing/api/v1/usage/9a2f..." },
"attributes": {
"billingAccountId": "0f1c1d3a-1111-4a2b-9c3d-000000000001",
"meterId": "3c7e...",
"idempotencyKey": "consulta-8f3a2b19",
"eventTimestamp": "2026-08-16T13:45:00.000Z",
"quantity": 1
}
},
"meta": { "isDuplicate": false }
}
Reenviar a mesma idempotencyKey devolve 201 com meta.isDuplicate: true e o evento original — não um erro e não um segundo lançamento.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod, ou nem meterId nem eventName informados |
403 | — | Token sem organizationId, ou sem BILLING_USAGE_CREATE |
404 | NOT_FOUND | Conta de cobrança ou medidor inexistente na organização |
422 | VALIDATION | Franquia estourada. details traz currentUsage, limit, attempted, meter, billingAccountId |
POST /billing/api/v1/usage/batch
Mesmo formato, em lote de 1 a 1.000 eventos.
{
"data": [
{ "type": "usage-events", "attributes": { "billingAccountId": "...", "eventName": "credito.consulta", "idempotencyKey": "c-1", "quantity": 1 } },
{ "type": "usage-events", "attributes": { "billingAccountId": "...", "eventName": "credito.consulta", "idempotencyKey": "c-2", "quantity": 1 } }
]
}
Resposta 201 — sucesso parcial é o comportamento normal:
{
"data": [ { "type": "usage-events", "id": "...", "attributes": { } } ],
"meta": { "recorded": 1, "duplicates": 1, "errors": 0 }
}
Quando há falhas, o campo errors aparece com { index, error } por item. Sempre leia meta: um lote pode retornar 201 com metade dos eventos rejeitados.
POST /billing/api/v1/invoices/generate
Monta a fatura de um ciclo a partir da assinatura. É o endpoint central do fechamento.
Request
{
"data": {
"attributes": {
"subscriptionId": "5b8d...",
"includeUsage": true
}
}
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
subscriptionId | uuid | Sim | Assinatura a faturar |
includeUsage | boolean | Não | Padrão true. Inclui UsageRecord não faturados com periodEnd até o fim do período corrente |
O que o motor faz, nesta ordem: gera o número da fatura, cria a fatura em DRAFT, adiciona a linha SUBSCRIPTION do preço-base × quantidade, adiciona uma linha SUBSCRIPTION por item de plano, adiciona linhas USAGE a partir dos registros agregados descontando includedUnits, marca esses registros como billed e recalcula os totais.
Resposta 201 — fatura em DRAFT com os itens. Chame finalize depois para congelar.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | subscriptionId ausente ou não é UUID |
404 | NOT_FOUND | Assinatura inexistente na organização |
A armadilha.
includeUsage: truelêUsageRecord, nãoUsageEvent. Se ninguém agregou o período antes, a fatura sai sem nenhuma linha de uso e sem erro. Veja a receita "Fechar um ciclo de faturamento" na §11.
POST /billing/api/v1/invoices/:id/finalize
DRAFT → PENDING. Grava finalizedAt e finalizedBy (o sub do token) e publica billing.invoice.finalized.
Depois disso a fatura não aceita PATCH nem novos itens. finalize não recalcula os totais — ele congela o que os itens já somaram. Confira o total antes de finalizar.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Fatura não está em DRAFT |
404 | NOT_FOUND | Fatura inexistente na organização |
POST /billing/api/v1/payments
Registra que o dinheiro entrou. O Billing não captura pagamento — quem captura é o gateway, ou o building block Payments.
Request
{
"data": {
"type": "payments",
"attributes": {
"billingAccountId": "0f1c1d3a-1111-4a2b-9c3d-000000000001",
"invoiceId": "7d2a...",
"amount": 3240.00,
"paymentMethod": "pix",
"externalRef": "E00038166202608161345s0123456789",
"metadata": { "gateway": "payments-bb" }
}
}
}
Com invoiceId, o serviço soma ao amountPaid da fatura e recalcula amountDue; quando o saldo chega a zero ou menos, a fatura vira PAID e o evento billing.invoice.paid é publicado. Pagamento parcial mantém PENDING. Em ambos os casos, o balance da conta de cobrança é incrementado pelo valor.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Fatura está VOID, ou já está PAID |
404 | NOT_FOUND | Conta de cobrança ou fatura inexistente |
POST /billing/api/v1/subscriptions/:id/cancel
{ "data": { "attributes": { "cancelAtPeriodEnd": true } } }
Corpo opcional; ausente, equivale a cancelAtPeriodEnd: false.
cancelAtPeriodEnd | O que acontece |
|---|---|
false (padrão) | status vira CANCELED e canceledAt é gravado. Terminal. |
true | O status não muda. Só a flag é gravada. A assinatura segue ACTIVE e sai da lista de findDueForBilling — ou seja, para de gerar fatura, mas ninguém a cancela no fim do período. Fechar o ciclo é sua responsabilidade (§15). |
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Transição inválida — assinatura já CANCELED ou EXPIRED |
404 | NOT_FOUND | Assinatura inexistente na organização |
10Início rápido
Do zero à primeira fatura com assinatura e uso medido. As chamadas seguem o ambiente de staging descrito em AMBIENTES.md.
Verificação. A sonda
GET /billing/healthfoi executada contra staging durante a redação deste documento (respondeu200). As demais chamadas foram conferidas linha a linha contraroutes/*.tse os schemas Zod, mas não foram executadas nesta redação — elas criam dados. Rode em staging, nunca em produção.
1. Autenticar no IAM
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
BASE=https://billing.bb.stg.catalisa.app/billing/api/v1
AUTH="Authorization: Bearer $TOKEN"; JSON="Content-Type: application/json"
2. Conferir que o serviço responde
curl -s https://billing.bb.stg.catalisa.app/billing/health | jq
{ "status": "ok", "service": "billing", "version": "5a7bc56", "timestamp": "2026-08-16T16:29:46.031Z" }
3. Criar a conta de cobrança
ACC=$(curl -s -X POST "$BASE/billing-accounts" -H "$AUTH" -H "$JSON" -d '{
"data": { "type": "billing-accounts", "attributes": {
"name": "Correspondente Exemplo LTDA",
"email": "financeiro@correspondente-exemplo.com.br",
"currency": "BRL", "taxId": "00000000000191", "taxIdType": "CNPJ" }}}' \
| jq -r '.data.id')
echo "conta: $ACC"
4. Criar o produto cobrável e o medidor
PROD=$(curl -s -X POST "$BASE/products" -H "$AUTH" -H "$JSON" -d '{
"data": { "type": "billing-products", "attributes": {
"name": "Consulta de credito", "pricingModel": "PER_UNIT",
"unitPrice": 0.80, "unitLabel": "consulta" }}}' | jq -r '.data.id')
METER=$(curl -s -X POST "$BASE/meters" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"usage-meters\", \"attributes\": {
\"productId\": \"$PROD\", \"name\": \"Consultas de credito\",
\"eventName\": \"credito.consulta\", \"aggregationType\": \"SUM\" }}}" \
| jq -r '.data.id')
5. Criar o plano e amarrar a franquia
PLAN=$(curl -s -X POST "$BASE/plans" -H "$AUTH" -H "$JSON" -d '{
"data": { "type": "subscription-plans", "attributes": {
"name": "Correspondente Padrao", "billingInterval": "MONTHLY",
"billingCycleType": "ANNIVERSARY", "basePrice": 2000.00, "trialDays": 0 }}}' \
| jq -r '.data.id')
# includedUnits = franquia; priceOverride presente => excedente cobrável
curl -s -X POST "$BASE/plans/$PLAN/items" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"subscription-plan-items\", \"attributes\": {
\"productId\": \"$PROD\", \"quantity\": 1,
\"includedUnits\": 5000, \"priceOverride\": 0.80 }}}" | jq '.data.attributes.items'
6. Assinar
SUB=$(curl -s -X POST "$BASE/subscriptions" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"subscriptions\", \"attributes\": {
\"billingAccountId\": \"$ACC\", \"planId\": \"$PLAN\", \"quantity\": 1 }}}" \
| jq -r '.data.id')
7. Medir consumo
curl -s -X POST "$BASE/usage" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"usage-events\", \"attributes\": {
\"billingAccountId\": \"$ACC\", \"eventName\": \"credito.consulta\",
\"idempotencyKey\": \"consulta-0001\", \"quantity\": 1 }}}" | jq '.meta'
{ "isDuplicate": false }
Reenvie o mesmo comando: a resposta continua 201, mas meta.isDuplicate vira true e nenhum evento novo é criado. É a garantia de que retry não vira cobrança dupla.
8. Conferir o consumo do período
curl -s -G "$BASE/usage/summary" -H "$AUTH" \
--data-urlencode "billingAccountId=$ACC" \
--data-urlencode "startDate=2026-08-01T00:00:00Z" \
--data-urlencode "endDate=2026-08-31T23:59:59Z" | jq '.data.attributes.items'
9. Gerar e finalizar a fatura
INV=$(curl -s -X POST "$BASE/invoices/generate" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"attributes\": { \"subscriptionId\": \"$SUB\", \"includeUsage\": true }}}" \
| jq -r '.data.id')
curl -s "$BASE/invoices/$INV" -H "$AUTH" \
| jq '{numero: .data.attributes.invoiceNumber, status: .data.attributes.status,
total: .data.attributes.total, itens: .data.attributes.lineItems}'
curl -s -X POST "$BASE/invoices/$INV/finalize" -H "$AUTH" \
| jq '.data.attributes.status'
"PENDING"
A fatura deste passo virá sem a linha de uso, porque o consumo do passo 7 ainda não foi agregado em
UsageRecord. Isso é esperado e é a armadilha mais importante do módulo — a receita seguinte resolve.
10. Registrar o pagamento
curl -s -X POST "$BASE/payments" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"payments\", \"attributes\": {
\"billingAccountId\": \"$ACC\", \"invoiceId\": \"$INV\",
\"amount\": 2000.00, \"paymentMethod\": \"pix\",
\"externalRef\": \"E0003816620260816...\" }}}" | jq '.data.attributes.status'
curl -s "$BASE/invoices/$INV" -H "$AUTH" | jq '.data.attributes.status'
"PAID"
Credenciais de staging. Nunca use credencial de produção em documentação ou script de exemplo.
11Receitas
Fechar um ciclo de faturamento com uso medido
Objetivo. Emitir a fatura do ciclo com a assinatura e o consumo excedente nas linhas corretas.
O ponto crítico: existe um passo de agregação entre o evento e a fatura, e ele não roda sozinho.
UsageEvent (bruto) ──agregação──▶ UsageRecord ──generate──▶ Invoice
▲
└── UsageService.aggregateUsageForPeriod()
não tem endpoint e não tem job
Enquanto o agendador não existir (§15), o fechamento tem duas formas:
Forma A — lançar o consumo direto na fatura. É o caminho que funciona só com a API pública hoje.
# 1. Consultar o consumo agregado do período
USO=$(curl -s -G "$BASE/usage/summary" -H "$AUTH" \
--data-urlencode "billingAccountId=$ACC" \
--data-urlencode "meterId=$METER" \
--data-urlencode "startDate=2026-08-01T00:00:00Z" \
--data-urlencode "endDate=2026-08-31T23:59:59Z")
QTD=$(echo "$USO" | jq -r '.data.attributes.items[0].quantity')
FRANQUIA=$(echo "$USO" | jq -r '.data.attributes.items[0].includedUnits // 0')
EXCEDENTE=$(( QTD > FRANQUIA ? QTD - FRANQUIA : 0 ))
# 2. Gerar a fatura só com a assinatura
INV=$(curl -s -X POST "$BASE/invoices/generate" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"attributes\": { \"subscriptionId\": \"$SUB\", \"includeUsage\": false }}}" \
| jq -r '.data.id')
# 3. Lançar o excedente como linha USAGE, enquanto ainda é DRAFT
if [ "$EXCEDENTE" -gt 0 ]; then
curl -s -X POST "$BASE/invoices/$INV/line-items" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"invoice-line-items\", \"attributes\": {
\"subscriptionId\": \"$SUB\", \"chargeType\": \"USAGE\",
\"description\": \"Consultas excedentes (agosto/2026)\",
\"quantity\": $EXCEDENTE, \"unitPrice\": 0.80,
\"periodStart\": \"2026-08-01T00:00:00Z\",
\"periodEnd\": \"2026-08-31T23:59:59Z\" }}}" > /dev/null
fi
# 4. Conferir e finalizar
curl -s "$BASE/invoices/$INV" -H "$AUTH" | jq '.data.attributes | {total, lineItems}'
curl -s -X POST "$BASE/invoices/$INV/finalize" -H "$AUTH" | jq '.data.attributes.status'
Forma B — agregar antes, em processo interno. Se você roda o Billing em monolito ou tem acesso ao container, chame UsageService.aggregateUsageForPeriod(organizationId, billingAccountId, periodStart, periodEnd) no seu agendador e depois use includeUsage: true. É o caminho que o motor foi desenhado para percorrer.
Armadilhas.
includeUsage: truesem agregação prévia produz fatura sem uso e sem erro. É silencioso. Se a sua fatura veio só com a mensalidade, é isto.finalizenão recalcula o total. ConfiratotalelineItemsantes; depois de finalizada, o caminho évoide refazer.generatenão é idempotente. Chamar duas vezes cria duas faturas emDRAFT, cada uma com um número. Só que a segunda não terá as linhas de uso, porque a primeira já marcou osUsageRecordcomobilled. Anule a duplicada.- O número da fatura vem de
COUNT(*) + 1por organização, no formatoINV-{ano}-{000001}. Duas gerações concorrentes podem colidir na unicidade(organizationId, invoiceNumber). Serialize o fechamento por organização. invoicePrefixda configuração não é aplicado ao número gerado — o prefixo é sempreINV-(§15).taxAmountediscountAmountsão sempre zero.totalé igual asubtotal. Imposto e desconto precisam entrar como linha explícita, ou vir de outro sistema.
Vender com franquia dura (bloqueio) ou com excedente cobrado
Objetivo. Escolher, por item de plano, se estourar a franquia trava o uso ou vira cobrança.
A decisão é feita por dois campos em SubscriptionPlanItem, e a lógica está em UsageService.checkUsageLimit:
includedUnits | priceOverride | Comportamento na ingestão |
|---|---|---|
| definido | ausente | Bloqueia. Evento que ultrapassaria a franquia recebe 422 com currentUsage, limit, attempted |
| definido | definido | Não bloqueia. Excedente é cobrado no fechamento |
| ausente | qualquer | Não bloqueia. Tudo é cobrado |
# Franquia dura: 200 documentos, sem excedente
curl -s -X POST "$BASE/plans/$PLAN/items" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"attributes\": { \"productId\": \"$PROD\", \"includedUnits\": 200 }}}"
# Franquia com excedente cobrado a R$ 1,20
curl -s -X POST "$BASE/plans/$PLAN/items" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"attributes\": { \"productId\": \"$PROD\",
\"includedUnits\": 200, \"priceOverride\": 1.20 }}}"
Armadilhas.
- O bloqueio só vale se houver assinatura
ACTIVEouTRIALna conta de cobrança. Sem assinatura, nada é bloqueado — o evento é aceito. - A franquia é contada contra o período corrente da assinatura, não contra o mês calendário. Se a assinatura começou dia 12, a janela vai do dia 12 ao dia 12.
- Um item por produto por plano. Repetir o
productIdretorna409 CONFLICT. - A troca de bloqueio para excedente é um
PATCHno plano? Não — não há rota de atualização de item de plano. Remova o item (DELETE /plans/:planId/items/:itemId) e adicione de novo compriceOverride. - Em lote, o bloqueio enxerga os eventos anteriores do mesmo lote, porque o processamento é sequencial. O evento que estoura entra em
errors[]; os anteriores ficam gravados.
Registrar pagamento vindo de um gateway externo
Objetivo. Fechar a fatura quando o dinheiro entrou por Pix, boleto ou cartão em outro sistema.
# webhook do gateway confirmou a liquidação
curl -s -X POST "$BASE/payments" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"type\": \"payments\", \"attributes\": {
\"billingAccountId\": \"$ACC\", \"invoiceId\": \"$INV\",
\"amount\": 3240.00, \"paymentMethod\": \"pix\",
\"externalRef\": \"E00038166202608161345s0123456789\",
\"metadata\": { \"origem\": \"payments-bb\", \"txid\": \"...\" } }}}"
# conferir se a fatura fechou
curl -s "$BASE/invoices/$INV" -H "$AUTH" \
| jq '.data.attributes | {status, total, amountPaid, amountDue}'
Armadilhas.
- Não há idempotência em pagamento. Reprocessar o mesmo webhook cria um segundo
Paymente credita o valor de novo. Guarde oexternalRefdo seu lado e consulteGET /payments?filter[invoiceId]=...antes de registrar, ou deduplique no seu consumidor de webhook. - Pagamento sem
invoiceIdnão abate fatura nenhuma. Ele só incrementa obalanceda conta. Use para adiantamento e crédito, sabendo que não existe hoje rotina de aplicar esse saldo numa fatura futura. - Pagar mais que o total leva a fatura a
PAIDe zeraamountDue— o excedente não vira crédito automático. - Fatura
VOIDrecusa pagamento com400; fatura jáPAID, também. - O estorno (
POST /payments/:id/refund) muda oPaymentparaREFUNDEDno estorno total, mas não reabre a fatura: ela permanecePAID. Reverter a fatura é decisão sua, e hoje exigevoidmais reemissão.
Migrar um cliente de plano
Objetivo. Trocar o plano de uma assinatura existente.
Não existe endpoint de troca de plano. PATCH /subscriptions/:id altera apenas quantity. O caminho é cancelar e recriar:
# 1. Cancelar a assinatura atual (imediato)
curl -s -X POST "$BASE/subscriptions/$SUB/cancel" -H "$AUTH" -H "$JSON" \
-d '{"data":{"attributes":{"cancelAtPeriodEnd":false}}}' | jq '.data.attributes.status'
# 2. Criar a nova
NOVA=$(curl -s -X POST "$BASE/subscriptions" -H "$AUTH" -H "$JSON" -d "{
\"data\": { \"attributes\": { \"billingAccountId\": \"$ACC\",
\"planId\": \"$NOVO_PLANO\", \"quantity\": 1, \"skipTrial\": true }}}" | jq -r '.data.id')
# 3. Lançar o rateio manualmente na próxima fatura, se houver
Armadilhas.
- O rateio não é aplicado automaticamente. O
ProrationServicecalcula rateio proporcional por dias, mas nenhum fluxo o chama — nenhuma linhaPRORATIONé criada por conta própria. Se você cobra pró-rata, calcule e lance como item da fatura emDRAFT. - Uma conta não pode ter duas assinaturas ativas no mesmo plano. A segunda retorna
409 CONFLICT. Planos diferentes, sem restrição. - Assinatura em conta de cobrança que não está
ACTIVEé recusada com400. skipTrial: trueevita que a migração conceda um novo período de teste — quase sempre é o que você quer numa troca de plano.- Alterar
basePricedo plano não altera assinaturas existentes: ounitPricefoi congelado na contratação. Isso é proposital e é o que preserva o histórico.
Descobrir por que uma fatura veio errada
Objetivo. Diagnóstico na ordem que resolve mais rápido.
# 1. O que a fatura efetivamente cobrou
curl -s "$BASE/invoices/$INV" -H "$AUTH" \
| jq '.data.attributes | {status, subtotal, total, amountPaid, amountDue,
itens: [.lineItems[] | {chargeType, description, quantity, unitPrice, amount}]}'
# 2. Quanto o cliente consumiu de fato no período
curl -s -G "$BASE/usage/summary" -H "$AUTH" \
--data-urlencode "billingAccountId=$ACC" \
--data-urlencode "startDate=2026-08-01T00:00:00Z" \
--data-urlencode "endDate=2026-08-31T23:59:59Z" | jq '.data.attributes.items'
# 3. O plano tem a franquia e o preço que o contrato diz?
curl -s "$BASE/plans/$PLAN" -H "$AUTH" | jq '.data.attributes.items'
# 4. A assinatura está no período que você acha que está?
curl -s "$BASE/subscriptions/$SUB" -H "$AUTH" \
| jq '.data.attributes | {status, currentPeriodStart, currentPeriodEnd, quantity, unitPrice}'
Ordem das causas prováveis, da mais para a menos comum:
- Faltou linha de uso → o período não foi agregado antes do
generate. É a causa número um. - Valor de uso menor que o esperado →
includedUnitsdescontou; confira o item de plano. - Total não bate com a soma das linhas → alguém finalizou depois de adicionar item por outro caminho;
finalizenão recalcula. - Consumo aparece no
summarymas não na fatura → osUsageRecordcorrespondentes já estavambilled, cobrados numa fatura anterior. - Período errado → a janela é o período da assinatura, não o mês calendário, e
billingCycleTypenão altera esse cálculo hoje (§15).
billedQuantityeunbilledQuantitydosummarynão são confiáveis. Hoje o serviço devolve semprebilledQuantity: 0eunbilledQuantityigual ao total. UsequantityeincludedUnits; ignore os outros dois (§15).
12Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token com organizationId e as permissões BILLING_*. As 46 rotas exigem os dois | Sim |
| Customers | BillingAccount.personId aponta para Person. Excluir a pessoa deixa personId nulo e a conta marcada como órfã | Não |
| Products | BillingProduct.externalProductId liga o item cobrável ao produto comercial | Não |
| Payments | Captura o dinheiro (Pix, cartão, boleto). O Billing registra o fato com externalRef apontando para a transação | Não |
| Webhooks Engine | Consome os eventos publicados no stream Redis e entrega ao sistema do cliente | Não |
| Audit Trail | Registra quem finalizou, anulou e estornou. A fatura já guarda createdBy e finalizedBy | Não |
| Pricing Engine | Precifica por risco antes da contratação; o Billing cobra o preço já definido | Não |
| Commerce | Venda e catálogo comercial; o Billing cuida da cobrança recorrente do que foi vendido | Não |
┌──────────────┐ define quem é o cliente ┌──────────────────────────┐
│ Customers │──────── Person ────────────▶│ │
└──────────────┘ │ BILLING │
┌──────────────┐ define o que se vende │ │
│ Products │─── externalProductId ──────▶│ BillingAccount │
└──────────────┘ │ Plan + PlanItem │
┌──────────────┐ preço por risco │ Subscription │
│Pricing Engine│──── basePrice ─────────────▶│ UsageMeter/Event/Record │
└──────────────┘ │ Invoice + LineItem │
┌──────────────┐ quem pode o quê │ Payment │
│ IAM │──── token + BILLING_* ─────▶│ │
└──────────────┘ └───────────┬──────────────┘
│
eventos billing.* no Redis Stream │
▼
┌──────────────┐ captura o dinheiro ┌──────────────────────────┐
│ Payments │◀─── externalRef ────────────│ Webhooks Engine │
│ (Pix/cartão) │ │ entrega ao ERP/CRM │
└──────────────┘ └──────────────────────────┘
│
┌──────────────┐ ▼
│ Audit Trail │◀──── quem finalizou/anulou ─────── sistemas do cliente
└──────────────┘
A divisão de responsabilidade que mais gera dúvida na venda: o Billing decide quanto cobrar e emite o documento; o Payments recebe o dinheiro. São problemas diferentes e vendidos separadamente. Um gateway de mercado — Vindi, Iugu, Asaas, Stripe — resolve o segundo e apenas arranha o primeiro quando a cobrança é de valor fixo. Quando a cobrança depende de consumo medido com franquia, o gateway não tem como saber o valor: alguém precisa calcular, e é isso que o Billing faz.
Eventos publicados — no stream Redis iam-events, consumidos pelo Webhooks Engine.
| Evento | Quando |
|---|---|
billing.account.created · .updated · .deleted | Ciclo da conta de cobrança |
billing.subscription.created | Assinatura criada (TRIAL ou ACTIVE) |
billing.subscription.activated | Saída do teste para ativa |
billing.subscription.paused · .resumed · .canceled | Ações de ciclo |
billing.subscription.quantity_changed | PATCH que alterou a quantidade |
billing.invoice.created · .finalized · .paid · .voided | Ciclo da fatura |
billing.payment.completed · .refunded | Pagamento e estorno |
billing.usage.event_recorded | Cada evento de uso novo (não os duplicados) |
billing.config.updated | Configuração da organização alterada |
billing.invoice.overdue,billing.subscription.expired,billing.payment.createdebilling.payment.failedestão declarados no enumBillingEventType, mas nenhum caminho de código os publica hoje. Não construa automação esperando recebê-los (§15).O evento
billing.usage.event_recordedé publicado por evento. Em ingestão de alto volume, filtre a assinatura de webhook por tipo, ou o consumidor vai receber tudo.
13Configuração e operação
Variáveis de ambiente — o Billing não tem variável própria; usa as compartilhadas.
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATABASE_URL | PostgreSQL. O módulo usa o schema billing | Sim | — |
REDIS_URL | Redis. Publicação de eventos e contadores de rate limit | Sim | — |
JWT_SECRET | Segredo HS256 do IAM, para validar o token. Mínimo 44 caracteres | Sim | — |
PORT | Porta em standalone | Não | 3000 (a porta registrada do Billing é 3010) |
DEPLOYMENT_MODE | monolith ou standalone | Não | standalone em main.ts |
MODULE_BILLING_URL | URL do Billing, usada por outros módulos em standalone | Não | '' |
RATE_LIMIT_ENABLED | Liga/desliga o rate limit global | Não | ligado |
RATE_LIMIT_GLOBAL_MAX | Requisições por janela | Não | 10000 |
RATE_LIMIT_GLOBAL_WINDOW | Janela em ms | Não | 60000 |
Configuração por organização — em GET/PATCH /billing/api/v1/config, não em variável de ambiente.
| Campo | Descrição | Padrão | Efeito real hoje |
|---|---|---|---|
defaultCurrency | Moeda padrão (3 letras) | BRL | Não aplicado na criação de fatura, que grava BRL fixo (§15) |
paymentTermDays | Prazo de vencimento, 0 a 365 | 30 | Aplicado em POST /invoices sem dueDate |
billingCycleType | CALENDAR_ALIGNED ou ANNIVERSARY | CALENDAR_ALIGNED | Não aplicado ao cálculo de período (§15) |
invoicePrefix | Prefixo do número da fatura, até 20 caracteres | — | Não aplicado; o prefixo é sempre INV- (§15) |
invoiceFooter | Rodapé, até 1.000 caracteres | — | Apenas armazenado |
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema billing, 12 tabelas. Valores em Decimal |
| Redis | Stream iam-events para publicação; contadores de rate limit |
| IAM | Origem do token. Validação da assinatura é local, sem chamada de rede |
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
| Eventos por lote de uso | 1 a 1.000 | Schema Zod e verificação no serviço |
| Tamanho do corpo | 1 MB | applyCommonMiddleware. Lote grande pode estourar antes do limite de 1.000 eventos |
| Página | 20 por padrão, 100 no máximo | getPaginationParams |
| Rate limit global | 10.000 req / 60 s (padrão) | rateLimitMiddleware |
idempotencyKey | 1 a 255 caracteres | Schema Zod |
| Nome de conta, produto, plano, medidor | 200 caracteres | Schema Zod |
eventName do medidor | 100 caracteres, único por organização | Schema Zod + banco |
trialDays, paymentTermDays | 0 a 365 | Schema Zod |
| Precisão monetária | Decimal(15,2) para valores, Decimal(15,4) para preço unitário e quantidade | Prisma |
Catálogo de erros — envelope { "error": "<código>", "message": "...", "details": { ... } }.
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod, ou regra de estado violada | A mensagem nomeia a regra. Ver os casos abaixo |
400 | VALIDATION | "Only DRAFT invoices can be updated / can have line items added / can be finalized" | A fatura já foi finalizada. Anule e refaça |
400 | VALIDATION | "Cannot void paid or already voided invoices" | Fatura paga não se anula. Use estorno do pagamento |
400 | VALIDATION | "Cannot pay voided invoice" / "Invoice is already paid" | Confira o status antes de registrar pagamento |
400 | VALIDATION | "Cannot cancel/pause/resume subscription in X status" | Transição inválida. Ver o grafo na §8 |
400 | VALIDATION | "Billing account is not active" / "Subscription plan is not active" | Ative a conta ou o plano antes de assinar |
400 | VALIDATION | "Refund amount exceeds available amount" | Já houve estorno parcial. Consulte refundedAmount |
401 | UNAUTHORIZED | Token ausente, inválido ou expirado | Renove pelo refresh token do IAM |
403 | — | Permissão BILLING_* faltando no token | Confira o papel e o que a organização contratou |
403 | — | Token sem organizationId | Autentique informando a organização. Vale para as 46 rotas |
404 | NOT_FOUND | Recurso inexistente ou de outra organização | Confira o id. Recurso de outro tenant é indistinguível de inexistente, de propósito |
409 | CONFLICT | E-mail de conta, nome de produto/plano, eventName ou produto já no plano | Escolha outro valor |
409 | CONFLICT | "Active subscription with this plan already exists" | A conta já assina esse plano |
422 | VALIDATION | Franquia de uso estourada | details traz currentUsage, limit, attempted. Ofereça upgrade ou libere excedente |
429 | — | Rate limit global | Recuo exponencial. Em ingestão de uso, prefira o lote |
500 | INTERNAL | Falha de banco ou de publicação de evento | Verifique PostgreSQL e Redis |
Observabilidade.
GET /billing/healthresponde{ status, service, version, timestamp }e não toca banco nem Redis — serve de liveness, não de readiness.- Todo evento publicado passa pelo
EventPublisher, que injeta contexto de trace (traceId,spanId) nos metadados e conta a métrica de evento publicado. Uma fatura pode ser rastreada da ingestão do uso até a entrega do webhook. - Bloqueio de franquia gera log de aviso
[USAGE BLOCKED] account=... meter=... current=... limit=... attempted=.... É o sinal para acompanhar cliente prestes a estourar o plano — e a base natural de um alerta comercial. - Estouro de rate limit vai para
logSecurityEventcomoRATE_LIMIT_EXCEEDED. - Não há métrica de negócio exposta (faturamento emitido, faturas vencidas). Monte pelo banco ou pelos eventos.
14Segurança e compliance
Isolamento entre tenants. O organizationId é claim assinado no JWT e nunca é lido do corpo da requisição. As 46 rotas do módulo declaram requireOrganization — foi conferido rota a rota, sem exceção. No repositório, organizationId entra na cláusula WHERE de toda consulta por id: findById(id, organizationId) usa findFirst({ where: { id, organizationId } }), e não findUnique({ where: { id } }). Na prática, pedir o id de uma fatura de outra organização devolve 404, não 403 — indistinguível de "não existe", o que evita confirmar a existência de recursos alheios.
Dados pessoais. BillingAccount guarda nome, e-mail, endereço de cobrança e identificação fiscal (taxId, tipicamente CPF ou CNPJ). UsageEvent.properties é JSONB livre e pode receber dado pessoal se você colocar lá — o módulo não filtra o conteúdo. A recomendação é gravar identificador opaco, não CPF nem nome, porque esse é o dado de maior volume e menor controle de retenção do módulo.
Enquadramento LGPD. taxId e e-mail de cobrança são dados pessoais quando a conta é de pessoa física. A exclusão é lógica (deletedAt), e não há expurgo automatizado — atender pedido de eliminação exige processo explícito. Há um detalhe relevante: BillingAccount.personId referencia Person com onDelete: SetNull, e existe personOrphaned para marcar a conta que perdeu o vínculo. O campo existe e é exposto na API, mas nenhum código o preenche hoje (§15) — não construa relatório de conformidade em cima dele.
Retenção fiscal versus direito ao esquecimento. Fatura emitida é documento com obrigação de guarda, e por isso a exclusão do módulo é lógica em tudo que tem valor histórico. Isso é intencional e é o comportamento correto do ponto de vista fiscal; significa que "apagar o cliente" não apaga o que foi cobrado dele.
Integridade do documento de cobrança. A fatura finalizada não aceita alteração — PATCH e novos itens são recusados fora de DRAFT. finalizedBy guarda o id do usuário que finalizou, e createdBy o de quem criou. Anular exige BILLING_INVOICES_VOID, permissão separada de UPDATE exatamente para poder ser concedida a menos gente.
Segregação de funções pelas permissões. O vocabulário do IAM separa as ações de risco: BILLING_INVOICES_FINALIZE (emitir), BILLING_INVOICES_VOID (anular), BILLING_PAYMENTS_REFUND (estornar) e BILLING_SUBSCRIPTIONS_CANCEL (cancelar) são permissões próprias, não derivadas de UPDATE. Isso permite montar papéis em que o atendimento consulta, o financeiro emite e só a gerência anula e estorna.
Uma ressalva que o desenho de papéis precisa conhecer:
POST /paymentsexigeBILLING_PAYMENTS_READ, e nãoBILLING_PAYMENTS_RECORD. Quem tem leitura de pagamentos consegue registrar pagamento e, com isso, marcar fatura como paga. Ao montar um papel de consulta, considere isso até que a permissão correta seja aplicada (§15).
Dados sensíveis de pagamento. O módulo não guarda número de cartão, CVV nem dado de instrumento de pagamento. Payment guarda valor, método como texto livre ("pix", "cartao") e externalRef — a referência da transação no gateway. Isso mantém o Billing fora do escopo de PCI-DSS, e é uma razão de peso para não começar a gravar dado de cartão em metadata.
Superfície de rede. Em standalone, applyCommonMiddleware aplica limite de corpo de 1 MB, CORS fail-safe (sem origens configuradas em produção, bloqueia cross-origin), cabeçalhos de segurança e rate limit global. Sem ele, o módulo subiria sem nenhuma dessas proteções, porque os middlewares globais do monolito não alcançam o processo standalone.
Autenticação. Não há rota pública além de /billing/health. Todo o resto exige Bearer JWT válido emitido pelo IAM, com permissão específica e organização no token.
15Limitações conhecidas
O Billing tem uma API de faturamento completa e uma automação ausente. Esta seção é a leitura obrigatória antes de qualquer proposta comercial.
Automação — o bloco mais importante
| Limitação | Impacto | Situação |
|---|---|---|
| Não há agendador nem consumidor | src/billing/jobs/ e src/billing/consumers/ existem, mas todos os export estão comentados. Nada acontece sozinho no módulo. Fechamento de ciclo, agregação de uso, marcação de vencido e ativação de teste dependem de chamada externa | Roadmap. É o que separa o status beta de producao |
aggregateUsageForPeriod não tem endpoint nem job | O passo que transforma UsageEvent em UsageRecord — pré-requisito para generate com includeUsage: true — só é alcançável de dentro do processo. Pela API pública, o caminho é lançar o uso como item de fatura (§11) | Roadmap |
billSubscriptionsDue não tem endpoint nem job | O método que fatura todas as assinaturas vencidas de uma organização e avança o período existe e é testado, mas nada o chama. Faturar em massa exige um generate por assinatura | Roadmap |
activateFromTrial não tem endpoint nem job | Assinatura em TRIAL não sai do teste sozinha e não vira ACTIVE por nenhuma rota. Ela permanece em TRIAL indefinidamente | Roadmap. Evite trialDays até que exista |
OVERDUE nunca é gravado | O status existe no enum e a consulta findOverdue está escrita, mas sem chamador. Fatura vencida continua PENDING. Régua de inadimplência é sua | Roadmap |
EXPIRED nunca é gravado | Declarado como transição válida de TRIAL e ACTIVE, mas nenhum código o grava | Roadmap |
cancelAtPeriodEnd: true não conclui o cancelamento | Grava a flag, mantém ACTIVE e tira a assinatura da fila de faturamento. Ninguém a cancela no fim do período | Roadmap |
CANCELED de fatura nunca é gravado | O enum tem CANCELED e VOID; só VOID é usado. Trate CANCELED como inexistente | Por design na prática |
Precificação e cálculo
| Limitação | Impacto | Situação |
|---|---|---|
| Precificação por faixas não é aplicada | BillingProduct.tiers é gravado, retornado pela API e ignorado no cálculo. pricingModel (TIERED, VOLUME, PACKAGE, FLAT_FEE) é rótulo de catálogo: a fatura sempre calcula quantidade × preço unitário. Não venda preço por faixa | Roadmap. É a lacuna funcional mais visível contra Stripe Billing e Chargebee |
| A agregação é sempre soma | UsageMeter.aggregationType aceita SUM, MAX, LAST, AVERAGE e COUNT_DISTINCT, mas aggregateByMeter usa _sum em todos os casos. propertyKey é gravado e não usado. Medição de pico ou de valor final entrega o total somado — silenciosamente errado | Roadmap. Até lá, use apenas SUM |
| Rateio calculado, nunca aplicado | ProrationService calcula rateio, crédito de downgrade e cobrança de upgrade, com testes. Nenhum fluxo o chama e nenhuma linha PRORATION é criada automaticamente | Roadmap |
| Sem tributos e sem desconto | taxAmount e discountAmount são sempre zero; total é igual a subtotal. Nenhuma rota os altera. Imposto e desconto entram como linha explícita | Roadmap |
| Sem emissão fiscal | Não emite NF-e nem NFS-e e não integra com emissor. É diferença relevante contra Vindi, Iugu e Asaas no Brasil | Fora do escopo atual |
| Sem troca de plano | PATCH /subscriptions/:id altera só quantity. Migrar de plano é cancelar e recriar (§11) | Roadmap |
billingCycleType não afeta o cálculo | CALENDAR_ALIGNED e ANNIVERSARY são gravados no plano e na configuração, mas calculateNextPeriodEnd sempre soma o intervalo à data de início — ou seja, sempre aniversário | Roadmap |
Fatura sempre em BRL | InvoiceService.createInvoice grava currency: 'BRL' fixo, ignorando defaultCurrency da configuração e a moeda da conta. Fatura vinda de generate usa a moeda da assinatura, então os dois caminhos podem divergir | Roadmap |
invoicePrefix não é aplicado | O número é sempre INV-{ano}-{sequencial}. O prefixo configurado é só armazenado | Roadmap |
Número de fatura por COUNT(*) + 1 | Duas gerações concorrentes na mesma organização podem colidir na unicidade. Serialize o fechamento por organização | Roadmap |
Operação e dados
| Limitação | Impacto | Situação |
|---|---|---|
summary devolve billedQuantity e unbilledQuantity fixos | Sempre 0 e o total, respectivamente, independentemente do que já foi faturado. Use quantity e includedUnits | Roadmap |
| Pagamento sem idempotência | Reprocessar webhook cria segundo Payment e credita de novo. Deduplique do seu lado (§11) | Roadmap |
| Estorno não reabre a fatura | Estorno total marca o Payment como REFUNDED, mas a fatura permanece PAID | Roadmap |
| Saldo da conta não é aplicado em fatura | balance é incrementado por pagamento sem invoiceId, e não existe rotina que use esse crédito numa fatura futura | Roadmap |
personOrphaned nunca é preenchido | markPersonOrphaned existe no repositório e não tem chamador. O campo é sempre false | Roadmap |
| Exclusão de plano não verifica assinaturas ativas | DELETE /plans/:id faz exclusão lógica sem checar se há assinatura viva usando o plano. Mesma coisa para produto e medidor | Roadmap |
POST /payments exige a permissão errada | Usa BILLING_PAYMENTS_READ enquanto BILLING_PAYMENTS_RECORD existe no vocabulário do IAM e não é usada. Quem lê pagamentos consegue registrar pagamento | Pendência conhecida — considere no desenho de papéis (§14) |
/health não verifica dependências | Responde estático. Banco fora do ar não derruba a sonda | Por design; use como liveness |
| Sem dunning e sem portal do assinante | Sem retry de cobrança recusada, régua de e-mail ou tela de autoatendimento | Fora do escopo atual |
| Eventos declarados e não publicados | billing.invoice.overdue, billing.subscription.expired, billing.payment.created e billing.payment.failed estão no enum e nenhum código os emite | Consequência da ausência de agendador |
| Lote grande pode estourar o limite de corpo | O schema aceita 1.000 eventos, mas bodyLimit corta em 1 MB. Com properties volumoso, o teto real é bem menor | Por design; divida o lote |
16Perguntas frequentes
Qual a diferença entre o Billing e o Payments? Preciso dos dois?
O Billing decide quanto cobrar e emite o documento; o Payments recebe o dinheiro. Se você cobra valor fixo e já tem gateway, talvez precise só do gateway. Se o valor depende de consumo medido com franquia, alguém precisa calcular — e o gateway não sabe fazer isso. Nesse caso, os dois: o Billing calcula e emite, o Payments captura, e você registra o pagamento no Billing com externalRef apontando para a transação.
O Billing substitui a Vindi, a Iugu ou o Asaas?
Não diretamente. Essas empresas são gateways com recorrência acoplada: elas capturam o dinheiro e emitem nota fiscal, coisas que o Billing não faz. O que o Billing substitui é a camada de cálculo — medição de uso, franquia, excedente, montagem da fatura —, que é justamente onde esses produtos são rasos porque foram desenhados para cobrança de valor fixo. A combinação comum é Billing calculando e um gateway brasileiro recebendo.
Por que a minha fatura veio sem a linha de uso?
Porque o consumo não foi agregado antes. POST /invoices/generate com includeUsage: true lê UsageRecord, não UsageEvent, e a agregação não roda sozinha hoje. Sem o passo intermediário, a fatura sai só com a assinatura — e sem erro nenhum. A receita "Fechar um ciclo de faturamento" na §11 mostra os dois caminhos.
O que acontece se a minha integração reenviar o mesmo evento de uso?
Nada é cobrado duas vezes. idempotencyKey é obrigatório e único por organização; o reenvio devolve 201 com meta.isDuplicate: true e o evento original. Use um identificador natural da operação — o id da consulta, do documento, da mensagem —, nunca um UUID gerado na hora da chamada, que mudaria a cada retry.
Dá para bloquear o cliente quando ele estoura a franquia?
Dá, e é o comportamento padrão quando o item de plano tem includedUnits sem priceOverride: o evento que ultrapassaria o limite recebe 422 com currentUsage, limit e attempted, e a sua aplicação usa esses campos para oferecer upgrade. Adicionando priceOverride ao mesmo item, o bloqueio some e o excedente passa a ser cobrado. Só vale se houver assinatura ACTIVE ou TRIAL na conta.
Posso cobrar por faixas — mais barato acima de dez mil unidades?
Hoje não. pricingModel aceita TIERED e VOLUME, e tiers é gravado e devolvido pela API, mas o cálculo da fatura sempre usa quantidade × preço unitário. Não venda preço por faixa (§15). O contorno é criar um produto por faixa e lançar as linhas de fatura conforme a regra, o que funciona mas é trabalho manual.
Como fica a assinatura em período de teste?
Ela nasce em TRIAL quando o plano tem trialDays e você não passou skipTrial: true — e fica em TRIAL. A transição para ACTIVE existe no serviço, mas nenhuma rota a expõe e nenhum job a executa. Enquanto isso não muda, a recomendação prática é criar a assinatura com skipTrial: true e controlar o período de teste na sua aplicação.
Consigo cancelar ao fim do período, como no Stripe?
Consegue marcar, mas não consegue concluir. cancelAtPeriodEnd: true grava a flag e a assinatura continua ACTIVE, saindo da fila de faturamento. Ninguém a cancela quando o período acaba, porque não há agendador. Se o seu produto depende disso, controle a data do seu lado e chame o cancelamento imediato quando chegar a hora.
A fatura finalizada pode ser corrigida?
Não. Depois de finalize, ela não aceita PATCH nem novos itens. O caminho é POST /invoices/:id/void e emitir outra — que é o comportamento correto para um documento de cobrança e o que permite explicar a um auditor o que foi cobrado e quando. Confira o total antes de finalizar: finalize congela o que os itens somaram e não recalcula nada.
O Billing calcula imposto?
Não. taxAmount e discountAmount são sempre zero e total é igual a subtotal. Se você precisa de ISS, retenção ou desconto na fatura, lance como linha explícita enquanto a fatura está em DRAFT, ou calcule no sistema fiscal. Emissão de NF-e e NFS-e também está fora do escopo.
Quanto custa? Vocês cobram percentual do meu faturamento?
A precificação está em definição, e não há tabela para citar. O compromisso de desenho, esse sim, está tomado: a Catalisa não cobra percentual sobre o faturamento processado pelo Billing — que é como Stripe Billing (0,7%), Chargebee (0,80%), Metronome (0,8% mais eventos) e Recurly (0,9% mais mensalidade) precificam, conforme as páginas públicas consultadas em 2026-08-16. Os direcionadores do nosso custo são operacionais: assinaturas ativas, eventos ingeridos, faturas emitidas.
Honestidade obriga a completar: não somos os únicos a fazer isso. Lago e Kill Bill também não cobram percentual da sua receita, e o Kill Bill anuncia exatamente esse posicionamento na página inicial. A diferença é que os dois são projetos de código aberto que você hospeda e opera; o Billing vem operado e dentro de uma plataforma onde identidade, clientes e catálogo já estão resolvidos.
Vale a pena construir isso internamente se as ferramentas de mercado existem?
A pergunta mudou de forma em 2026. As três ferramentas independentes de medição de uso — Metronome, Orb e m3ter — foram compradas em seis meses por Stripe, Adyen e Salesforce (§4, Caso 5). Quem hoje quer cobrança por consumo compra de um processador de pagamento, que cobra percentual da receita, ou opera um projeto de código aberto. O Billing existe para dar uma terceira opção dentro da plataforma que o cliente já usa. Dito isso, a §15 é longa por um motivo: se você precisa hoje de faixas de preço, tributo na fatura e fechamento automático, a ferramenta de mercado entrega e o Billing ainda não.
Por que o status é beta se ele já está em staging respondendo?
Porque a API está pronta e a automação não. As 46 rotas funcionam, estão cobertas por testes de unidade e integração e o isolamento por tenant foi conferido rota a rota. O que falta é o agendador que fecharia o ciclo, agregaria o uso, marcaria o vencido e ativaria o teste. Chamar isso de produção seria pedir para o cliente descobrir na implantação — a §15 lista cada item.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md