Subscription — Khái niệm cốt lõi
Subscription vs Enrollment: template vs runtime
Đây là phân biệt quan trọng nhất:
| Subscription | Enrollment | |
|---|---|---|
| Là gì | Template/sản phẩm: giá, lịch, điều kiện | Một lần ghi danh thực tế của attendee |
| Tính chất | Cấu hình tĩnh (admin tạo) | Runtime, có vòng đời/trạng thái |
| Ví dụ | "Gói Toán học kỳ 1" | "Bé An ghi danh gói Toán học kỳ 1" |
⚠️ Subscription là template configuration; state runtime không chuyển sang nó. State enrollment/booking chỉ sửa qua các service confirmation hiện có — UI setup không trực tiếp sửa Enrollment/Invoice/Booking.
Vòng trạng thái Enrollment
Enrollment đi qua: Submitted → Approved/Confirmed. Billing chỉ chạy cho enrollment active, approved, khớp billing schedule + term context.
BillingSchedule: nguồn canonical cho lựa chọn kỳ
Có hai mức lịch, đừng nhầm:
| Lịch | Nghĩa |
|---|---|
| SubscriptionTermBillingSchedule | Lịch "khả dụng" của subscription term (tất cả kỳ có thể book). |
| EnrollmentBillingSchedule | Lịch đã book cho một enrollment cụ thể (chỉ các kỳ thực sự đặt). |
Manage Add-ons / Manage Discounts chỉ hiện các kỳ thuộc EnrollmentBillingSchedule (đã book), không hiện kỳ chưa book. Submit kỳ chưa book → bị từ chối.
Khi booking, UI lấy period từ API:
GET /api/ConsumerEnrollment/Subscriptions/{id}/BillingSchedulesMỗi row: scheduleId ổn định, context subscription/term, BookingStart, BookingEnd. BookingStart/BookingEnd ưu tiên hơn billing period start/end khi hiển thị & submit.
SubscriptionTerm: vòng đời link/archive theo StatusId
SubscriptionTerm (bản ghi link subscription ↔ Term) có field StatusId tách biệt với IsActive. Đừng nhầm hai khái niệm:
| Trạng thái | IsActive | StatusId | Ý nghĩa |
|---|---|---|---|
| Approved | true | EntityStatus.Approved (500) | Linked term active, dùng bình thường. |
| Archived | true | EntityStatus.Archived (600) | Đã unlink nhưng còn booking usage; giữ active, không xóa. |
| Soft deleted | false | (bất kỳ) | Xóa mềm theo protocol Aimy; không phải archive, không restore từ UI. |
⚠️
IsActive = falseluôn là soft delete. Archive không dùngIsActive = false(và không dùng kiểu "zero-column" encode qua schedule). Archived term vẫnIsActive = true, chỉ khácStatusId = 600.
Quy tắc hiển thị theo từng surface (chỉ tính term IsActive = true):
| Surface | Approved (500) | Archived (600) | Inactive (IsActive=false) |
|---|---|---|---|
| Booking UI (chọn kỳ mới) | ✅ hiện | ❌ ẩn | ❌ ẩn |
| Subscription Manager | ✅ hiện | ✅ hiện | ❌ ẩn |
| Subscription Billing Manager | ✅ hiện | ✅ hiện | ❌ ẩn |
- Billing chỉ tính schedule khi
schedule.IsActive = truevà parent termIsActive = truevớiStatusId ∈ {500, 600}, vàSubscription.IsActive = true. - Bất biến:
INV-SUB-36,INV-SUB-37,INV-SUB-BM-01/02.
Unlink/re-link booked term: archive thay vì xóa
- Unlink term có booking usage → archive (
IsActive = true,StatusId = 600); không đụngSubscriptionTermBillingSchedule(giữ cho Manager & Billing). - Unlink term không có booking usage → soft delete như cũ (
IsActive = false). - Re-link term đã archived (cùng subscription + term,
IsActive=true,StatusId=600) → restore: setStatusId = 500, giữ nguyênProductIds+ schedules, bỏ qua popup chọn product, không tạo bản ghi trùng. - Link khi không có archived row active → tạo linked term mới + mở popup chọn product như cũ.
- Term
IsActive = falsekhông bao giờ được restore qua UI link flow.
Invoice: vòng trạng thái tài chính
Billing Details hiển thị theo invoice (không phải EnrollmentInvoiceLog — log nội bộ bị ẩn):
| Trạng thái | Khi nào |
|---|---|
Initialised (0) | Sau khi Skip; trạng thái nền. |
Generated (75) | Invoice vừa sinh khi vào workflow detail page. |
Approved | Sau khi Approve. |
Hành động ↔ invoice (không tác động EnrollmentInvoiceLog):
Add-on & Discount theo kỳ
- Áp theo từng kỳ (period) của enrollment, không phải toàn enrollment.
- Đại diện bằng cặp (Add-on/Discount, EnrollmentSchedule period).
- Period rows lấy từ EnrollmentSchedule đã book, không sinh từ price type của add-on.
- Bulk select-all chỉ xuất hiện khi nhóm có > 3 kỳ.
- Lưu = "desired state": check = áp, uncheck = gỡ; trùng cặp trong request → từ chối.
Skip: reset một kỳ
Skip là hành động cấp invoice: hủy scheduler đang chạy (nếu có) + đưa invoice về Initialised (0) — vẫn reset status dù không có scheduler. Archive booking line liên quan, rồi regenerate invoice hoặc issue credit note theo trạng thái invoice trước đó — giữ nhất quán tài chính.
BookingAndBilling vs BillingOnly
| BookingAndBilling | BillingOnly (TypeId=1) | |
|---|---|---|
| Program section | Hiện, validation active | Ẩn, không chặn save/publish |
| Cần SubscriptionProduct để publish | Có | Không |
| Link term cần product planned check | Có | Không |
Loại được chọn trước khi tạo; sau khi subscription tồn tại thì không đổi được.
Yêu cầu booking từ Product (requirements)
Yêu cầu attendee khi booking được resolve từ Product trong Program của subscription, theo term của schedule được chọn (SubscriptionTerm.ProductIds).
| Dimension | Đánh giá |
|---|---|
| Age | Trong khoảng tuổi Product yêu cầu. |
| School Year | Khớp năm học Product yêu cầu. |
| Level | So Attendee.Level (nguồn chính thức) với MinLevel/MaxLevel của Product. |
| Height / Weight | Trong khoảng đo Product yêu cầu. |
| Swimming Competency | So bằng: Attendee.SwimmingCompetency == giá trị Product (không phải range). |
Quy tắc:
- Dimension không có bound (Product không cấu hình) hoặc
<Reference>_Enabled = false→ coi như chưa cấu hình, không chặn booking. - Nhiều Product cùng hiệu lực yêu cầu Swimming khác nhau → dimension coi như không cấu hình.
- Attendee phải thỏa tất cả dimension enforce; staff tắt toggle (Admin Mode, transient) → bỏ qua dimension đó; customer không bypass được.
- Không đạt → chặn chọn schedule ở Step 1 và reject submit ở Step 2 (backend revalidate).
Ví dụ thực tế: Bé An (SwimmingCompetency = "Non Swimmer") book gói bơi yêu cầu Competent. → Chặn chọn/submit khi thiếu năng lực. → Chọn schedule bị chặn (dimension Swimming Competency), submit bị reject; staff tắt toggle tạm thời được.
Extend, Rollover và Duplicate
- Extend thêm schedule liền kề vào enrollment hiện có trong cùng Term, lưu thành period riêng; không sửa pattern, add-on, booking hay invoice đã tồn tại. Schedule đã qua ngày hiện tại luôn bị loại và server reject nếu client cũ gửi lên.
- Rollover chuyển period đủ điều kiện sang linked target Term, không tạo Subscription mới và không ghi finance trực tiếp. Job chạy Individual hoặc Batch dùng cùng rule; rerun không tạo period trùng.
- Duplicate chỉ clone cấu hình Subscription theo các section được chọn, luôn tạo bản Unpublished/ Unbookable và không mang theo customer data.
Ví dụ thực tế: Rollover bị submit lại do người dùng refresh. → Không nhân đôi enrollment ở Term mới. → Job thứ hai bỏ qua period đã chuyển và báo kết quả processed/skipped/failed.
Publish lock và invoice grouping
- Subscription đã publish khóa các thay đổi thương mại/cấu trúc: price, billing settings, linked term, billing schedule và add-on. Guard ở service reject cả request mixed (field locked + field editable) theo kiểu toàn bộ request không được lưu; unpublish là đường mở khóa duy nhất.
InvoiceSplitRuleIdcho phépAccount,Attendee,Subscription; fallback legacy đi qua rule của program category rồi đến attendee-level. Chỉ schedule chưa invoiced dùng rule mới; line vẫn giữAttendeeId, còn invoice gộp hiển thị danh sách attendee.- Flyer là ordered manifest thuộc Subscription; admin save không gửi field flyer thì giữ nguyên, gửi danh sách rỗng thì clear, gửi danh sách mới thì replace. Public chỉ nhận URL/path an toàn.
Ví dụ thực tế: Hai attendee cùng một account dùng rule
Account. → Giảm số invoice nhưng không mất truy vết. → Header invoice hiển thị tên gộp, từngInvoiceLinevẫn giữ attendee tương ứng.
Enrichment trong Subscription: cấu hình → occurrence → Booking charge
Enrichment là lựa chọn dịch vụ theo ngày trong tuần gắn với Subscription Booking; không phải generic Add-on và không phải một dòng phí recurring của Subscription Billing.
| Tầng | Nguồn chính | Quy tắc |
|---|---|---|
| Setup | SubscriptionProduct + Product | Product phải active/approved, đúng ProductType.Enrichment, cùng BU/Program. |
| Theo Term | TermProduct | Phải thuộc Term/Program scope, có RateStandard và RateStandardItemId hợp lệ. |
| Step 2 | lựa chọn subscriptionProductId + weeklyPattern | Chỉ nhận Mon–Fri; count/amount từ preview server, không tin client. |
| Booking | TermBookingLine + TermAttendance | Mỗi occurrence hợp lệ tạo một line có giá và accounting item authoritative. |
| Subscription Billing | EnrollmentBillingSchedule | Chỉ dùng schedule/add-on theo kỳ; không copy phí Enrichment từ Booking. |
Price và accounting item luôn resolve từ TermProduct hiện tại tại các boundary preview, submit, confirm và Manage Enrichments. RateStandard = 0 vẫn là lựa chọn hợp lệ; occurrence vẫn được đếm. Các invariant chính: INV-SUB-ENR-SETUP-001, INV-SUB-STEP2-ENR-001 v2, INV-BOOK-SUB-ENR-001, INV-SUB-MANAGER-ENR-001/002.
Ví dụ thực tế: Phụ huynh chọn Morning Care từ Thứ 2 đến Thứ 6, rate $5. → Step 2 báo 5 occurrences và $25. → Confirm tạo 5 TermBookingLine giá $5, còn invoice Subscription chỉ giữ phí schedule gốc.
BillingSchedule boundary của Enrichment
Subscription Billing Details chỉ xem số kỳ chưa invoice trong các EnrollmentBillingSchedule đang active và thuộc đúng SubscriptionTermBillingSchedule đang mở. Period khác của cùng Subscription/Term không được làm xuất hiện cảnh báo Generate Billing Required.
Ví dụ thực tế: Một enrollment có hai period nhưng Billing Details đang mở schedule A và A đã có invoice. → Period B không được làm modal chặn schedule A. → Trang hiển thị trạng thái invoice của A.
Billing Generation
Manual/Auto là setting theo Subscription, mặc định Manual. Auto tạo một job cho ngày invoice chưa sinh sớm nhất, sau mỗi lần chạy tự reschedule kỳ chưa sinh tiếp theo; unpublish hoặc chuyển Manual hủy job pending. Manual và Auto ghi cùng billed status và không backfill invoice lịch sử.
Ví dụ thực tế: BU bật Auto cho gói có hai kỳ quá hạn. → Sinh đủ các kỳ đến hạn rồi lập lịch kỳ kế tiếp; nếu một kỳ lỗi, các kỳ trước đã thành công vẫn giữ trạng thái đã sinh.
Tiếp theo: Kiến trúc & dữ liệu.