Problem/Motivation
Sites need subscriptions that combine a flat recurring fee with usage-based (metered) charges computed each billing period. Subscription Manager has no concept of usage today.
Design discussion has clarified that usage support splits into two parts, and only one of them varies by connector. The rating step (turning measured usage into a monetary amount for the period) is site-domain logic and identical regardless of processor. The delivery step depends on which scheduling mode a connector uses (see #3618739: Add a charge-based connector mode with a local billing cycle engine for the mode taxonomy):
- Self-scheduling connectors (the remote service owns the billing cycle and generates the charge itself, e.g. Stripe Billing): usage or its amount must be pushed to the remote service before it finalizes the period's invoice (e.g. Stripe's Billing Meters API or invoice items).
- Charge-based connectors (Subscription Manager owns the cycle per #3618739: Add a charge-based connector mode with a local billing cycle engine, e.g. GoCardless #3616779: GoCardless connector module (bank debit: Bacs, SEPA, ACH, PAD), Helcim #3618742: Helcim connector module (charge-based card payments, CAD/USD)): there is nothing to deliver separately; the rated amount is simply folded into the period charge the engine requests. These connectors need no metering concept at all.
Proposed resolution
Define a UsageRaterInterface service seam: given a subscription and a billing period, return rated lines — each with a label, quantity, unit amount, and an authoritative total. Subscription Manager provides the interface, a RatedLine value object, and the invocation points; sites (or contrib) implement raters as tagged services, and multiple raters compose (their lines concatenate). No measurement or aggregation is implemented in this module.
For charge-based connectors: the #3618739: Add a charge-based connector mode with a local billing cycle engine cycle engine invokes the raters and includes the result in the computed period amount — usage rated in arrears over the just-ended period, the base fee unchanged in advance — and records the lines on the charge for the audit trail and the future tax seam (#3618741: Add a pluggable tax/invoice service seam (calculation, evidence, invoice reference), keeping tax engines out of core). No connector-facing API is added for this path.
Delivery for self-scheduling connectors is deferred to #3615738: Support metered (usage-based) prices alongside licensed subscription items: a self-scheduling subscription has no local cycle anchor, so the delivery cadence and period-window semantics can only be designed against a concrete remote metering API. The Stripe work defines them; this issue deliberately adds no speculative delivery interface.
Remaining tasks
Patch with a test rater and kernel tests; change record.
User interface changes
None in this issue. Rated lines are visible to administrators in the charge record's data; subscriber-facing usage display in the portal can follow separately.
API changes
New UsageRaterInterface and RatedLine value object; a new tagged-service collection point. No changes to existing connectors or interfaces.
Data model changes
None. Rated lines are recorded in the existing charge entity's data map.
Release notes snippet
Sites can now bill usage-based (metered) charges alongside a flat recurring fee: implement the new UsageRaterInterface as a tagged service and the billing cycle engine folds the rated amounts into each period's charge, recording the lines on the charge record. Charge-based connectors need no changes; pushing usage to self-scheduling services like Stripe Billing follows separately.
Issue fork subscription_manager-3615735
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
colanComment #3
colanComment #4
colanProblem/Motivation
Sites need subscriptions that combine a flat recurring fee with usage-based (metered) charges computed each billing period. Subscription Manager has no concept of usage today.
Design discussion has clarified that usage support splits into two parts, and only one of them varies by connector. The rating step (turning measured usage into a monetary amount for the period) is site-domain logic and identical regardless of processor. The delivery step depends on which scheduling mode a connector uses (see #3618739: Add a charge-based connector mode with a local billing cycle engine for the mode taxonomy):
Proposed resolution
UsageRaterInterfaceservice seam: given a subscription and a billing period, return rated line amounts. Subscription Manager provides the interface and invocation points; sites (or contrib) implement raters. No measurement/aggregation is implemented in this module.deliverUsage($subscription, array $rated_lines), in a separate interface so existing connectors are unaffected) invoked by cron ahead of the remote cycle close. The Stripe connector's implementation maps rated lines to its metering/invoice-item APIs in its own project.Remaining tasks
Agree the rater interface shape (per-line: label, quantity, unit amount, total); patch with a test rater; change record. Sequencing: depends on #3618739: Add a charge-based connector mode with a local billing cycle engine for the charge-based path; the self-scheduling delivery method can land independently.
User interface changes
None in this issue; usage display in the portal can follow.
API changes
New
UsageRaterInterface; new optional connector interface for usage delivery. Existing connectors unaffected.Data model changes
None here; period/charge storage belongs to #3618739: Add a charge-based connector mode with a local billing cycle engine.
Comment #5
colanUpdated the summary: the components-field text was superseded by the rating/delivery split in #4, and the delivery half for self-scheduling connectors is now explicitly deferred to #3615738: Support metered (usage-based) prices alongside licensed subscription items (rationale in the summary — no local anchor means no honest way to design the cadence and period window until a concrete remote metering API is on the table; nothing in the charge-based path waits on it).
Concrete design for the rating seam:
UsageRaterInterface: one method,rate(SubscriptionEntityInterface $subscription, int $period_start, int $period_end): array, returning a list ofRatedLinevalue objects. A rater may return zero lines (usage within the plan's allowance), one, or several (itemized overages, tier bands); implementations must be side-effect free and idempotent for a given period.RatedLine: final value object withlabel,quantity(free-form string, e.g. "312 GB" — display only),unitAmountandtotalas decimal strings in major currency units, matchingamount_basis. Thetotalis authoritative: tiered pricing means quantity × unit does not always equal total. Currency is the subscription's; a line carries no currency of its own.subscription_manager.usage_rater), collected in priority order. Multiple raters compose — the engine concatenates all lines from all raters, so independent modules can each contribute lines without knowing about each other.computePeriodAmount()becomes base + Σ line totals, with usage rated in arrears over the just-ended period ([previous anchor, anchor]) while the base fee stays in advance for the period being opened — the standard hybrid shape. The rated lines are stored on the charge record indata['rated_lines'](label, quantity, unit_amount, total, and the usage window), which is the audit trail now and the input to the tax seam (#3618741: Add a pluggable tax/invoice service seam (calculation, evidence, invoice reference), keeping tax engines out of core) later. For a subscription's very first anchor the arrears window predates the subscription; raters simply find no usage there and return no lines.upgrade()concerns the base fee only; usage keeps accruing and is rated at the next anchor as usual.subscription_manager_test; kernel coverage for usage folded into the charge amount, lines recorded on the charge, multiple raters composing, zero-base-with-usage charging, usage-window bounds passed to raters, and the no-raters case remaining byte-identical to today's behavior.No data-model changes and no changes to existing connectors. Change record to follow with the MR.
Comment #7
colanImplemented in the MR, per the updated summary and #5, in two commits.
The seam.
UsageRaterInterface::rate($subscription, $period_start, $period_end)returns zero or moreRatedLinevalue objects — label, display quantity, unit amount, and an authoritative total as major-unit decimal strings (malformed money is rejected at construction, while the rater that produced it is still on the stack). Raters register as services taggedsubscription_manager.usage_rater, collected on the cycle engine in priority order via the standard service-collector pattern; multiple raters compose by concatenating their lines. Implementations must be side-effect free and idempotent per period, since a retried charge rates the same period again.Engine integration. At each cycle anchor the engine rates usage in arrears over the period that just ended (day-clamped like every other anchor computation) while the base fee stays in advance, folds the line totals into the period amount, and records the lines with their usage window in the charge record's
data['rated_lines']— the audit trail now, the #3618741: Add a pluggable tax/invoice service seam (calculation, evidence, invoice reference), keeping tax engines out of core tax seam's input later. Zero base with rated usage charges normally (a free plan with metered overage is a legitimate shape); zero base with no usage keeps the settled-locally path from #3618739: Add a charge-based connector mode with a local billing cycle engine. With no raters registered, billing is byte-identical to before. Proration inupgrade()remains base-only. No data-model or config changes, so no update hook — a cache rebuild picks up the collector.Tests. New
UsageRatingTest(six scenarios) driven by a state-controlledTestUsageRaterregistered twice to prove composition and priority order; the existing suite passing unchanged is the no-raters regression check. Suite is at 90 kernel tests / 1103 assertions.Change record drafted. Delivery for self-scheduling connectors follows in #3615738: Support metered (usage-based) prices alongside licensed subscription items as scoped in the summary.
Comment #10
colan