Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Subscription & Recurring Billing

A design reference for plan-driven invoice generation on a recurring schedule

1. Requirements

1.1 Functional requirements

  • Attach a party (a customer being billed, or a supplier paid recurringly) to a Billing Arrangement tracking one or more priced Recurring Plans, a status, and a rolling current billing period.
  • Generate a real receivable or payable invoice once a billing period is due, sized from the arrangement’s plan lines, stamped with the period covered (from_date / to_date) and a back-reference to the arrangement.
  • Support three timing modes for when the invoice is raised: end of period (arrears), start of period (prepaid), or N days before the period starts (advance notice).
  • Support an optional trial window before real billing starts; invoices generated during a trial are fully discounted rather than suppressed outright.
  • Drive the arrangement’s status from whether the most recent invoice is settled: unpaid-past-due moves it into a grace window, and past that into an unpaid or auto-cancelled state, per policy.
  • Support proration: fixed-rate and price-list plans scale down for a partial period; a flat monthly-rate plan is instead prorated by the fraction of the start/end calendar month covered.
  • Allow immediate cancellation or a cancel-at-period-end toggle, and let a cancelled arrangement be restarted from a fresh period.
  • Let many arrangements be processed on a schedule, and let one be re-processed or force-caught-up on demand.
  • Cap how many cycles a single pass fires for one arrangement, so an idle or newly-imported arrangement doesn’t generate years of back invoices in one call.

1.2 Non-functional requirements

  • Idempotency: re-running the scheduled process against the same arrangement on the same day must not create a second invoice for an already-invoiced period.
  • Fault isolation: one arrangement failing during a bulk run must not stop the rest of the batch, and the failure must be recorded, not swallowed.
  • Consistency with settlement state: paying (or reversing) an arrangement’s invoice must recompute its status immediately, not at the next scheduled pass.
  • Currency and interval consistency: all plans on one arrangement share a billing interval, and the currency must agree with the party’s billing currency.

1.3 Constraints

  • Recurring billing is a producer of ordinary receivable/payable invoices, not a parallel invoicing system — generated documents are the same invoice type used everywhere else, just system-created and pre-populated.
  • The subsystem never talks to an external payment channel itself; a plan may only optionally record which collection channel a downstream collection ask should use. Actually requesting or capturing payment is the payment-provider integration layer’s concern.
  • Proration, trial handling, and calendar-month alignment are governed by one global settings record, not per-arrangement overrides.

2. High-Level Design

2.1 Component diagram

2.2 Walkthrough

  1. A daily scheduled task creates and submits a Billing Run, for every non-cancelled arrangement or one named arrangement.
  2. The run fans candidate arrangements out in batches onto a background queue, so a large portfolio does not block the scheduler thread.
  3. Each arrangement is asked to process() for the run’s posting date: it checks whether the current period already has an invoice and whether firing one is actually due (§3.2); if so, it builds a receivable or payable invoice from the plan lines, applies proration if configured, and (by default) submits it.
  4. After generating an invoice, the arrangement’s current-period window advances (or the arrangement completes/cancels if past its end date), and its status is recomputed from the invoice’s state.
  5. Separately, whatever eventually settles that invoice — a directly recorded settlement, or one arriving through a collection ask raised against it — triggers the same status recomputation, so a payment snaps the arrangement back to Active without waiting for the next scheduled run.

3. Deep Dive

3.1 Data model

Billing Arrangement The header record: party_type / party (Customer or Supplier — drives receivable or payable billing), status, start_date / end_date, optional trial dates, the rolling current_invoice_start / current_invoice_end window, days_until_due, generate_invoice_at (the three timing modes from §1.1), cancel_at_period_end, and generate_new_invoices_past_due_date (keeps billing even while a prior invoice is unpaid). It holds one or more Plan Lines, which must all share one billing interval — no mixing a monthly plan with an annual one.

Recurring Plan A reusable, priced offering: an item, currency, cost center, a billing interval (day/week/month/year) with a repeat count, and one of three price-determination modes — fixed rate, price-list lookup, or flat monthly rate. A plan may also reference a collection channel, used to validate that a downstream collection ask for its invoices settles into the channel the plan expects — the plan itself does not initiate that ask.

Plan Line A child row on a Billing Arrangement: which Recurring Plan, and what quantity.

Recurring Billing Settings A single global record: whether proration is on, how many grace days an overdue invoice gets before the arrangement is marked unpaid, and whether it should instead auto-cancel once grace lapses.

Billing Run A submittable record carrying a posting date and an optional single-arrangement scope; submitting it kicks off processing, so a bulk billing pass is itself an auditable, re-runnable transaction rather than a bare function call.

Retired design note: an early version linked each generated invoice back to its arrangement through a separate join table, since retired for a direct reference field on the invoice (migrated once). At least one other call site in the codebase still queries the old table and silently finds nothing.

3.2 Scheduling and period math

The rolling billing period is a pair of dates, recomputed rather than stored as independent facts:

  • Period length comes from the plan’s billing interval: day-based runs interval_count − 1 days past its start; week-based runs interval_count × 7 − 1 days; month/year-based runs interval_count months/years past its start, minus one day. Follow calendar months instead snaps the period end onto the true calendar month/quarter end (requires a month-based interval and a defined end date). A computed end past the arrangement’s own end date is clamped to it.
  • When the next invoice fires: arrears at the period end, prepaid at the period start, “days before” a fixed number of days ahead of the start.
  • Whether generation is allowed is gated together: not already cancelled; not blocked by an outstanding prior invoice unless the past-due override is set; posting date reached the trigger date; and — the catch-up cap — posting date is not more than one additional cycle past the period’s end, so one call never bills more than a cycle ahead.
  • Double-invoicing is prevented by checking, before generating anything, whether an invoice already exists posted inside the current period; if so, the period is treated as already billed.
  • After generation, the period advances to the day after the period just billed, unless that crosses the end date — the arrangement then auto-cancels (if so configured) or is left for the status step to mark completed.
  • A freshly created arrangement with a past start date is caught up immediately: it bills every elapsed period up to today, stopping early on cancellation, a non-advancing period, or an unpaid invoice blocking further generation.
  • A bulk pass processes arrangements in batches on a background queue; each runs inside its own try block, so one validation failure is logged and skipped rather than aborting the batch.

3.3 Status lifecycle

Status is recomputed wholesale, not incrementally transitioned — every recomputation re-evaluates a fixed precedence: trial status first, then completion (no outstanding invoice and past the end date), then past-grace, then past-due-but-within-grace, then simply active. If none apply (an invoice exists but isn’t yet due), the status is left as-is. “Past due” and “past grace” key off the most recent invoice’s due date and paid state, with the grace threshold being the settings record’s day count added to that due date.

Two independent triggers cause a recomputation: the scheduled process() pass, and settlement activity. Recording a settlement against an invoice, or reversing one, re-checks every arrangement referenced by that settlement’s invoices and recomputes their status immediately — this is what snaps an arrangement back to Active as soon as a payment is recorded, rather than waiting for the next scheduled run.

3.4 Proration

Two distinct proration mechanisms coexist:

  • For fixed-rate and price-list plans, proration (when the settings toggle is on) scales the line amount by the fraction of the period elapsed as of today. This factor is forced to 1 whenever the timing mode is prepaid or days-before — proration under this mechanism only ever applies to the arrears mode, typically shrinking a first invoice starting partway through a period.
  • For the flat monthly-rate mode, a separate calendar-day-based calculation instead prorates by how many days of the start/end month fall outside the period — independent of the arrears/prepaid distinction above.

3.5 Interaction contract (illustrative)

Arrangement.process(posting_date)
  -> if due and not yet invoiced: generate invoice, advance period, recompute status

Arrangement.cancel()
  -> status = Cancelled; optionally bills a final short invoice for the elapsed
     part of the current period

Arrangement.restart(posting_date)
  -> only valid from Cancelled; status = Active, fresh period from posting_date,
     prior invoices untouched

Arrangement.force_fetch_updates()
  -> computes the correct trigger date for the timing mode and calls process()
     for it even if today falls outside the stored period window

3.6 Error handling and idempotency

  • Duplicate generation is prevented by the current-period invoice check in §3.2 — safe to call process() repeatedly for the same date.
  • Runaway catch-up is prevented by the one-cycle cap on how far past the current period end a single call will bill.
  • A failing arrangement inside a bulk run is caught, logged against it, and the batch continues rather than aborting.
  • An unpaid prior invoice blocks further billing by default; the past-due override exists for businesses that want billing to keep accumulating, leaving overdue-balance handling to a separate collections process.
  • Currency and interval mismatches are rejected at validation time — plans on one arrangement must share a billing interval, and the currency must match the party’s billing currency or the legal entity’s default.
  • Trial safety: an invoice generated while trialing is still a real, submitted document — not skipped — but carries a forced 100% discount, leaving an audit trail without any amount owed.

4. Scale and Reliability

  • Load pattern: driven by the daily schedule, not user traffic — a single Billing Run can reference thousands of arrangements, so processing never runs inline on the scheduler thread.
  • Batching: candidate arrangements are split into fixed-size batches and enqueued onto a background queue, bounding memory/time per job.
  • Fault containment: each arrangement runs inside its own error boundary; one validation failure is rolled back and logged without affecting the rest of the run.
  • Idempotent re-entry: “already invoiced” and “not yet due” are both re-checked from persisted state, so re-running a Billing Run for an already-processed date is safe.
  • Status freshness: status is recomputed from scratch on every relevant event (processing, settlement, reversal) rather than patched, so a read is only as stale as the last event that touched the arrangement.
  • Monitoring: alert on arrangements stuck in Grace Period past their window, and on the rate of per-arrangement error-boundary log entries during bulk runs — a rising rate signals a systemic issue rather than isolated party-level problems.

5. Trade-off Analysis

Decision Trade-off
Recurring billing generates ordinary invoices, not a parallel artifact Downstream processes (tax, discounting, deferred recognition, the general-ledger posting funnel) work unmodified, at the cost of limited control once an invoice exists.
Status recomputed wholesale, not transitioned incrementally Simple and self-healing, at the cost of re-deriving several conditions on every save rather than recording the “why” of the last transition.
One global settings record for grace period, proration, and auto-cancel Simple to administer, but every arrangement shares the same policy — no per-plan or per-segment lever today.
Catch-up on creation bills every elapsed period in one call Gets a backdated or imported arrangement current immediately, at the cost of one save potentially generating many invoices synchronously.
A single-cycle cap bounds how far ahead one call will bill Protects against runaway billing after an outage, but recovering from a genuinely long one needs multiple passes rather than one.
Settlement activity directly triggers status recomputation Keeps status accurate immediately after a payment, at the cost of coupling the settlement path to an arrangement-specific side effect.
Two independent proration algorithms (elapsed-fraction vs. calendar-day-exclusion) Each fits its own price mode naturally, but the two can disagree if a plan changes mode, with no shared abstraction between them.

6. What to Revisit as the System Grows

  • Per-arrangement or per-plan grace/proration policy: a single global settings record is workable for a small, homogeneous customer base; a platform serving many distinct billing policies will want this scoped lower than global.
  • A genuinely long scheduler outage: the one-cycle catch-up cap is a good safety valve for a normal missed run, but recovering an arrangement un-processed for many cycles needs repeated manual invocation rather than one bounded recovery path.
  • Unifying proration: the two independent algorithms should converge if plans are ever allowed to change price-determination mode mid-life, to avoid inconsistent partial-period charges.
  • The retired invoice-arrangement join table: at least one call site elsewhere still queries it and silently returns nothing; worth cleaning up so it fails loudly, or is removed.
  • This module is genuinely compact: one header record, one plan record, two thin child tables, one settings singleton, one batch trigger. The real complexity is the date arithmetic and status precedence above, not entities left out here.

Was this page helpful?