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.
- Edit mode:
Submit & Confirmstops after submit to review the Billing Difference; it only confirms when you clickConfirmin the modal. - Confirm/Terminate/Cancel/Submit all accept a mute notification intent (Mute Notifications); muting only skips emails, does not change lifecycle/validation/billing.
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.
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).
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.