Product packages
A product package defines the offer a customer buys. Its starting terms are separate from the plan's recurring terms.
A product package defines the offer a customer buys. Its starting terms are separate from the plan's recurring terms.
Stay in the loop 🚀
A purchased subscription stores its own prices. A difference between the offer shown today and an existing subscription is therefore not enough to establish a pricing error.
The Administrator guide covers the screen procedures.
PackageProduct.price stores a starting price for a packageId/productId pair. SubscriptionPlanProduct.price stores the corresponding plan price. PackageResponse derives totals from those relationships; its per-product recurringPrice is the plan price matched by product ID, or zero if that product is absent from the plan. Changing a product's base price is not the same operation as changing either relationship price. Package product model, response mapping.
There are three different results to distinguish:
| Result | How it is obtained |
|---|---|
| Base offer totals | Starting prices summed from package products; recurring prices summed separately from plan products. |
| Totals for selected products | Base nonoptional products plus selected quantities and applicable shipping. |
| Stored subscription values | MemberPackage.currentPrice, MemberPackage.recurringPrice and its product rows, initialized during purchase processing and changed by later subscription operations. |
Both base sums exclude unpublished products. They include nonoptional products and optional products marked properties.isRequired on the respective package or plan relationship. These sums do not add shipping. The returned products[] comes from the published package products. .
When getPackageWithOptionalProducts receives a nonempty selection array, it replaces those totals:
sentProductIds from submitted rows with truthy (nonzero) quantities. Rows with quantity zero are excluded from this set.(starting price + shipping) × quantity to the starting total and (recurring price + shipping) × quantity to the recurring total.An optional product is included in this calculation only when submitted, even if marked required in the base response. An empty selection array leaves the base totals unchanged. The calculator skips unmatched product IDs; that does not establish that every checkout route accepts such a request. .
Within this calculator, submitting a nonoptional product at quantity zero does not remove its base contribution: its ID is outside sentProductIds, so step 2 adds it. This describes the calculator's fallback, not a valid API input; the quantity DTO requires an integer of at least 1. .
Shipping uses the selected country's rate for a physical product. A missing rate or a nonphysical product contributes zero. The free-product exemption applies only when both starting and recurring product prices are zero. A zero starting price alone does not waive shipping. .
Assume these fictional products are published and all amounts are NOK:
| Product | Starting price | Recurring price | Selection |
|---|---|---|---|
| Digital access, nonoptional | 99 | 199 | Included in the base. |
| Print, optional and required on both relationships | 40 | 60 | Quantity 2, with shipping 10 per copy. |
The base response is NOK 139 starting and NOK 259 recurring. With the print selection supplied, the calculation becomes:
99 + (40 + 10) × 2 = 199.199 + (60 + 10) × 2 = 339.These are source-derived calculations, not observed charges. A package-only physical product would have zero recurring product price, but could still add recurring shipping if its starting price is nonzero. The calculation shown here does not apply an explicit rounding step. .
In the initial-payment-success path, PurchasePackageTransaction reloads the payment with its package, plan and product relationships, and obtains selections from CheckoutState. It creates a MemberPackage with:
packageId and selected subscriptionPlanId.currentPrice initialized from Payment.amount. This is the current-period total, not a permanent first-payment snapshot: RecurrentPaymentTransaction later rewrites it from MemberPackage.recurringPrice. .autoRenew from the plan and the initial nextRenewal from the package's period.recurringPrice from these rows, including shipping.Gift subscriptions start WAITING_FOR_ACTIVATION with nextRenewal: null; the ordinary branch starts ACTIVE. The order also records recurringPriceAtOrder. These stored values explain why today's package response is insufficient evidence for a historical charge. , .
The transaction reads package and plan relationships at processing time. This trace does not establish that terms are frozen when checkout first opens. Changes during an already-started checkout need provider-specific validation.
A recurrent event, or a provider update returning no payment to process, exits before these purchase writes. Database failures roll back the transaction's database writes; the package-purchased and checkout-completed events are emitted after commit. This does not mean a database rollback reverses provider money movement or proves downstream delivery. , .
The package requires a duration or introPeriodFixedEndDate. A duration calculates the end from the starting date; a fixed date gives purchasers the same configured end. The service rejects a fixed date combined with duration fields, or a missing valid alternative. A custom duration needs both unit and frequency. Later recurring periods come from the plan. , .
Direct-link lookup applies these guards in order:
| Guard | Failure | What the result establishes |
|---|---|---|
| A published package matches the supplied tenant and slug. | packageNotFound, HTTP 404 | No matching published offer was returned. An unpublished package can produce the same result as an incorrect tenant or slug. |
isAvailableViaSlug is enabled. | notAvailableViaSlug, HTTP 404 | The package passed lookup but direct-link sales are disabled. |
| The fixed introduction end date is not before today. | campaignIsOver, HTTP 400 | The earlier guards passed, but the fixed date has expired. |
These are API results; checkout renders its own error screens. .
The last guard compares day boundaries in Europe/Oslo, using the same introPeriodFixedEndDate that controls the starting period. Equality passes: a fixed end of 30 September permits this date check throughout 30 September and fails from 1 October. If the direct-link flag is off on 1 October, notAvailableViaSlug wins because it is checked first. A draft package fails the first guard instead. , , , .
Passing these guards proves neither correct publisher routing nor payment completion. My page and manual admin orders have separate availability flags. The direct-link date guard must not be assumed to apply identically to those routes. .
The full address is constructed from the admin request's protocol and host, followed by /checkout/ and the slug. The observed admin generated an admin.subrite.no address; correct publisher routing remains unverified. The copy control inside Slug copies only that field; the control beside the full address copies the complete URL. The Administrator guide therefore requires opening the copied address privately and checking the publisher and offer before sharing. , , .
Selecting Draft, Publish or Archive submits all changed tabs immediately, subject to validation. The update transaction writes package configuration and package-product links and invalidates the package cache. It does not rewrite existing MemberPackage records, migrate their plan, charge or refund them, or send subscriber messages. A separate plan or subscription change can have different consequences. , .
Archiving removes the offer from published-package lookup and default published listings. It does not cancel existing subscriptions or refund payments. No guarantee about a checkout already in progress follows from these publication checks.
Deletion first rejects linked subscriptions in ACTIVE or WAITING_FOR_ACTIVATION with packageIsActiveAsSubscriptions. It then checks the preference references onNewUserCompletedPackageId and buyNewSubscriptionPackageId, returning packageIsReferencedInSettings when either is present. Success soft-removes the package and its product links. This handler has no landing-page dependency check, despite the admin help text; no restore control was verified. .
Source inspected on 28 September 2026 at 932b0744713157826f0441fe1909d96dfede9f9a; deployed revision not established. UI observations date from 26 September and used an owner session. The example package stayed draft.
This is a behavior reference. An authorized route for retrieving historical selections and purchase records, including access and retention, has not been validated here. The named records explain what evidence would distinguish offer calculation from stored purchase values; they are not an instruction to query production. Publisher routing, ordinary-role visibility, live error screens and provider behavior during an in-progress checkout remain untested. No payment, repair or entitlement-delivery test was performed.