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):

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.

Command icon 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

colan created an issue. See original summary.

colan’s picture

colan’s picture

colan’s picture

Title: Support usage-based (metered) components on subscriptions » Add a rating step for usage-based charges, delivered per connector scheduling mode

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):

Proposed resolution

  • Define a UsageRaterInterface service 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.
  • For charge-based connectors: #3618739: Add a charge-based connector mode with a local billing cycle engine's cycle engine invokes the rater and includes the result in the computed period amount. No connector-facing API is added for this path.
  • For self-scheduling connectors: add an optional connector interface method (e.g. 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.
  • Rated lines pass through the tax/invoice seam (#3618741: Add a pluggable tax/invoice service seam (calculation, evidence, invoice reference), keeping tax engines out of core) like any other lines, so usage charges are taxed and invoiced consistently.

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.

colan’s picture

Issue summary: View changes

Updated 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 of RatedLine value 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 with label, quantity (free-form string, e.g. "312 GB" — display only), unitAmount and total as decimal strings in major currency units, matching amount_basis. The total is authoritative: tiered pricing means quantity × unit does not always equal total. Currency is the subscription's; a line carries no currency of its own.
  • Registration: tagged services (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.
  • Engine integration: at the cycle anchor, 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 in data['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.
  • Zero-amount interplay: base 0 with no usage keeps the settled-locally path from #3618739: Add a charge-based connector mode with a local billing cycle engine; base 0 with rated usage charges normally (a free plan with metered overage is a legitimate shape).
  • Upgrades/downgrades: unaffected. Proration in upgrade() concerns the base fee only; usage keeps accruing and is rated at the next anchor as usual.
  • Tests: a state-controlled test rater in 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.

colan’s picture

Status: Active » Needs review

Implemented in the MR, per the updated summary and #5, in two commits.

The seam. UsageRaterInterface::rate($subscription, $period_start, $period_end) returns zero or more RatedLine value 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 tagged subscription_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 in upgrade() 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-controlled TestUsageRater registered 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.

  • colan committed a55533de on 1.0.x
    Issue #3615735: Cover the rating seam with a state-controlled test rater...

  • colan committed 9df60006 on 1.0.x
    Issue #3615735: Add the usage rating seam to the billing cycle...
colan’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.