Subscription Dunning & Fa
Smart retry, card-expiry nudges and involuntary-churn recovery for failed renewals (BYOK PSP)
Failed card renewals are silent revenue leaks: a declined charge quietly cancels a subscription and the customer churns without ever meaning to. Chasing those failures by hand is slow and most teams simply lose the revenue. This module adds a full dunning engine on top of Odoo invoicing for SaaS and subscription businesses, with configurable retry back-off, soft/hard decline routing, card-expiry nudges and escalating recovery campaigns that suspend or cancel only when every attempt is exhausted. A cron drives it hands-free, and it runs on your own payment-provider keys (BYOK) so no card data is ever stored or proxied in Odoo.
Key Features
Configurable retry plans with back-off
gb.dunning.plan sets max_attempts, grace_days and ordered gb.dunning.plan.step rows with per-step delay_hours. get_delay_for_attempt returns the wait before each retry, falling back to an exponential 6h/24h/72h/168h schedule when no explicit steps are defined.
Soft vs hard decline routing
classify_reason matches the PSP reason code against the plan's soft_decline_reasons (insufficient funds, issuer unavailable) and hard_decline_reasons (lost/stolen card, do-not-honor). Soft declines are rescheduled while a hard decline aborts retries immediately, so effort goes where it can succeed.
Card-expiry nudges
gb.dunning.campaign stores card_last4, card_exp_month and card_exp_year. _is_card_expiring compares against the plan's expiry_nudge_days, and _send_expiry_nudge emails the customer before the token expires, setting card_expiry_warned so the reminder is sent once.
Per-invoice recovery campaigns
Each failed account.move out_invoice drives a campaign through draft, active, recovered, suspended and cancelled states on mail.thread. run_retry charges, logs the outcome and either marks recovered or schedules the next_attempt_date until max_attempts or the grace_deadline is reached.
Configurable exhaustion action
When all retries fail, _finalise_exhausted applies the plan's final_action: suspend the campaign (when auto_suspend is on), cancel it, or schedule a mail todo activity for manual review. Recovery never silently drops a customer without the action you chose.
Full attempt log and KPIs
Every cycle writes a gb.dunning.attempt with attempt_number, success/failed state, reason_code and charged amount; _compute_reason_class tags each as success/soft/hard. Campaigns roll up attempt_count, failed_count and recovered_amount so you can measure revenue saved.
Hands-free cron processing
cron_process_due_campaigns runs run_retry on every active campaign whose next_attempt_date is due, keeping the batch alive past individual errors. cron_expiry_nudges sweeps active campaigns for soon-to-expire cards, so the whole recovery flow runs unattended.
BYOK PSP, no card data stored
_charge posts a real HTTP request to the customer's own PSP (Stripe, Adyen, Braintree, GoCardless or a custom HTTP gateway) using the campaign's own psp_api_key/psp_api_secret and the stored psp_token_ref; no PAN is ever held in Odoo, only last-4 and expiry for display. Without an API key the campaign simply runs in simulation/preview mode instead of failing. An account.move action_open_dunning button and start_dunning_for_overdue helper launch campaigns from invoices.
Use Cases
Screenshots
Retry Attempts
Dunning Campaigns
Dunning Plans
Run Dunning Retries
Why Choose This Module
Failed card renewals are silent revenue leaks. This module adds a full dunning engine on top of Odoo invoicing and subscriptions: smart retry schedules, card-expiry reminders before the next charge, and escalating recovery campaigns that suspend or cancel only when every attempt is exhausted. It runs on your own payment-provider keys (BYOK) — we never store or proxy card data.
Specifications
- Compatible: Odoo 18.0 / 19.0
- License: OPL-1
- Languages: English
- Author: Pokutsoft
- Dependencies: account, mail
- Support: support@pokutsoft.com
Update date: 2026-07-16