Subscription — Core concepts
Subscription vs Enrollment: template vs runtime
This is the most important distinction:
| Subscription | Enrollment | |
|---|---|---|
| What it is | Template/product: price, schedule, conditions | One actual enrollment of an attendee |
| Nature | Static configuration (admin creates) | Runtime, has a lifecycle/status |
| Example | "Maths Package Term 1" | "An enrolls in Maths Package Term 1" |
⚠️ The Subscription is the template configuration; runtime state is not transferred to it. Enrollment/booking state is only changed through the existing confirmation services — the setup UI does not edit Enrollment/Invoice/Booking directly.
Enrollment status lifecycle
An enrollment moves through: Submitted → Approved/Confirmed. Billing only runs for active, approved enrollments that match the billing schedule + term context.
BillingSchedule: canonical source for period selection
There are two schedule levels — don't confuse them:
| Schedule | Meaning |
|---|---|
| SubscriptionTermBillingSchedule | The "available" schedule of a subscription term (all bookable periods). |
| EnrollmentBillingSchedule | The schedule booked for a specific enrollment (only periods actually booked). |
Manage Add-ons / Manage Discounts only show periods from EnrollmentBillingSchedule (booked) and do not show unbooked periods. Submitting an unbooked period → rejected.
When booking, the UI fetches periods from the API:
GET /api/ConsumerEnrollment/Subscriptions/{id}/BillingSchedulesEach row: a stable scheduleId, subscription/term context, BookingStart, BookingEnd. These two fields take precedence over billing period start/end for display & submit.
SubscriptionTerm: link/archive lifecycle via StatusId
SubscriptionTerm (the record linking subscription ↔ Term) has a StatusId field separate from IsActive. Don't conflate the two:
| State | IsActive | StatusId | Meaning |
|---|---|---|---|
| Approved | true | EntityStatus.Approved (500) | Active linked term, used normally. |
| Archived | true | EntityStatus.Archived (600) | Unlinked but still has booking usage; stays active, not deleted. |
| Soft deleted | false | (any) | Soft delete per the Aimy protocol; not archive, not restorable from UI. |
⚠️
IsActive = falseis always soft delete. Archive does not useIsActive = false(and does not use the "zero-column" encoding via schedules). An archived term staysIsActive = true, onlyStatusId = 600differs.
Visibility rules per surface (active terms only, IsActive = true):
| Surface | Approved (500) | Archived (600) | Inactive (IsActive=false) |
|---|---|---|---|
| Booking UI (new period selection) | ✅ shown | ❌ hidden | ❌ hidden |
| Subscription Manager | ✅ shown | ✅ shown | ❌ hidden |
| Subscription Billing Manager | ✅ shown | ✅ shown | ❌ hidden |
- Billing only includes a schedule when
schedule.IsActive = trueand the parent term hasIsActive = truewithStatusId ∈ {500, 600}, andSubscription.IsActive = true. - Invariants:
INV-SUB-36,INV-SUB-37,INV-SUB-BM-01/02.
Unlink/re-link booked term: archive instead of delete
- Unlink a term with booking usage → archive (
IsActive = true,StatusId = 600); do not touchSubscriptionTermBillingSchedule(kept for Manager & Billing). - Unlink a term without booking usage → soft delete as before (
IsActive = false). - Re-link an archived term (same subscription + term,
IsActive=true,StatusId=600) → restore: setStatusId = 500, keepProductIds+ schedules, skip the product-selection popup, do not create duplicate records. - Link when no active archived row exists → create a new linked term + open the product-selection popup as usual.
- A term with
IsActive = falseis never restored through the UI link flow.
Invoice: financial status lifecycle
The Billing Details page displays per invoice (not EnrollmentInvoiceLog — the internal log is hidden):
| Status | When |
|---|---|
Initialised (0) | After Skip; the background state. |
Generated (75) | Invoice just generated when entering the workflow detail page. |
Approved | After Approve. |
Action ↔ invoice (no effect on EnrollmentInvoiceLog):
Add-on & Discount per period
- Applied per period of an enrollment, not the whole enrollment.
- Represented by the pair (Add-on/Discount, EnrollmentSchedule period).
- Period rows come from the booked EnrollmentSchedule, not generated from the add-on price type.
- Bulk select-all only appears when a group has > 3 periods.
- Save = "desired state": checked = apply, unchecked = remove; duplicate pairs in a request → rejected.
Skip: reset one period
Skip is an invoice-level action: it cancels the running scheduler (if any) + returns the invoice to Initialised (0) — the status resets even when there is no scheduler. It archives the related booking line, then regenerates the invoice or issues a credit note depending on the invoice's prior state — to keep finances consistent.
BookingAndBilling vs BillingOnly
| BookingAndBilling | BillingOnly (TypeId=1) | |
|---|---|---|
| Program section | Shown, validation active | Hidden, does not block save/publish |
| SubscriptionProduct required to publish | Yes | No |
| Link term needs product planned check | Yes | No |
The type is chosen before creation; once the subscription exists it cannot be changed.
Booking requirements from Product
The attendee requirements for subscription booking are resolved from the Product in the subscription's Program, per the term of the selected schedule (SubscriptionTerm.ProductIds).
| Dimension | Evaluation |
|---|---|
| Age | Within the Product's required age range. |
| School Year | Matches the Product's required school year. |
| Level | Compare Attendee.Level (the authoritative source) with the Product's MinLevel/MaxLevel. |
| Height / Weight | Within the Product's required measurement range. |
| Swimming Competency | Equality check: Attendee.SwimmingCompetency == Product value (not a range). |
Rules:
- A dimension with no bounds (Product not configured) or
<Reference>_Enabled = false→ treated as unconfigured, does not block booking. - Multiple concurrently active Products requiring different Swimming values → the dimension is treated as unconfigured.
- The attendee must satisfy all enforced dimensions; toggling off a dimension (Admin Mode, transient) → that dimension is ignored; a customer cannot bypass it.
- Failure → blocks schedule selection in Step 1 and rejects submit in Step 2 (backend revalidates).
Real-world example: An (SwimmingCompetency = "Non Swimmer") books a swimming package that requires Competent. → Block selection/submit when the child lacks the competency. → Schedule selection is blocked (the Swimming Competency dimension) and submit is rejected; staff can toggle it off temporarily.
Next: Architecture & data.