Subscription — Common cases
How to read each case: start with the symptom/question, then read the expected behavior, how to verify it in the UI/API/data/code, the source anchor, and the Real-world example. These cases prioritize running behavior; open the linked spec/code when you need the original contract.
"Customer booking reports a schedule clash"
The system intentionally blocks double-booking. On submit, it compares the schedule against the attendee's existing enrollments/bookings; on a clash → the whole request fails, no partial state is created.
- Editing one enrollment: the current EnrollmentId is excluded from the check (no self-conflict).
- Direct-confirm / future-confirm: uses the same guard → also blocked on a clash.
Spec:
subscription-booking-double-booking-guard+ the requirement "validates existing enrollment schedules before runtime state creation". For normal booking, see Booking → Common cases.
Real-world example: A child already has an After School enrollment on Wednesday; the parent books another package that clashes on the same Wednesday afternoon. → Avoid recording the child in two places at the same time. → The whole request fails at submit, and no partial enrollment is created.
"Manage Add-ons doesn't show the period I want"
The popup only shows periods from EnrollmentBillingSchedule (booked), and does not show unbooked periods of the subscription term. Submitting an unbooked period → rejected.
- Bulk "select all" only appears when a group has > 3 periods.
- Period rows come from the enrollment schedule, not the add-on price type.
Real-world example: Admin wants to add a meal add-on for Term 3, but the child has not booked Term 3. → Only attach add-ons to periods that were actually booked. → Term 3 does not appear in the popup; forced submit is rejected.
"What happens to the invoice when I Skip a period?"
Skip returns the invoice to Initialised (0) and cancels the active scheduler (if any). Depending on the invoice's prior state: the system regenerates the invoice or issues a credit note to keep finances consistent, and archives the related booking line.
Real-world example: A family goes on holiday and asks to skip a July period that already has an invoice. → Do not charge for a period the child will not use. → Skip → invoice
Initialised, credit note for the issued part, and the July booking line is archived.
"Create a BillingOnly package without a product"
BillingOnly (TypeId=1):
- Hides the Program section; does not block save/publish.
- Can be published without a
SubscriptionProduct. - Link term does not need a product planned check.
Whereas BookingAndBilling still requires a product to publish + a planned check when linking a term.
Note: the type is chosen before creation; once the subscription exists it is read-only.
Real-world example: A center charges a recurring facility fee that is not tied to any class session. → Create a billing-only package without configuring product. → Create a
BillingOnlypackage and publish it immediately withoutSubscriptionProduct.
"Can't archive a package"
Only unpublished packages (Unbookable) can be archived. A published package → the Current tab does not allow archive, and a direct request is rejected by the server. Archive/restore does not change publish status.
Real-world example: Admin wants to clean up "Term 1 2025" from the list while it is still bookable. → Avoid hiding a package customers can still book. → Unpublish first; if the package is still bookable, archive is hidden and the server rejects direct requests.
"Unlinking a booked term still shows in Billing but disappears from Booking"
This is by design. Unlinking a term that has booking usage → the system archives it (keeps IsActive = true, sets StatusId = 600) and does not delete the schedules.
- Booking UI: the archived term is no longer offered for new period selection.
- Subscription Manager & Subscription Billing Manager: still show the archived term + schedules to handle in-flight booking/billing.
- If you unlink a term without booking → normal soft delete (
IsActive = false), it disappears from all three surfaces.
A term with
IsActive = falseis soft deleted, not archived — it cannot be restored from the UI.
Real-world example: Admin removes Term 2 from a package, but Term 2 already has 30 active bookings. → Keep active booking/billing intact while stopping new sales. → The term is archived: hidden in Booking UI, still visible in Subscription/Billing Manager.
"Re-linking an old term asks to choose a product / creates duplicates"
Re-linking exactly the archived term (same subscription + term) will restore, not create new:
- Sets
StatusIdfrom600→500; skips the product-selection popup. - Keeps the existing
ProductIdsandSubscriptionTermBillingSchedule— no product loss. - Does not create duplicate
SubscriptionTerm/schedule records; reloading each surface shows one active set. - If there is no active archived row → it opens the product-selection popup and creates a new linked term.
Real-world example: Admin accidentally unlinks Term 2 (archived) and wants to link it again. → Restore the exact old product + schedule instead of creating duplicates. → Re-link changes
StatusId600 → 500, skips the product popup, and keepsProductIds.
"Booking Cut-Off rejects a fractional number"
The UI accepts whole days and stores minutes (CutOffMinute, 1 day = 1440). Entering 1.5 → rejected (no auto-rounding). 0/null = no cut-off → booking is not blocked.
Real-world example: Admin wants registration to close 1.5 days before and enters
1.5. → Cut-off uses whole days to avoid ambiguous minute thresholds. → The system rejects1.5; admin enters1or2(CutOffMinute= 1440/2880).
"Changing the Subscription Manager filter doesn't show new data"
By design: changing a filter does not auto-reload the table — click Apply to load per the current filter. Initial page load runs automatically; a successful mutation (confirm/cancel/ terminate/…) also refreshes. A stale pending request response does not override new data; if data jumps → check the "latest active request wins" logic.
Real-world example: Admin changes the Term to view another period, but the table still shows the old period. → Avoid expensive loads while merely adjusting the filter. → After changing, click
Apply; the table then reflects the selected Term.
"The booking period the customer picks doesn't match"
The UI must fetch periods from GET .../Subscriptions/{id}/BillingSchedules (canonical), and not compute them client-side. Each row has BookingStart/BookingEnd that take precedence over billing period start/end. An empty endpoint response → an "unavailable" state + blocks submit; on error → a retryable error state that blocks submit with the old selection.
Real-world example: A package bills monthly, but booking starts mid-month. → Let the customer choose the correct bookable range. → UI reads
BookingStart/BookingEndfromBillingSchedules; empty response shows "unavailable" and blocks submit.
"Step 2 does not show Enrichment"
Check in order: whether the Product is active/approved, ProductType is Enrichment, the SubscriptionProduct has the correct Program/BU, the TermProduct has the correct Term/Org, and both RateStandard and RateStandardItemId are valid. Missing one condition makes the option ineligible; do not fix it by sending an amount from the client. If the preview API/feature flag is unavailable, the UI fails closed.
Real-world example: Morning Care is still active but has no accounting item. → Do not expose a choice that could calculate money incorrectly. → Step 2 hides the option and Setup/API returns a configuration error.
"Enrichment amount does not match the selected days"
Occurrences are calculated from the selected schedule, inclusive BookingStart/BookingEnd, Mon–Fri, and the same closure-day rule as real generation. The server preview is the source of truth; zero-rate still counts, and an old/stale response must not overwrite the new result.
Real-world example: Select Morning Care Mon–Fri in a week with five open days at a
$5rate. → Preview reports5 occurrences / $25; adding a weekend is invalid. → The toolbar adds$25to the total instead of multiplying independently by the number of checkboxes.
"The Subscription invoice contains an Enrichment charge"
This is the wrong boundary. Enrichment belongs to TermBookingLine and Booking Billing/Reconciliation; Subscription Billing uses only the schedule amount plus Add-on/Discount from EnrollmentBillingSchedule. Check EnrollmentInvoiceLog/InvoiceLine for the exact schedule; do not add TermBookingLine to the recurring invoice.
Real-world example: The Booking has five Enrichment occurrences at
$25, while the Subscription schedule is$5. → Booking reconciliation processes$25; the Subscription invoice remains$5. → If it is$30, stop issuing it and check the projection/service boundary.
"Manage Enrichments will not remove an occurrence"
Historical, invoiced, credited, attended, or Booking-locked occurrences are immutable. A request containing a duplicate, out-of-scope, out-of-Term, or ineligible occurrence must fail atomically; it must not mutate the other valid dates. A change after invoicing must go through the Booking/Finance adjustment flow.
Real-world example: Staff wants to remove an invoiced day and add next week. → Manage Enrichments returns lock/adjustment-required. → It does not edit the invoice directly or silently deactivate only part of the selection.
"Rollover alert disappears after linking a term"
The Rollover capability is now implemented as an explicit runtime action. Linking a Term still completes and refreshes the linked term; use Rollover only when the target is active, linked and approved. It reuses existing runtime and does not create a new Subscription or finance record.
Real-world example: An admin links the next approved Term and needs to move existing runtime. → Run Rollover once; a duplicate active job or target period is rejected.
"Customer's permission to choose start/end"
Depends on the package settings (Booking Start Selection, Booking End Selection):
| Setting | Customer can choose? |
|---|---|
Staff Only (default, 0) | No |
Staff & Customer | Yes |
Staff can always choose both start and end.
Real-world example: A center wants customers to choose their own package start date. → Allow parents to pick the start instead of staff only. → Set
Booking Start Selection=Staff & Customer; customers can see and choose the start date.
"Published package rejects an edit"
Published setup locks price, billing settings, linked Term, billing schedule and add-on/bus-stop configuration. The request is rejected as a whole if it includes a locked field; unpublish first. Discount and display metadata remain separate according to their current rules.
Real-world example: An admin changes price and description together. → Neither partial change is committed. → Unpublish, update the locked field, then publish again.
"Need another period for an existing enrollment"
Use Extend for an adjacent future schedule in the same Term. Admin may Submit or Submit & Confirm; customer is submit-only and constrained by settings. Original booking/invoices stay unchanged and past schedules are rejected.
Real-world example: Add next week's period. → Create one Pending period, leaving prior invoice history intact.
"Duplicate or Rollover?"
Use Duplicate for a new setup template without runtime/finance records. Use Rollover to move existing runtime to an active linked approved target Term without creating a new Subscription or finance row.
Real-world example: Need the same enrollment in next Term. → Rollover, not Duplicate.
"Flyer disappears after Save"
Omitted Flyer data preserves the existing file; an empty list clears it; a non-empty list replaces it. Validate the PDF/page/image size limits and inspect the ordered manifest when Step 1 cannot render it.
Real-world example: A settings request omits Flyer while changing description. → Keep the current Flyer; do not treat omission as a delete.