26 de jul. de 2025·8 min de leitura

Modelo de dados para app de faturamento: clientes, faturas e totais que funcionam

Modelo de dados para app de faturamento: modele clientes, faturas, itens e status de pagamento para que os totais permaneçam corretos, auditáveis e fáceis de manter.

Modelo de dados para app de faturamento: clientes, faturas e totais que funcionam

Por que os totais de fatura quebram em apps reais

Os totais das faturas geralmente parecem corretos no primeiro dia. Eles quebram no décimo dia, quando pessoas reais começam a editar faturas, aplicar descontos, registrar pagamentos parciais e pedir reembolsos.

A maioria dos problemas vem do fato de os totais não terem uma fonte de verdade clara. Uma tela gerada por IA pode armazenar total = 120.00 na fatura enquanto os itens somam 119.99 depois do arredondamento. Mais tarde, alguém edita uma quantidade, mas o total armazenado não é atualizado. Agora o PDF, o banco de dados e o registro de pagamento discordam.

“Totais corretos” é mais do que matemática básica. Significa que seu app pode reproduzir os mesmos números sempre que aplicar as mesmas regras, mesmo meses depois. Também significa que os totais batem com o que o usuário viu quando enviou a fatura, incluindo impostos, descontos e quaisquer ajustes manuais.

Outra causa é a perda de histórico. Se você sobrescrever uma fatura no lugar, não consegue responder perguntas básicas depois: o que mudou, quem mudou, se a mudança aconteceu antes ou depois de um pagamento ou reembolso, e qual versão foi realmente enviada.

Um pequeno cenário mostra como isso quebra rápido: você envia uma fatura por 10 horas de trabalho. O cliente paga metade. Depois você corrige as horas para 9,5. Se seu modelo de dados não separar versões de fatura dos pagamentos, seu app pode mostrar o cliente como com pagamento em excesso, em falta, ou totalmente pago dependendo da tela que abrirem.

Salvar um único campo “total” parece rápido no começo, mas cria dor assim que você adiciona edições, pagamentos parciais, créditos e reembolsos.

Entidades centrais que você precisa (e o que cada uma significa)

Um modelo de dados confiável para faturamento separa quem você cobra, o que você cobra e o que aconteceu depois que a fatura foi enviada. Se isso se misturar, totais e status desviam.

Contatos: cliente nem sempre é quem paga

Um Customer é a conta com a qual você faz negócios (uma empresa ou pessoa). Mantenha os contatos separados para que você possa mudar para onde as faturas vão sem alterar o registro do cliente.

Uma configuração comum é um registro de cliente de longa duração (nome, moeda padrão, ID fiscal, notas) mais contatos separados de cobrança e de entrega. Assim, “Enviar faturas para a contabilidade, mas enviar para nosso armazém” não vira endereços sobrescritos.

Faturas e itens: o que você enviou vs o que está editando

Uma Invoice é o documento que você pretende enviar, com um cliente, data de emissão, data de vencimento, moeda e número da fatura. Trate rascunho vs enviado como fases da mesma fatura. Se você permitir alterações depois de enviar, adicione uma Invoice Version (ou revisão) para ter um histórico limpo.

Para cobranças, trate Line Items como a fonte de verdade para os valores. Um item pode referenciar um Product/Service do catálogo ou ser um item customizado em texto livre. Não force tudo numa tabela de produtos, e não armazene totais extras nos itens a menos que haja uma razão clara.

Eventos de dinheiro: pagamentos não são reembolsos

Um Payment registra dinheiro recebido e como ele foi aplicado a uma ou mais faturas. Um Refund é dinheiro devolvido, geralmente ligado a um pagamento. (Um Payout é dinheiro que você envia, e muitas vezes não está relacionado a faturas.)

Mantenha os status de ciclo de vida mínimos e estáveis (draft, sent, void). Trate “pago/em atraso/parcialmente pago” como derivado de pagamentos e datas, não como um campo editado manualmente.

Passo a passo: desenhe o esquema antes de construir telas

Se você construir a UI primeiro, suas tabelas tendem a copiar o que a primeira tela precisou. É assim que um modelo de dados de faturamento acaba com lacunas: status pouco claros, histórico faltando ou faturas que podem ser editadas até virar nonsense.

Comece pelo ciclo de vida da fatura porque ele guia suas regras e seus dados. Um ciclo simples funciona para muitos apps: draft (não final), sent (cliente viu), paid (liquidado), void (cancelado e não cobrável). Decida quais mudanças são permitidas em cada etapa antes de nomear qualquer coluna.

Uma ordem prática:

  • Defina estados e transições (draft -> sent -> paid, e void a partir de draft ou sent).
  • Use IDs internos imutáveis (como UUIDs) e mantenha números de fatura separados para humanos.
  • Decida o que fica bloqueado após o envio (itens, alíquotas, endereço de cobrança).
  • Liste os campos mínimos exigidos por tabela.
  • Tome a decisão de moeda cedo (moeda única vs multi-moeda real).

Para IDs, use uma chave interna imutável para cada registro. Depois adicione um invoice_number que seja único, legível e nunca mude depois de enviado. Usuários vão citar o número da fatura, não sua chave primária.

Seja explícito sobre editabilidade. Você pode permitir corrigir um erro de digitação no nome do cliente após o envio, mas não mudar quantidades ou preços. Se quiser permitir mudanças monetárias, modele-as como uma nova versão, uma nota de crédito ou um ajuste, não uma reescrita silenciosa.

Campos mínimos viáveis podem ficar pequenos:

  • Customer: id, name, email, billing_address
  • Invoice: id, customer_id, invoice_number, status, issued_at, currency
  • Line item: id, invoice_id, description, quantity, unit_price
  • Payment (se você rastrear): id, invoice_id, amount, received_at

Apps com moeda única podem armazenar valores monetários como unidades menores inteiras (como centavos). Apps multi-moeda precisam da moeda em cada campo de dinheiro e regras claras de taxa de câmbio.

Como manter os totais corretos e consistentes

A maioria dos bugs de “total errado” vem de propriedade pouco clara. Se sua UI edita um item, o total da fatura deve mudar em um lugar previsível, sempre. Decida cedo o que é calculado, o que é armazenado e quando os valores se tornam finais.

Comece pelo item. Armazene as entradas brutas (unit_price e quantity) e armazene o line_total calculado pela sua app no momento. Use unidades menores inteiras (como centavos) para todo campo monetário, incluindo descontos e valores de imposto. Isso evita surpresas de ponto flutuante como 19.999999 aparecendo como 20.00.

Uma regra útil é: calcule a partir dos campos brutos, armazene o que você exibe e seja consistente sobre quando você recalcula.

Escolha uma fonte de verdade clara

Você tem três opções práticas. Misturá-las sem regra cria deriva:

  • Calcular os totais dinamicamente a partir dos itens sempre que carregar uma fatura.
  • Armazenar os totais da fatura e atualizá-los a cada edição.
  • Fazer ambos: calcular, armazenar e então validar.

Se fizer ambos, trate os valores calculados como o validador. Quando discordarem, marque um alerta em vez de escolher silenciosamente um dos dois.

Lide com edições sem reescrever o histórico

Decida quando uma fatura é editável. Uma abordagem comum: rascunhos são editáveis, faturas enviadas ficam bloqueadas, e mudanças acontecem via nova versão, nota de crédito ou linha de ajuste.

Exemplo: você enviou uma fatura com alíquota de 7,5%. Um mês depois a alíquota muda. Se você recalcular usando a alíquota de hoje, a fatura antiga muda e o cliente vê um valor diferente do enviado.

Para evitar isso, faça um snapshot das entradas que importam quando a fatura se torna final: alíquota usada, regras de desconto, modo de arredondamento e moeda. A recálculo então usa o snapshot, não as configurações atuais da conta.

Modele o status de pagamento sem se limitar

Make It Production Ready
Turn a prototype into production-ready code with refactoring and deployment prep.

Muitos bugs de faturamento começam quando um campo tenta fazer demais. “Status” costuma ser usado para tudo: draft vs sent, atrasado vs pago, reembolsado vs disputado. Isso funciona para um demo, depois quebra assim que você suporta pagamentos parciais.

Mantenha duas ideias separadas:

  • Invoice lifecycle status: o que o documento é (draft, sent, void, written off)
  • Payment status: o que aconteceu com o dinheiro (unpaid, partial, paid, overpaid, refunded, disputed)

Eles podem discordar, e isso é normal. Uma fatura pode estar “sent” e “unpaid”. Pode estar “void” e ainda ter um pagamento que precisa ser reembolsado.

Modele movimentações de dinheiro, não apenas um rótulo

Em vez de armazenar “paid: true”, registre cada movimento de dinheiro em tabelas como payments e refunds (ou uma única tabela payment_events). Também rastreie tentativas de pagamento separadamente de pagamentos bem-sucedidos. Cartões falham, são tentados novamente e depois têm sucesso. Se você registrar apenas sucessos, perde a história e o suporte fica mais difícil.

Uma regra simples: uma linha por evento real, nunca reescrever o histórico. Se um pagamento for revertido, adicione um evento de reembolso ou chargeback ligado ao pagamento original.

Defina “pago” como um cálculo

Decida o que “pago” significa e torne isso consistente em toda parte (UI, emails, relatórios). Uma definição comum:

  • net_paid = sum(successful payments) - sum(refunds and chargebacks)
  • amount_due = invoice_total - net_paid
  • O status de pagamento é baseado em amount_due (0 significa pago, negativo significa pagamento em excesso)

Exemplo: uma fatura total é $100. O cliente paga $60, depois paga $50. Net paid é $110, então a fatura está “overpaid” em $10. Se você depois reembolsar $10, adicione um evento de reembolso e ela volta a “paid” sem editar os pagamentos antigos.

Impostos, descontos e arredondamento que não surpreendam usuários

A maioria dos bugs “meus totais não batem” acontece porque impostos, descontos e arredondamento são adicionados tardiamente. Se seu modelo de dados deixar essas regras explícitas, sua UI pode permanecer simples e os usuários podem verificar cada número.

Onde armazenar o imposto

O imposto pode ficar no nível do item, no nível da fatura, ou em ambos. O que importa é ter uma fonte de cálculo clara.

Imposto a nível de linha é mais fácil de explicar: cada item tem um flag de tributável, um snapshot da alíquota e um valor de imposto calculado. Isso funciona bem quando itens diferentes têm taxas diferentes, ou alguns são isentos.

Imposto a nível de fatura funciona quando tudo compartilha uma alíquota, mas fica confuso com taxas mistas. Se fizer imposto a nível de fatura, armazene a alíquota usada e a base tributável aplicada para que edições futuras não mudem o histórico.

Descontos e regras de arredondamento

Descontos devem ser modelados como percentual ou valor fixo. Decida a que eles se aplicam (por linha, subtotal ou total após imposto) e codifique isso.

Um padrão prático é tratar ajustes como itens de linha de primeira classe: frete como linha positiva, descontos e créditos como linhas negativas, e cupons como entradas explícitas.

O arredondamento também precisa de uma regra clara. Se você arredonda por linha, o total da fatura pode diferir em um centavo de arredondar apenas no subtotal. Escolha uma política e armazene os resultados arredondados que você exibe.

Para fazer os totais parecerem confiáveis, mostre as contas da mesma forma que você computa: subtotal, cada ajuste, detalhamento de impostos e total final.

Edições, versões e histórico de auditoria

Faturas parecem simples até alguém perguntar “O que realmente enviamos?” Se seu app permite editar livremente faturas antigas, totais e imposto podem mudar depois do fato, e a confiança desaparece.

Quando uma fatura é enviada

Trate “sent” como uma linha na areia. Mantenha um snapshot imutável do que o cliente viu, mesmo se seu produto mudar preços, alíquotas ou regras de arredondamento depois.

Você pode fazer isso com linhas versionadas ou um payload congelado. No mínimo, faça snapshot das informações de exibição ao cliente no momento do envio, dos itens como enviados (incluindo alíquota), dos totais enviados, dos termos e data de vencimento, e metadados de envio como sent_at e email do destinatário.

Depois decida o que ainda pode mudar sem afetar dinheiro. Notas e tags internas normalmente são seguras para editar. Campos monetários como unit_price, quantity, descontos, imposto e moeda devem ser bloqueados assim que enviados (ou só mudar via nova versão).

Lidando com correções

Negócios reais cometem erros. Não “edite o histórico” para escondê-los. Use ações explícitas de correção: uma nota de crédito pela diferença, anulação com motivo, ou cancelar e reemitir com um novo número de fatura (mantendo o antigo para auditoria).

Evite deleções permanentes. Faturas deletadas quebram numeração, relatórios e conciliação de pagamentos. Anular mantém o registro, define o valor devido para zero e preserva o rastro.

Para equipes não técnicas, mantenha um pequeno log de mudanças: quem mudou o quê, quando e por quê.

Exemplo: um cliente, duas faturas, pagamento parcial, reembolso

Eliminate Rounding Surprises
Fix hidden float math and 1-cent mismatches across UI, PDFs, and exports.

Aqui está um cenário concreto que você pode usar para checar o modelo de dados do seu app de faturamento.

Customer: Green Field Studio (um contato de cobrança, uma moeda).

Fatura 1: taxa única de setup

Invoice INV-1001 tem um item: taxa de setup de $500.00. Suponha imposto de 8%.

Subtotal: $500.00 | Tax: $40.00 | Total: $540.00

Um único pagamento de $540.00 é registrado. A fatura muda de Sent para Paid, e o saldo aberto vira $0.00.

Fatura 2: plano mensal com itens, desconto, imposto

Invoice INV-1002 tem três itens: plano mensal ($200.00), assentos extras (3 x $20 = $60.00) e add-on de suporte prioritário ($50.00). Subtotal é $310.00.

Aplicar desconto de 10% no subtotal: -$31.00, então o subtotal com desconto é $279.00. Imposto de 8% é $22.32.

Total devido: $279.00 + $22.32 = $301.32

Agora, os eventos que você deve ver após cada passo:

StepWhat happensPayments totalRefunds totalNet paidOpen balanceStatus
1Invoice sent$0.00$0.00$0.00$301.32Sent
2Partial payment of $150.00$150.00$0.00$150.00$151.32Partially paid
3Final payment of $151.32$301.32$0.00$301.32$0.00Paid
4Refund $50.00 tied to the support line item$301.32$50.00$251.32$0.00Paid (partially refunded)

Note o que muda (e o que não muda) no Passo 4: o total da fatura permanece $301.32, e o saldo aberto permanece $0.00 porque você já coletou o valor total. O reembolso é um movimento de dinheiro separado, então o cliente agora tem $50.00 a devolver (frequentemente registrado como crédito do cliente).

Erros comuns que protótipos gerados por IA cometem

Ferramentas de IA podem levar você a um demo funcional rápido, mas faturamento é cheio de regras pequenas que falham silenciosamente. Se seu modelo de dados estiver mesmo que levemente errado, você acaba com totais que mudam, pagamentos que não batem e relatórios em que não se confia.

Matemática financeira que deriva ao longo do tempo

Um erro comum em protótipos é usar floats para dinheiro (como 19.99) e somá-los em lugares diferentes. Isso cria pequenas diferenças de arredondamento que aparecem como um centavo de divergência entre o total da fatura, o total de pagamentos e o PDF.

Use unidades menores inteiras (centavos) para valores armazenados, e arredonde uma vez na borda (exibição, PDF, export). Se precisar armazenar decimal, use um tipo decimal de precisão fixa e seja consistente.

Totais que param de corresponder após edições

Muitos protótipos armazenam subtotal, tax_total e total na fatura mas nunca os recalculam quando itens mudam. Ou recalculam só na criação, não na edição, importação ou atualizações via API.

Um padrão mais seguro: calcule totais a partir dos itens e ajustes, persista os totais calculados e registre um momento claro de “último calculado”. Se algo mudar, acione a recalculação em um lugar, não em cada tela.

Os erros por trás da maioria dos “por que este total está errado?” são previsíveis: dividir regras de cálculo entre UI e backend, permitir edições monetárias depois de pagamentos sem registro claro de mudança, atualizar totais sem atualizar saldo devido, misturar imposto por linha e por fatura de forma inconsistente, e arredondar linhas diferente do total final.

Campos de status que te enredam

Protótipos costumam juntar “sent” e “paid” em um único status, o que quebra assim que você tem pagamentos parciais, falhas de pagamento ou reembolsos. Mantenha o estado de entrega (draft/sent/void) separado do estado de pagamento (unpaid/partial/paid/overpaid).

Também nunca delete pagamentos para “desfazer” eles. Registre uma reversão ou reembolso para que o histórico permaneça verdadeiro.

Checklist rápido antes de enviar

Stop Total Drift Fast
Send your repo and we’ll repair calculation bugs, rounding issues, and broken invoice states.

Antes de lançar, faça uma passada focada na correção do dinheiro, não no polimento da UI.

  • Armazene dinheiro como unidades menores inteiras (por exemplo, centavos), não floats. Mantenha a moeda na fatura e em cada registro de pagamento para nunca misturar USD e EUR.
  • Torne faturas imutáveis após o envio. Se precisar permitir edições, crie uma nova versão (ou nota de crédito) e mantenha campos snapshot (nome do cliente, endereço, IDs fiscais, descrições de itens, preços) exatamente como eram no momento do envio.
  • Modele pagamentos e reembolsos como registros separados. Um reembolso não é um pagamento negativo escondido na mesma tabela, a não ser que você seja explícito sobre relatórios e comportamento de “pago”.
  • Verifique totais com uma única fonte de verdade. Totais armazenados devem corresponder: sum(line totals) + impostos/taxas - descontos, com o arredondamento aplicado em um único lugar.
  • Defina “pago” por escrito e codifique isso. Por exemplo: uma fatura é paga quando (pagamentos - reembolsos) é maior ou igual ao valor devido, e ela não está anulada. Decida como tratar pagamento em excesso.

Depois disto, execute um relatório de reconciliação simples: para um intervalo de datas, liste o valor devido de cada fatura, pagamentos totais, reembolsos totais e saldo restante. Se você não conseguir gerar isso limpo do seu modelo, falta algo importante.

Próximos passos: validar, refatorar e deixar pronta para produção

Uma vez que suas tabelas e status pareçam corretos no papel, prove seus totais com casos de teste que reflitam comportamento real:

  • Editar um item após o envio da fatura (mudança de quantidade, preço, remover item)
  • Pagamento parcial, depois outro pagamento parcial, depois um reembolso
  • Aplicar um desconto, então mudar a alíquota, então remover o desconto
  • Anular uma fatura após uma tentativa de pagamento
  • Mudar os dados do cliente e confirmar que faturas históricas não reescrevem o passado

Adicione uma checagem de reconciliação que você possa rodar a qualquer momento: compare o total da fatura (o que você faturou) vs pagamentos líquidos (soma de pagamentos menos reembolsos). Se isso não bater com o saldo esperado, você sabe onde procurar.

Se você herdou um app de faturamento gerado por IA que “funciona na maior parte” mas desmorona em torno de totais, mudanças de estado ou reembolsos, equipes frequentemente recorrem ao FixMyMess (fixmymess.ai) para um diagnóstico rápido do código e reparo. Uma auditoria gratuita pode ser suficiente para apontar onde os totais são calculados, onde ocorrem mudanças de estado e quais campos estão derivando, para que você possa torná-lo seguro antes da produção.

Perguntas Frequentes

What should be the “source of truth” for an invoice total?

Use line items (plus explicit adjustments like discounts, shipping, credits) as the source of truth, and compute totals from them in one place. If you also store invoice totals for speed, treat stored totals as a cached snapshot and validate them against a recomputation so drift gets flagged early.

How do I stop 1-cent rounding errors from showing up?

Store money as integer minor units (like cents) and round with one consistent rule. Floating point math will eventually create 1-cent mismatches between the UI, PDFs, exports, and payment records.

Should users be able to edit an invoice after it’s sent?

Make invoices editable only in draft by default, and lock monetary fields once sent. If a correction changes money, create a new version, credit note, or adjustment entry instead of silently rewriting the old invoice.

Why model payments and refunds as separate records?

Keep them separate because they represent different real-world events. A payment records money received; a refund records money returned, usually tied to a specific payment, and both should remain in history for support and reconciliation.

What invoice statuses should I store vs calculate?

Use a small lifecycle status for the document itself, such as draft, sent, and void. Derive payment-related labels like “unpaid”, “partial”, “paid”, or “overpaid” from the invoice total, dates, and the net of payments minus refunds.

How do I keep historical totals stable when tax rates or pricing rules change?

Snapshot the inputs that affect totals at send time, like tax rate, discount rules, rounding mode, and the customer display details. Recalculations should use the snapshot so old invoices don’t change when account settings change later.

Should tax be calculated per line item or at the invoice level?

Pick one tax approach and stay consistent. Line-level tax is usually easiest to explain and supports mixed rates, but invoice-level tax can work if everything shares one rate and you store the rate and taxable base used.

How do I define “paid” when there are partial payments and refunds?

Define it as a calculation: amount_due = invoice_total - (sum(payments) - sum(refunds/chargebacks)). This handles partial payments, overpayments, and refunds without needing a brittle “paid: true” field.

Should I delete invoices or void them?

Avoid hard deletes because they break numbering, audit history, and reconciliation. Prefer voiding with a reason so the record remains, the amount due becomes non-collectible, and you can still explain what happened later.

My AI-generated invoicing app “mostly works” but breaks around totals—what should I do?

If you have an AI-built invoicing prototype where totals drift, statuses are inconsistent, or refunds break balances, FixMyMess can diagnose and repair the codebase quickly. We offer a free audit to pinpoint where calculations and state changes diverge, then make it production-safe with human-verified fixes.