Subscription plans
A subscription plan defines which products a subscription includes, what they cost each billing period and whether it renews automatically.
A subscription plan defines which products a subscription includes, what they cost each billing period and whether it renews automatically.
Stay in the loop 🚀
A plan price edit changes the plan and eligible package prices in one database transaction. Existing subscriptions have their own stored prices, updated through separate workflow or deferred processing. This separation explains why the plan can show a new amount while a subscription still shows the old one.
This reference describes source behavior at main revision 932b074, inspected on 28 September 2026. It does not establish the deployed revision or the outcome of a particular price change.
| Value | Role |
|---|---|
Product.price | Default copied when the admin adds the product to a plan. |
SubscriptionPlanProduct.price | Recurring unit price for that product on this plan, keyed by subscriptionPlanId and productId. |
PackageProduct.price | Starting unit price for the product in the sale offer. It can differ from the plan price. |
MemberPackageProduct.price / recurringPrice | The purchased subscription's stored starting and recurring unit prices. |
MemberPackage.currentPrice / recurringPrice | Stored amount for the current period, initialized from the purchase payment / recurring total calculated from subscribed products. |
currentPrice is not permanently the first payment: the rewrites it from recurringPrice.
At purchase, Subrite stores MemberPackage.subscriptionPlanId and copies the plan's autoRenew. The recurring total is calculated from the selected subscription products, quantities and applicable shipping. It is not a live lookup of the plan's displayed total. The ordinary recurring-total helper calculates Σ((product recurringPrice + shipping) × quantity) and waives shipping only when both starting and recurring product prices are zero. , , , .
The update service locks the tenant's plan row, validates product changes and upserts the plan-product records. It starts propagation only if an existing product's old and new prices differ. Adding a product or saving the same price does not meet that condition.
The propagation workflow receives the submitted planProducts as a snapshot. It counts subscriptions attached to the plan, divides them into batches of 500 and runs up to 5 batches concurrently. Selection has no Active-status filter. A completed save therefore establishes neither that every batch ran nor that every subscription was eligible for an immediate update. , , , , .
The current-plan batch makes the following decision for each subscription:
| Condition at batch processing | Stored result |
|---|---|
hasCustomPrice = true | Skip it, even if a deferral condition below also applies. No pending record is created by this batch for that subscription. |
Otherwise, a change has status PENDING_FOR_NEXT_RENEWAL | Store a pending price snapshot; keep the current recurring prices. |
Otherwise, a Vipps subscription has a local VippsPayment with its agreement ID and status DUE | Store a pending price snapshot. This is a local-record check, not a request to Vipps. |
| Otherwise, an e-invoice subscription has a qualifying next-period invoice | Store a pending price snapshot. |
| None of these conditions applies |
The three deferral conditions are an OR: any one is enough. The e-invoice query joins invoice → order → subscription and requires invoice provider EINVOICE, isCreditNote = false, and periodStartDate >= nextRenewal after both dates are converted to Oslo calendar days. Delivery and payment status are not predicates. The method name's “sent invoice” wording does not prove delivery.
The immediate update changes recurring prices, not currentPrice, subscription status or payment collection. Its total sums matching subscribed product rows using the new plan unit prices and existing quantities. For shipping, this batch treats a zero new recurring price as free; that differs from the ordinary helper's two-price condition. Do not assume identical immediate and deferred totals for that physical-product boundary. , , , .
This source-derived example uses one digital product, quantity 1, with a plan price changing from NOK 199 to NOK 229. It is not a production reproduction.
DUE charge retains NOK 199 and gets a pending NOK 229 snapshot.hasCustomPrice = true is skipped, even if it also has a DUE charge.Linked package prices follow a separate rule during the plan save. A package-product price equal to the previous plan-product price changes to the new price: NOK 199 becomes NOK 229. A package-product price of NOK 99 stays NOK 99. The comparison is numeric equality; an intentionally configured NOK 199 offer is not protected by its purpose or an introductory-offer flag. .
PendingSubscriptionPlanPriceUpdate stores a subscription ID, plan ID, product IDs with base recurring prices, and isProcessed. It stores prices rather than a final subscriber total. A separate path also creates these records for pending switches whose planChange.newPlanId points to the edited plan. Those subscribers may still be on another plan; this incoming-switch path does not apply the batch's hasCustomPrice exclusion. , .
Renewal and completed-change paths schedule deferred processing with a 5-minute start delay. This is scheduling configuration, not a promise that prices change 5 minutes after saving the plan. , , , .
The apply function selects unprocessed records for the subscription's current plan, oldest first. It updates matching, non-deleted subscription-product rows, marks each pending record processed, then recalculates the recurring total if a product row was affected. Records for another plan are not selected; a processed record alone does not prove that a matching product row changed.
For example, two snapshots created at distinct times with NOK 219 then NOK 229 leave the matching product at NOK 229. If the subscription has moved to another plan, neither snapshot applies. The function does not recheck hasCustomPrice or the original deferral conditions. A custom price entered after queueing is therefore not protected by this function. That sequence needs engineering review before making any case-specific correction. .
A failed or successful save is insufficient to reconstruct the whole price update:
subscriptionPlanPropagationFailed maps to HTTP 503 and is thrown inside the plan transaction, causing its database changes to roll back. Workflow start is an external side effect, however. A different failure after an accepted start does not establish that the workflow never ran; the inspected update path has no cancellation to undo it. , , .WorkflowExecutionAlreadyStartedError is swallowed. That does not start a second workflow with the newer snapshot. Saving the unchanged price again is not a retry because it fails the price-change predicate. This is a source-derived boundary, not a reproduced incident. , .RECURRING_PRICE_UPDATED activity capture. A later failure does not undo previously committed batches, and a missing activity entry is not proof that no price write occurred. , .No supported route for retrieving workflow inputs, pending records or retained activity evidence was verified in this documentation run. For an unresolved case, engineering/operations need to establish the deployed revision, tenant and plan, edit times, old/new amounts and effective processing input before deciding on a correction. Keep case data in the authorized case system; this reference does not prescribe a retry or repair.
The admin menu requires the subscription feature and plans-section access; mutation endpoints require UPDATE on PLANS_AND_PRODUCTS. Moving from a plan and appearing as a destination are separate settings: source-plan change permissions allow the move, while the destination must be Published and enabled for the relevant picker. The admin picker excludes the subscription's current plan. , , .
subscriptionCount > 0 locks billing period, renewal and gift controls and disables deletion. The count has no Active-status predicate, so ended subscriptions count despite helper text referring to “active members.” The same count blocks product removal and adding required products. New optional products can be added to linked packages; this path does not add them to existing subscriptions. , , .
Archiving changes plan status without canceling subscriptions. Packages have independent statuses. The UI's subscription-based deletion restriction is separate from the removal service, which rejects any linked package, including drafts, before soft-deleting the plan and its associations. .
Use for the illustrated creation and management procedures. Controls were observed in an owner session on 26 September 2026 and an authorized session with uninspected role on 28 September. Ordinary-role restrictions, price propagation, provider behavior and the failure sequences above were not runtime-tested.