SEMANTIC BILLING
Invoice Truth
The invoice is the unit of collection — its state drives dunning, webhook routing, entitlements, and reconciliation. But today the invoice lies: consolidation hides prorations, uncollectible invoices show charges never collected. Credit notes fix this by making every adjustment a first-class accounting instrument.
PDE must ship before credit notes can reference clean invoices
credit notes enable automatic reinstatement in the brain
The invoice is the central record — not the subscription. All payment retry logic, dunning state, and revenue recognition pivot on invoice events and state transitions.
The amount shown on a Stripe invoice should equal the actual amount the customer owes. Today it doesn't — the invoice shows the original billed amount while the real debt is hidden in metadata.
Trying to prevent the user from paying the original invoice is counterintuitive AND a losing battle. They see it, they click it, and the system either blocks them or double-charges.
When payment fails and the invoice goes uncollectible, the user ends up with two invoices in their dashboard: the original they recognize and a synthetic consolidated invoice they don't. They don't know which to pay.
Draft invoices are created but not finalized. Editable. Payment not yet due. Dunning doesn't process draft invoices.
Finalized invoices are ready for payment. Invoice details locked. Payment due date set.
Payment succeeded. Revenue recognized. Dunning state cleared. All retry counters cleared.
Retries exhausted. Marked as uncollectible. Account may be flagged bad_debt. May trigger subscription cancellation.
When an invoice goes uncollectible, issue a credit note for the unused in-advance service. The invoice amount adjusts. That adjusted amount is the debt. No metadata. No consolidation.
Pipeline: receive invoice.marked_uncollectible event → classify line items as in-advance or in-arrears → calculate unused fraction per in-advance item → issue credit note via Stripe API → invoice amount_remaining adjusts to the debt amount.
For each in-advance line item: (days_remaining / total_days_in_period) × line_item_amount. Sum these to get the credit note total. In-arrears items get zero credit.
Five steps: user returns → sees original invoice with adjusted amount → pays via Unified Checkout → bad-debt flag lifted → new subscription created with fresh BCA. No synthetic invoices.
Migrating the 906 to honest invoicing means issuing retroactive credit notes against their uncollectible invoices for unused in-advance time. The invoice amount adjusts. When the user returns, the new flow pays the original invoice at its corrected amount.
After all batches complete, run consistency check across all 906 accounts. Verify: credit note exists, amount_remaining correct, old metadata preserved (historical), reinstatement path works.
Bad debt dropped 93% — from ~13.7K to ~1.3K in three days during the November cleanup. The strongest proof that the recompute engine works at scale.
If Stripe rejects credit notes on uncollectible invoices, the entire mechanism needs rethinking. This is the load-bearing prerequisite.
If Stripe rejects credit notes on uncollectible invoices, the entire approach is blocked. Fallback: 1:1 replacement invoices (simpler than consolidated, but still synthetic).
Credit notes make every invoice tell the truth — the amount shown is the amount owed.