Estágio 02 · 02-18
LockedPagamento é um dos domains mais densos de engenharia em qualquer produto. Money is hard: idempotência, double-charge, partial failures, refunds, chargebacks, taxas, multiple currencies, FX, tax (IVA/ICMS/sales tax), invoicing, dunning, subscriptions, prorations, webhooks duplicados, reconciliation com PSP, ledgers, accounting compliance (ASC 606, PCI-DSS escopo). Erros aqui custam dinheiro real e podem virar fraude/processo.
Quase todo dev que tenta integrar Stripe/Adyen/Mercado Pago "do jeito tutorial" produz código que funciona no happy path e quebra na primeira retry, webhook duplicado, ou reembolso parcial. Senior conhece o modelo correto: payment intent, idempotency key, ledger imutável, webhook signature verification, reconciliation diária.
Este módulo é payments por dentro: anatomia de cobrança, PSP arquitetura, idempotência forte, double-entry ledger, webhooks e replay safety, subscriptions com prorations, tax calculation, refund/chargeback flows, multi-tenant marketplace, e PCI-DSS escopo. Logística monetiza entregas e split entre lojistas/entregadores, esse módulo materializa isso.
Fluxo simplificado:
pm_xxx). Você guarda token, não cartão.PSPs (Stripe, Adyen, Pagar.me, Mercado Pago) abstraem isso, mas você ainda decide auth+capture-now vs auth-then-capture-later (pré-autorização hotelaria), 3DS challenge, etc.
API moderna: você cria intent (objeto stateful no PSP) representando intenção de cobrar valor X. Atualiza status (requires_payment_method → requires_confirmation → succeeded/failed). Idempotente.
Anti-pattern antigo: chamar charge direto e tratar resposta. Falha catastrófica em retries.
Stripe PaymentIntent, Adyen Sessions, MP Preference. Use sempre.
Network falha. Cliente retry. Sem idempotency, você cobra 2x.
PSPs aceitam Idempotency-Key header (UUID v4). PSP detecta repetição na chave e retorna mesmo resultado.
Você também precisa idempotency interna: endpoint POST /orders/:id/charge deve aceitar key do cliente, persistir resultado. Retry com mesma key retorna mesmo response sem efeito colateral. Implementação: tabela idempotency_keys (key pk, request_hash, response_body, created_at).
Janela de validade: 24h-7d típico. TTL via created_at.
PSP envia eventos (payment_intent.succeeded, charge.refunded, customer.subscription.updated) pro seu endpoint via HTTP POST. Síncronos não substituem webhooks: webhook é a fonte de verdade.
Regras:
Pedido tem estados (pending, requires_payment, paid, shipped, delivered, canceled, refunded, partially_refunded). Pagamento idem.
Modele explicitamente como finite state machine. Transições válidas únicas. Inválidas viram erro (ex: capturar pedido já capturado retorna idempotente, não duplica).
Conceito de accounting: cada transação afeta ≥ 2 contas, débitos = créditos. Imutável. Audit trail completo.
event: charge $100
Cash: +$100 (debit)
Sales: +$100 (credit)
event: refund $30
Sales return: +$30 (debit)
Cash: -$30 (credit)
Implementação: tabela ledger_entries (id, txn_id, account_id, amount, direction, currency, posted_at). Sums por account = saldo. Imutável (sem update/delete; correções via reversal entry).
Em produto, ledger é a fonte de verdade financeira; orders.status='paid' é cache/read model. Reconciliation cruza ledger com extrato PSP, diferenças investigadas.
Refund = reverter cobrança. Total ou parcial. PSP API: refunds.create(charge_id, amount?).
Cuidados:
refunded; partial → partially_refunded com refunded_amount.Customer reclama; issuer reverte fundos preventivamente. Você precisa responder com evidência (Stripe Dashboard), fotos de delivery, logs, tracking. Win/lose.
Impacto operacional: reservar contingência, reduzir taxa de chargeback (alvo < 1%), implementar 3DS pra shift de liability.
3D Secure: autenticação do cardholder via app banco. Strong Customer Authentication (PSD2 EU): obrigatório em txns elegíveis.
Liability shift: txn 3DS bem-sucedida transfere risco ao issuer (em chargeback de "didn't authorize"). Vale custo de friction em alto-valor.
Fluxo: PSP retorna requires_action → cliente faz challenge → confirm. Anti-pattern: ignorar requires_action e considerar success.
Sales tax/IVA/ICMS é caos:
Não calcule na mão. Use TaxJar, Avalara, Stripe Tax. Cache rates com TTL. Docs fiscais (NF-e, fatura europeia) geradas via emissor.
Cobre na moeda do customer; settled na sua. PSP faz conversão (com markup). Alternativa: você processa na sua moeda; customer paga FX no banco.
Money em código: nunca float. Use bigint em centavos (cents/cents-of-cents). Exemplo: R$ 19,99 = 1999. Lib: dinero.js, money.js.
Currency code ISO 4217 (BRL, USD, EUR). Persistência: amount_cents bigint, currency char(3).
Cobranças recorrentes. Conceitos:
active, past_due, canceled).Webhooks essenciais: invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted.
Logística é multi-tenant: lojista paga, plataforma fica com fee, entregador recebe parcela. Split:
transfer_data ou application_fee.KYC (Know Your Customer) obrigatório nos accounts. PCI-DSS escopo expande.
Padrão de segurança pra cartão. Níveis baseados em volume.
Sempre evite tocar PAN. Use elements/checkout. Nunca log PAN, CVV.
Ledger interno vs extrato PSP devem bater. Diariamente:
txn_id ou correlation ID.Automation: job batch nightly, dashboard com discrepancy aberto. Senior aceita zero não explicado.
Token (pm_xxx) + customer id PSP. Re-cobrança recorrente via token. Customer Portal (Stripe) permite usuário gerenciar.
Update card detection: PSP atualiza expiry/PAN automaticamente em alguns casos (Account Updater). Ative.
Pix é instant payment via BCB. PSPs intermediam: Stripe ainda limitado, Mercado Pago/Pagar.me/iugu oferecem. Fluxo: gera QR / copia-e-cola → user paga → webhook em segundos.
Reconcile via end-to-end ID (E2E ID). Refund via reversal Pix com janela.
Pagamento offline. Gera boleto, customer paga em banco/app. Liquidação D+1 a D+3. PSPs (Pagar.me, MP) emitem.
Cancele automaticamente após vencimento. Dunning: enviar 2º email, gerar 2ª via.
Você precisa, sem consultar:
Adicionar billing à Logística: pagamento de pedidos com split entre lojista, plataforma e entregador.
stripe_account_id em users.application_fee_amount (plataforma) e transfer_data.destination (lojista). Entregador recebe via transfer separado pós-entrega./webhooks/stripe valida signature, dedupe (tabela processed_webhook_events), processa async via fila.payment_intent.succeeded, payment_intent.payment_failed, charge.refunded, charge.dispute.created, transfer.created.ledger_entries imutável, double-entry./orders/:id/refund (full ou partial), gera reversal entries, chama Stripe refund.balance_transactions últimas 24h.external_id. Diff list em tabela reconciliation_discrepancies.amount_cents bigint, currency char(3) em todas as colunas monetárias.dinero.js no app code.Order.status com transições validadas.