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" |
⚠️ Booking from a subscription uses the Subscription as template configuration, but does not transfer runtime state ownership to the Subscription. 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). |
Key rule: Manage Add-ons / Manage Discounts only show periods from EnrollmentBillingSchedule (booked), and do not show unbooked periods. Submitting an unbooked period is rejected.
When booking, the UI fetches periods from the API:
GET /api/ConsumerEnrollment/Subscriptions/{id}/BillingSchedulesEach row has: a stable scheduleId, subscription/term context, BookingStart, BookingEnd. BookingStart/BookingEnd 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: keep
IsActive = true, setStatusId = 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). Relevant statuses:
| 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). Skip still resets the status even when there is no scheduler. The system archives the related booking line and 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.
Next: Architecture & data.