Products
A product defines digital access or print delivery included in a subscription.
A product defines digital access or print delivery included in a subscription.
Stay in the loop 🚀
Product belongs to a tenant. Its category identifies digital or physical delivery; isOptional separately identifies an add-on. Adding a product to a plan initializes the plan product's price from Product.price. The plan then owns that recurring price, while a package has its own starting price. Later product-price edits do not cascade into those records. Sources: plan selection, product update.
When a subscription is created through a package, mapToMemberPackageProduct writes these values:
| Subscriber-product field | Value copied at creation |
|---|---|
MemberPackageProduct.price | Package product price for the initial period |
MemberPackageProduct.recurringPrice | Matching plan product price, or 0 when that product has no plan row |
MemberPackageProduct.productId | Shared product identity used to read current product attributes |
In this mapper, a truthy manualPrice overrides both prices; 0 uses the package/plan fallback. The separate plan-only mapper starts both from the plan product price, subject to the same manual-price condition. These product-row values are distinct from MemberPackage.currentPrice, the subscription's current-period total. .
mapToOrderItems uses recurringPrice when isRecurring=true, otherwise price. It copies the product's current tax into the new order item. An existing order item retains its copied tax; changing the product does not rebuild it. .
Consider one digital subscriber-product row with quantity 1, initial price NOK 99 and recurring price NOK 199, without shipping or a manual override. The publisher later changes the product base price from NOK 199 to NOK 249 and tax from 0 to 0.25.
A newly constructed recurring order line still uses gross NOK 199. It uses the new tax rate: 199 / 1.25 = 159.20 before tax, leaving NOK 39.80 tax. The product's new NOK 249 base price does not enter this calculation. Subrite rounds the unit tax-exclusive amount to two decimals. This is a source-derived example, not an executed renewal. .
The plan picker requests products from the current tenant with the selected main/optional type and status=published. Draft and archived products are excluded. Request failures produce an error toast, so an empty picker alone does not establish that no matching product exists. , .
The filter also applies to existing relationships when responses are built. SubscriptionPlanResponse omits nonpublished products before deriving its products and price. PackageResponse independently filters its package products and plan products before deriving the initial and recurring totals. Archiving a product can therefore remove it from a connected offer's returned products and change its displayed totals, even though the relationship rows still exist. , .
The product-status update does not cancel subscriptions or rewrite subscriber prices. Nor is published product status a condition in the access lookup described below. Offer visibility and existing entitlement are separate paths.
Creating a product neither creates a subscription nor links a content channel. At subscription-product creation, owner access is initialized as assignSeatToOwner || maxQuantityPerOrder===0. Zero therefore grants owner access in this mapper; it does not mean no access. Changing only assignSeatToOwner later changes the product default without rewriting existing MemberPackageProduct.hasAccess. , .
getActiveProductsForMember first collects Active subscriptions owned by the member and Active subscriptions reached through an accepted, undeleted seat. It then joins their subscriber products. The direct digital path requires hasAccess=true; the seat path does not. The seat path carries subscription IDs into the final product query, so its result is not proof that the member accepted a separate seat on every returned product. This subscription-level breadth is visible in source; its intended behavior for multiple digital products has not been confirmed by Engineering. .
IP access follows a different predicate: digital category, supportsIpAccess=true, a registered range containing the supplied IP, an Active subscription and matching tenant. It does not require owner hasAccess. Enabling the flag alone creates no range. Content-channel lookup then maps the eligible product IDs through tenant-scoped ProductContent links. These checks explain backend eligibility; they do not establish publisher-site cache behavior. , .
Member seats included maps to maxQuantityPerOrder. The update handler locks the tenant's product row and evaluates these conditions inside its product transaction:
| Requested change | Source behavior |
|---|---|
| Lower the nonnegative limit while any subscriber-product row exists | Rejects with field error memberSeatsCannotBeDecreased on maxQuantityPerOrder. The check does not require an Active subscription. |
| Increase from exactly 1 to more than 1, with existing subscriber-product rows | Starts owner-access propagation. Increases from 2 to 3 or 0 to 2 do not meet this branch. |
| Turn IP support off while linked IP ranges remain | Rejects with cannotDisableIpAccessWhileIpRangesExist; it does not delete the ranges. |
The propagation worker changes this product's hasAccess=false subscriber-product rows to true in batches of 1,000, without an Active-subscription filter. A successful product save does not prove that this worker has finished. A workflow-start failure produces productOwnerAccessPropagationFailed with HTTP 503 and rolls back the product transaction. , , .
That rollback conclusion applies to this specific failure. Tags are processed after the product transaction commits; a later tag error can leave the product change saved. A generic failed response is therefore insufficient evidence of rollback. No supported route for retrieving propagation status or historical failures was verified for this article; this is a behavior reference, not a retry procedure.
productNameAlreadyExists belongs to the name field. Names are compared after trimming and lowercasing, within the same tenant and main/optional type. Changing capitalization or surrounding spaces does not resolve a collision. An update that keeps the normalized name and type unchanged skips this collision check, allowing other fields on legacy duplicates to be edited. , .
Deletion checks plan references first (productUsedInSubscriptionPlans), then package references (productUsedInPackages), then content links (productUsedInContentChannels). The first error does not rule out later blockers. With none present, the transaction soft-removes the product and its tags. The admin has no undo control; its permanent-deletion warning should not be taken as a description of storage or historical-record removal. .
Source checked 28 September 2026 at 932b0744713157826f0441fe1909d96dfede9f9a; deployed revision was not established. Calculations and state branches above were traced from source, without executing payments, entitlement changes or deletion. Stock enforcement, external caching and propagation completion remain unverified. For the illustrated controls, see the .