ReferenceSubscriptions

Subscriptions

The subscriptions module (schema subscriptions) is a real recurring-billing engine: a billing schedule, a renewal sweep, and a dunning ladder driven off a vaulted merchant-initiated (MIT) credential. The MIT/vault primitive itself lives in payments (the PaymentProvider.chargeVault operation + the mit stored-credential context); this module builds the schedule/renewal/dunning that commerce explicitly dropped at its founding. It charges through the payments provider port (the vendor-neutral seam payments publishes) and books revenue to accounting through the same in-process repo seam commerce and cash use (createAccountingModule(deps).repo) — never hand-rolled HTTP, never the bus.

Exactly-once renewal is the core invariant: a renewal is claimed as a UNIQUE (subscription, period) invoice under a row lock, charged with the deterministic key sub-renewal:{sub_id}:{period}, and next_bill_at advances only inside the settle transaction — so a re-running sweep never double-charges, and an indeterminate (transport-failure) charge is never treated as a decline. See Money invariants.

Operator routes

x-operator-token, rate-limited before the token check.

MethodPathPurpose
POST/v1/subscriptionsCreate a subscription (tenant_ref in the body).
GET/v1/subscriptions/:idFetch one subscription.
POST/v1/subscriptions/:id/pausePause an active subscription.
POST/v1/subscriptions/:id/resumeResume a paused subscription.
POST/v1/subscriptions/:id/cancelCancel a subscription.
POST/v1/ops/subscriptions/renewal-sweep/runOperator-triggered run of the renewal sweep (also runs on a timer).
POST/v1/ops/subscriptions/renewal-reconcile/runOperator-triggered run of the renewal reconcile pass.
POST/v1/ops/subscriptions/renewal-reminder/runOperator-triggered run of the ARL/§17600 renewal-reminder sweep.
POST/v1/ops/subscriptions/revenue-posting-drain/runOperator-triggered drain of the accounting revenue-posting outbox.
GET/v1/subscriptions/auditOperator audit-log read (makeAuditReadRoute).

An invalid lifecycle transition returns 409 invalid_transition; a failed write returns 503, never a false success.

Shopper-scoped routes (storefront BFF)

Registered outside the operator gate and accepted with either the operator token or the narrower storefront token (the same B7-4 pattern commerce/payments use). Every route is scoped to (tenant, customer_ref) and a mutation is ownership-checked in the repo, so a shopper can only touch their own subscription.

MethodPathPurpose
GET/v1/tenants/:tenant/customers/:customerRef/subscriptionsList a shopper’s own subscriptions.
POST/v1/tenants/:tenant/customers/:customerRef/subscriptions/:id/cancelShopper self-serve cancel (immediate; a cancellation-confirmation email is sent).
POST/v1/tenants/:tenant/customers/:customerRef/subscriptions/:id/pauseShopper self-serve pause.
POST/v1/tenants/:tenant/customers/:customerRef/subscriptions/:id/resumeShopper self-serve resume (next_bill_at recomputes forward — never back-bills).

Renewal reminders (auto-renewal law)

Every store sends a pre-billing renewal reminder; the sweep covers active and past_due subscriptions. renewalReminderDaysBeforeBilling is per-store (default 7) with an enforced floor of 3 — values below the floor clamp to the default and are logged; reminders cannot be configured off. A shopper’s marketing unsubscribe never blocks these (they are transactional); a hard bounce records the reminder as undeliverable, visibly, never as sent. Subscription creation at storefront checkout requires recorded consent evidence (subscriptionConsent — disclosure version, content hash, IP hash, user agent), persisted on the subscription row.

Merchant routes

fdk_ credential (Authorization: Bearer), tenant resolved from the credential per credential-carries-ownership, scope manage:own_subscriptions. The portal’s Subscriptions page. A failed read is a 503, never an empty list.

MethodPathScopePurpose
GET/v1/merchant/subscriptionsmanage:own_subscriptionsList the caller’s subscriptions.
GET/v1/merchant/subscriptions/:idmanage:own_subscriptionsFetch one (cross-tenant/unknown are an indistinguishable 404).
POST/v1/merchant/subscriptions/:id/cancelmanage:own_subscriptionsCancel.
POST/v1/merchant/subscriptions/:id/pausemanage:own_subscriptionsPause.
POST/v1/merchant/subscriptions/:id/resumemanage:own_subscriptionsResume.

Events

Declared empty deliberately (see module.yaml) — this module posts revenue to accounting via the same in-process repo seam commerce/cash use, and records every state change to its own audit_log, so it publishes and consumes no EventBridge events.

DirectionEvent
Publishes(none — in-process accounting seam + audit log)
Consumes(none)

Manifest gates

Two behavior keys in the store manifest gate the storefront surfaces this module powers:

  • behavior.subscriptions.enabled — the Subscribe & Save switch . Default false; behavior.subscriptions.discounts maps interval-days ("30", "60") -> percent off, and behavior.subscriptions.autoEnrollBundles defaults false.
  • behavior.accounts.subscriptionSelfServe — the account-page self-serve cancel/pause surface. Defaults true.

Seams (honest notes)

  • Renewal + dunning run off a sweep, not a scheduler service — renewal-sweep/run is both a timer target and an operator-triggerable drain (the beacon dispatch/run precedent).
  • The MIT credential is vaulted by payments; subscription creation at checkout is documented on the Seam closures page (subscribe-and-save).