# Subscription plans

**Summary:** A subscription plan defines which products a subscription includes, what they cost each billing period and whether it renews automatically.

- Role: [Technical Support](https://www.subrite.no/technical-support)
- Source: https://www.subrite.no/technical-support/subscription-plans
- Updated: 2026-09-28
- Markdown index: https://www.subrite.no/technical-support/llms.txt

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](https://github.com/subrite/subrite-crm/tree/932b0744713157826f0441fe1909d96dfede9f9a), inspected on 28 September 2026. It does not establish the deployed revision or the outcome of a particular price change.

<a id="which-record-owns-the-price"></a>
## Which record owns the price?

| 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 [recurring-payment renewal](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/transactions/recurrent-payment.transaction.ts#L148-L158) 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. [Product-price copy](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/frontend/src/components/admin/SubscriptionPlans/components/ProductSelection/index.tsx#L87-L101), [plan-product ownership](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/entities/subscription-plan-product.entity.ts#L8-L32), [price mapping and calculation](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/common/utils/order-utility.ts#L173-L273), [purchase writes](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/transactions/purchase-package.transaction.ts#L155-L202).

<a id="what-saving-a-price-change-starts"></a>
## What saving a price change starts

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. [Save and trigger](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L130-L237), [price comparison](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L482-L552), [batch setup](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/temporal/temporal-worker.bootstrap.ts#L2479-L2496), [batch size](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/common/constants/batch-size.constant.ts#L2), [concurrency](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/temporal/workflows/propagate-subscription-plan-update.workflow.ts#L18-L38).

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 | Update the existing subscription-product recurring prices and subscription recurring total. |

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. [Batch decisions and writes](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L561-L840), [provider selection](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L1052-L1083), [invoice predicate](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/repositories/invoice.repository.ts#L248-L271), [Vipps predicate](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/repositories/vipps-payment.repository.ts#L43-L54).

<a id="follow-one-price-change"></a>
## Follow one price change

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.

- A non-custom subscription with no pending change or qualifying billing record receives recurring product price and total NOK 229 when its batch applies the update.
- A non-custom Vipps subscription with a locally recorded `DUE` charge retains NOK 199 and gets a pending NOK 229 snapshot.
- A current-plan subscription with `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. [Package-price inheritance](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L987-L1049).

<a id="when-deferred-prices-apply"></a>
## When deferred prices apply

`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. [Pending record](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/entities/pending-subscription-plan-price-update.entity.ts#L6-L32), [incoming switches](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L523-L552).

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. [Renewal caller](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/transactions/recurrent-payment.transaction.ts#L85-L103), [e-invoice renewal caller](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/transactions/renew-e-invoice-subscription.transaction.ts#L127-L140), [completed-change caller](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/member-package-change.service.ts#L177-L187), [enqueue](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/member-package-change.service.ts#L526-L550).

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. [Deferred application](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/member-package-change.service.ts#L560-L642).

<a id="failure-and-repeated-save-boundaries"></a>
## Failure and repeated-save boundaries

A failed or successful save is insufficient to reconstruct the whole price update:

- **Workflow start failure:** `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. [Start and surrounding transaction](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L194-L237), [start error](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L496-L552), [HTTP mapping](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/controllers/subscription-plan.controller.ts#L148-L158).
- **Two edits in one minute:** the workflow ID contains plan ID, tenant ID and the Oslo minute. While that workflow remains running, another edit with the same ID can save after `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. [ID and start](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/temporal/services/temporal-client.service.ts#L863-L875), [duplicate handling](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/temporal/services/temporal-client.service.ts#L176-L209).
- **Partial processing:** each batch activity has a maximum of 3 attempts, including its initial attempt. Direct subscription/product writes commit before `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. [Batch retry policy](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/temporal/workflows/propagate-subscription-plan-update-batch.workflow.ts#L7-L21), [commit before activity capture](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L748-L840).

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.

<a id="why-a-plan-is-unavailable-or-locked"></a>
## Why a plan is unavailable or locked

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. [Permissions](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/controllers/subscription-plan.controller.ts#L131-L173), [admin picker](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/frontend/src/components/admin/Members/components/SubscriptionSwitch/index.tsx#L362-L404), [change and availability controls](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/frontend/src/components/admin/SubscriptionPlans/components/PlanPermissionsSettings/index.tsx#L39-L203).

`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. [Count](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L870-L884), [product restrictions](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L442-L479), [optional-product propagation](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L239-L273).

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. [Removal checks](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L291-L330).

Use [Subscription plans in Administrators](https://www.subrite.no/administrators/subscription-plans?md=true) 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.

## Related pages

- [Subscription plans](https://www.subrite.no/administrators/subscription-plans?md=true): A subscription plan defines which products a subscription includes, what they cost each billing period and whether it renews automatically.
