Subscription — Codebase flow
Subscription spans both frontend (Tuke/Topi) and backend (Tux). This page lists the main paths; API/spec names are anchors for lookup.
Booking flow (customer submit)
Schedule-clash guard (overlap validation)
On submit/edit/confirm, the system compares the submitted schedule rows with:
- Enrollment in
Submitted/Confirmed(compareEnrollmentPattern). - Normal TermBookingOrder in
Submitted/Quote/Submitted_Change. - Normal TermBooking in
Approved/Attendance_Approved.
Any clash → the whole request fails (atomic, no partial state). Edit excludes the current EnrollmentId so it does not self-conflict. Direct-confirm & future-confirm use the same guard.
Direct-confirm & Submit & Confirm (admin)
- Only admin/staff may use direct-confirm; customer/non-admin → rejected.
- Uses the Subscription as template, but runtime mutation happens only through existing confirmation services (UI setup does not directly mutate Enrollment/Invoice/Booking).
- The response does not say "confirmed success" unless runtime confirmation semantics are complete.
- Submit & Confirm: submits with a direct-confirm intent; on success the enrollment is confirmed immediately, no need to go through Booking Manager. Normal submit keeps pending/submitted.
- Subscription Edit mode uses the existing direct-confirm intent; it does not call
PreviewBillingDifference. Billing Difference applies to eligible Admin Edit normal bookings. - Confirm/Terminate/Cancel/Submit all accept a mute notification intent (Mute Notifications); muting only skips emails, does not change lifecycle/validation/billing.
Subscription Enrichment: preview → submit → confirmed Booking
POST /api/ConsumerEnrollment/EnrichmentPricingPreviewis read-only; if the preview is stale, fails, or cannot resolve configuration, Step 2 must not continue.- Submit does not accept count, rate, amount, invoice id, or credit-note id as authority; the server resolves
TermProductagain before mutation. - Confirmed generation keeps the normal Subscription zero-priced line and creates an Enrichment line with
RateStandardItemId,Price,Quantity = 1,Amount,TotalAmount, plus the corresponding attendance. - Closure day, inclusive
BookingStart/BookingEnd, Mon–Fri, deduplication, and overlap must match between preview, submit, and generation.
Real-world example: The user changes the selection from Mon–Fri to Mon–Wed while the old preview is returning. → The client ignores the old response by fingerprint. → The toolbar shows only the current Mon–Wed amount.
Billing Details: Schedule / Approve / Skip
| Action | Backend does |
|---|---|
| Schedule | Creates one InvoiceScheduler / invoice; ScheduledOn = Invoice.Date (UTC). |
| Approve | Updates invoice → Approved; creates InvoiceScheduler with ScheduledOn = DateTime.UtcNow. |
| Skip | Deactivates active scheduler (if any); updates invoice → Initialised (0). |
The page derives button intent from invoice status, not from
EnrollmentInvoiceLog.
- Billing completeness counts only active
EnrollmentBillingSchedulerows belonging to the exact schedule being viewed; it does not count everyEnrollmentPeriodfor the same Subscription/Term.
Subscription Billing Manager
- Filter Site + Subscription (required) + Term (optional); option source:
GET /Subscription/WithTerms(not/Subscription/List, does not readPeriods). Click Apply to load the table. - Each row = one
SubscriptionTermBillingSchedule(including archived termsStatusId=600): Term, Period, Invoice Date, Due Date, Stats (generated/expectedper active-approved enrollment), Action. - View → redirects to Billing Details with site/subscription/term context. Non-SuperAdmin (
BusinessUnit.TypeId ≠ 99): manager read-only, no approve/skip/generate. - SuperAdmin (
TypeId = 99): rows have Calculate → drawerGET /api/subscription-billing/ScheduleSummary/{id}(+POSTwhen no summary exists yet), then Generate Subscription Booking (POST /api/ConsumerEnrollment/BillingEnrollment/{scheduleId}); closing the drawer → refresh grid. - Invoice generation requires the schedule to have
SubscriptionPriceId+ an activeSubscriptionPriceLine(includingUnitPrice = 0.00); missing → validation fails before persisting, no fallback to the schedule amount.
Manage Add-ons / Discounts
Only mutate the add-on selection belonging to the enrollment; billing side effects go through the backend service (the popup does not directly approve/skip/schedule/regenerate invoice).
Manage Enrichments for Subscription Booking
- Subscription Manager adds a separate Manage Enrichments action next to Manage Add-ons.
- Read returns the product, occurrence date, selected state, amount, and lock reason; reading does not mutate state.
- Write accepts only the Booking +
TermProductId + Date; the server checks BU/site/Subscription/attendee/Term, eligibility, invoice/credit/attendance/Booking lock, and saves atomically. - Only future occurrences that are not invoiced/credited/attended and are not locked can be added/removed. An invoiced change requires the Booking/Finance adjustment flow; Manage Enrichments does not write Invoice, CreditNote, or logs itself.
- Save retry must be idempotent; multiple requests for the same semantic key leave at most one active line.
Real-world example: Staff sends one valid date and one date outside the Term. → The API rejects the whole request. → It does not partially add the valid date or change the invoice/credit log.
Setup: conventions to keep when coding
| Convention | Note |
|---|---|
| Archive only when unpublished | Published package → server rejects archive. |
CutOffMinute stores minutes, UI days | Decimal rejected; 0/null = no cut-off. |
Billing Type locked to Flat Rate | Dynamic request rejected/normalized. |
BillingOnly editable before create | After it exists → read-only. |
| New Add Price = blank | Do not prefill from viewed/saved price. |
| Rollover alert muted | Notification-only: does not change booking/enrollment/billing/term planner state. |
Booking lifecycle notifications
- Submitted (still pending) →
New Subscription Booking (Client)for the customer +(Staff)for admin/staff. - Confirmed →
Subscription Booking Confirmed (Client); Cancelled →(Client)with site contact phone/email. - Submit & Confirm → does not queue submitted/pending; only confirmed.
- Mute intent (admin action) → no email queued/sent that would otherwise be; lifecycle still persists.
- Notifications are created after the lifecycle has persisted; rendering is read-only, does not mutate state.
- Placeholders resolve from context:
,,,,,, etc.
Frontend (Subscription Manager)
- Subscription filter:
GET /api/Subscription(admin). Site filter limits options by site. - Changing a filter does not auto-reload; click Apply to load the table per the current filter. Initial page load runs automatically; a successful mutation (confirm/cancel/delete/ terminate/rollover) also refreshes. A stale pending request response does not overwrite newer data (prevents races on fast switching).
- Expanded row: shows add-ons then discounts; Update button opens Manage Add-ons.
Where to find code
Frontend is in Tuke/ (Next.js) and partly Topi/ (old admin). Backend + API are in Tux/ (ConsumerEnrollment, Subscription, Invoice services).
Next: Common cases.
Current implementation flows
- Admin Manager operations use
api/SubscriptionManager/*with BU scope; customer Extend remains on the consumer API and is submit-only. - Extend validates same Term, adjacency and non-past schedule before creating a pending period. Rollover validates target Term/active-job safety; Duplicate copies configuration and never copies runtime finance data.
- Published write guards reject locked fields as one request; discount remains intentionally editable.
- Flyer persistence preserves omitted data, clears with an empty list and replaces with a non-empty list; PDF pages are materialized into ordered image blobs.
- Auto billing creates/cancels/reschedules the existing scheduler record according to Subscription publish/mode state.
Real-world example: A request mixes a locked price update and editable description. → The API rejects the entire request, so the client cannot accidentally persist only the description.