# Products

**Summary:** A product defines digital access or print delivery included in a subscription.

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

<a id="follow-the-value-that-produced-the-order"></a>
## Follow the value that produced the order

`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](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/frontend/src/components/admin/SubscriptionPlans/components/ProductSelection/index.tsx#L87-L101), [product update](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/product.service.ts#L97-L219).

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. [Source: subscription product mapping](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/common/utils/order-utility.ts#L173-L225).

`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. [Source: order construction](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/common/utils/order-utility.ts#L130-L149).

<a id="worked-price-and-tax-change"></a>
### Worked price and tax change

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. [Source: unit tax calculation](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/payment/entities/order-item.entity.ts#L45-L46).

<a id="publication-status-changes-returned-offers-not-subscriptions"></a>
## Publication status changes returned offers, not subscriptions

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. [Sources: picker request](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/frontend/src/components/admin/SubscriptionPlans/components/ProductSelection/index.tsx#L60-L123), [tenant filtering](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/product.service.ts#L262-L273).

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. [Sources: plan response](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/dto/subscription-plan-response.dto.ts#L23-L61), [package response](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/dto/package-response.dto.ts#L46-L96).

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.

<a id="digital-access-follows-the-subscription-path"></a>
## Digital access follows the subscription path

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`. [Sources: initial access](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/common/utils/order-utility.ts#L181-L222), [product writes](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/product.service.ts#L97-L219).

`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. [Source: member access query](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/repositories/product.repository.ts#L29-L159).

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. [Sources: IP lookup](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/repositories/product.repository.ts#L164-L190), [channel resolution](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/content/services/content-channel.service.ts#L83-L111).

<a id="seat-edits-and-the-save-boundary"></a>
## Seat edits and the save boundary

**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. [Sources: update conditions and rollback](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/product.service.ts#L97-L219), [error response](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/controllers/product.controller.ts#L101-L117), [worker writes](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/subscription-plan.service.ts#L843-L867).

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.

<a id="name-and-deletion-errors"></a>
## Name and deletion errors

`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. [Sources: name query](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/repositories/product.repository.ts#L193-L231), [update exception](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/product.service.ts#L304-L335).

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: deletion](https://github.com/subrite/subrite-crm/blob/932b0744713157826f0441fe1909d96dfede9f9a/api-server/src/modules/product/services/product.service.ts#L222-L259).

<a id="verification-limits"></a>
## Verification limits

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 [Administrators Products guide](https://www.subrite.no/administrators/products?md=true).

## Related pages

- [Products](https://www.subrite.no/administrators/products?md=true): A product defines digital access or print delivery included in a subscription.
