Skip to main content
The cheapest failed charge is the one that never runs. If a customer has topped up their wallet, a renewal should spend that balance first — no gateway round-trip, no decline, no dunning. That’s wallet-first billing: the renewal engine tries the wallet, and only falls back to the card if the wallet can’t cover the charge.

The order of preference

The sequence is wallet → card → dunning, and it runs on every renewal cycle:
  1. The engine creates the next-period invoice (idempotent on renewal_{subId}_{periodStart}, so a double-scan can’t double-bill).
  2. If the subscription’s customer has a matching CustomerAccount by email, it attempts a wallet debit for the plan amount, referenced by the invoice id.
  3. Wallet covered it → the invoice is marked paid with rail: wallet, no gateway is called at all.
  4. Wallet short or absent → it falls back to charging the saved card (rail: card).
  5. Card declines → it hands the invoice to the dunning engine exactly as before.
The wallet debit is all-or-nothing — it only succeeds if the balance covers the whole plan amount. Duro never part-pays an invoice from the wallet and charges the remainder to a card; a partial balance simply falls through to the card path untouched.

Why this is a recovery feature, not just a payment feature

Duro is recovery-first: the whole system is built to turn money-at-risk into money-recovered. Wallet-first billing moves that fight upstream of the failure. A customer who tops up ₦20,000 has pre-funded their next four ₦5,000 renewals — those four charges will never decline, never enter dunning, never send a “your card failed” email, and never risk an involuntary churn. The best dunning campaign is the one that never has to start.

Idempotency carries through

The debit is referenced by the invoice id (walletdebit_{invoiceId}), so the wallet’s (account, reference) uniqueness guarantees a given invoice draws from the balance at most once — even if the renewal job is retried after a wallet debit succeeded but before the invoice was marked paid. The retry finds the debit already applied and proceeds to settle the same invoice, rather than double-charging the balance.

What the ledger records

A wallet-funded renewal posts to the finance ledger with the wallet as the funding source: the renewal is recorded as recognised revenue, and the wallet balance is drawn down via a wallet_debit entry. Reporting sees the same MRR it always did — the source of the cash changed, not the revenue. Next: the dunning engine that still catches the renewals the wallet couldn’t cover.