Subscription — Case thường gặp
Cách đọc mỗi case: bắt đầu từ triệu chứng/câu hỏi, sau đó đọc behavior đúng, cách kiểm tra ở UI/API/dữ liệu/code, source anchor và Ví dụ thực tế. Các case ở đây ưu tiên behavior đang chạy; nếu cần contract gốc, mở spec/code được dẫn ở cuối case.
"Khách book bị báo trùng lịch"
Hệ thống cố ý chặn double-booking. Submit → so schedule với enrollment/booking đang có của attendee; trùng → cả request fail, không tạo state một phần.
- Edit một enrollment: EnrollmentId hiện tại được loại trừ khỏi check (không tự xung đột).
- Direct-confirm / future-confirm: dùng cùng guard → cũng bị chặn nếu trùng.
Spec:
subscription-booking-double-booking-guard+ requirement "validates existing enrollment schedules before runtime state creation". Phía normal booking xem Booking → Case thường gặp.
Ví dụ thực tế: Trẻ đã có enrollment After School Thứ 4; phụ huynh book thêm gói trùng đúng chiều Thứ 4. → Tránh ghi nhận trẻ ở hai chỗ cùng giờ. → Cả request fail lúc submit, không tạo enrollment một phần.
"Manage Add-ons không thấy kỳ tôi muốn"
Popup chỉ hiện các kỳ thuộc EnrollmentBillingSchedule (đã book), không hiện kỳ chưa book của subscription term. Submit kỳ chưa book → bị từ chối.
- Bulk "select all" chỉ hiện khi nhóm có > 3 kỳ.
- Period rows lấy từ enrollment schedule, không từ price type của add-on.
Ví dụ thực tế: Admin thêm add-on bữa ăn cho Term 3 nhưng trẻ chưa book Term 3. → Chỉ gắn add-on vào kỳ đã thực sự book. → Term 3 không hiện trong popup; cố submit → bị từ chối.
"Skip một kỳ thì hóa đơn ra sao?"
Skip đưa invoice về Initialised (0), hủy scheduler đang active (nếu có). Tùy trạng thái invoice trước đó: regenerate invoice hoặc issue credit note để giữ nhất quán tài chính, đồng thời archive booking line liên quan.
Ví dụ thực tế: Gia đình đi nghỉ, xin bỏ kỳ tháng 7 đã xuất hóa đơn. → Không thu phí kỳ trẻ không dùng. → Skip → invoice
Initialised, credit note đảo phần đã issue, archive booking line tháng 7.
"Tạo gói BillingOnly mà không cần product"
BillingOnly (TypeId=1):
- Ẩn Program section; không chặn save/publish.
- Publish được dù không có
SubscriptionProduct. - Link term không cần product planned check.
Trong khi BookingAndBilling vẫn bắt buộc có product để publish + planned check khi link term.
Lưu ý: chọn loại trước khi tạo; sau khi subscription tồn tại thì read-only.
Ví dụ thực tế: Trung tâm thu phụ phí cơ sở vật chất hàng kỳ, không gắn buổi học cụ thể nào. → Lập gói chỉ để tính tiền, khỏi cấu hình product. → Tạo gói
BillingOnly, publish ngay dù không cóSubscriptionProduct.
"Không archive được gói"
Chỉ archive được gói unpublished (Unbookable). Gói đã publish → Current tab không cho archive, request trực tiếp bị server từ chối. Archive/restore không đổi publish status.
Ví dụ thực tế: Admin muốn dọn gói "Term 1 2025" đang mở bán. → Tránh ẩn nhầm gói khách vẫn book được. → Unpublish trước; gói còn bookable → nút archive bị chặn, server từ chối.
"Unlink term có booking xong vẫn thấy trong Billing nhưng mất ở Booking"
Đúng như thiết kế. Unlink term đang có booking usage → archive (giữ IsActive = true, set StatusId = 600), không xóa schedule.
- Booking UI: term archived không còn để khách chọn kỳ mới.
- Subscription Manager & Subscription Billing Manager: vẫn thấy term + schedule archived để xử lý booking/billing đang chạy.
- Nếu unlink term không có booking → soft delete bình thường (
IsActive = false), biến mất khỏi cả ba surface.
Term
IsActive = falselà soft delete, không phải archive — không restore được từ UI.
Ví dụ thực tế: Admin gỡ Term 2 khỏi gói nhưng Term 2 đã có 30 booking đang chạy. → Giữ booking/billing đang chạy không bị mất, ngừng bán kỳ mới. → Term bị archive: ẩn ở Booking UI, vẫn hiện ở Subscription/Billing Manager.
"Re-link lại term cũ bị hỏi chọn product / ra bản trùng"
Re-link đúng term đã archived (cùng subscription + term) sẽ restore, không tạo mới:
- Set
StatusIdtừ600→500; bỏ qua popup chọn product. - Giữ nguyên
ProductIdsvàSubscriptionTermBillingSchedulecũ — không mất sản phẩm. - Không tạo
SubscriptionTerm/schedule trùng; reload mỗi surface chỉ thấy một bộ active. - Nếu không có archived row active → mới mở popup chọn product và tạo linked term mới.
Ví dụ thực tế: Admin lỡ unlink Term 2 (đã archive) rồi muốn gắn lại. → Khôi phục đúng sản phẩm + schedule cũ, không tạo bản trùng. → Re-link đổi
StatusId600 → 500, bỏ qua popup product, giữ nguyênProductIds.
"Booking Cut-Off nhập số lẻ bị lỗi"
UI nhận ngày nguyên, lưu thành phút (CutOffMinute, 1 ngày = 1440). Nhập 1.5 → bị từ chối (không tự làm tròn). 0/null = không có cut-off → không chặn booking.
Ví dụ thực tế: Admin muốn chốt đăng ký trước 1,5 ngày nên gõ
1.5. → Cut-off chỉ tính theo ngày nguyên để tránh mốc phút mơ hồ. → Hệ thống từ chối1.5; admin nhập1hoặc2(lưuCutOffMinute= 1440/2880).
"Đổi filter Subscription Manager không thấy data mới"
Cố ý: đổi filter không tự reload bảng — bấm Apply mới load theo filter hiện tại. Load đầu trang tự chạy; mutation (confirm/cancel/terminate/…) thành công cũng refresh. Response request cũ (còn pending) không ghi đè data mới; nếu thấy data nhảy → kiểm tra logic "latest active request wins".
Ví dụ thực tế: Admin đổi Term để xem kỳ khác nhưng bảng vẫn hiện kỳ cũ. → Tránh load tốn request khi chỉ đang chỉnh filter. → Đổi xong bấm
Apply, bảng mới hiện theo Term đã chọn.
"Period booking khách chọn không khớp"
UI phải lấy period từ GET .../Subscriptions/{id}/BillingSchedules (canonical), không tự tính ở client. Mỗi row có BookingStart/BookingEnd ưu tiên hơn billing period start/end. Endpoint trả rỗng → trạng thái "unavailable" + chặn submit; lỗi → trạng thái retryable + chặn submit với selection cũ.
Ví dụ thực tế: Gói có kỳ tính phí theo tháng nhưng booking bắt đầu giữa tháng. → Cho khách chọn đúng khoảng được phép book. → UI lấy
BookingStart/BookingEndtừBillingSchedules; rỗng thì hiện "unavailable" và chặn submit.
"Step 2 không hiện Enrichment"
Kiểm tra lần lượt: Product có active/approved không, ProductType có phải Enrichment không, SubscriptionProduct có đúng Program/BU không, TermProduct có đúng Term/Org không, và có RateStandard + RateStandardItemId hợp lệ không. Thiếu một điều kiện → option không eligible; không sửa bằng cách gửi amount từ client. Nếu preview API/feature flag không sẵn sàng, UI fail closed.
Ví dụ thực tế: Morning Care còn active nhưng thiếu accounting item. → Không expose lựa chọn có thể tính tiền sai. → Step 2 ẩn option và Setup/API trả lỗi cấu hình cần bổ sung.
"Enrichment amount không khớp số ngày đã chọn"
Occurrence được tính theo schedule đã chọn, inclusive BookingStart/BookingEnd, Mon–Fri và cùng closure-day rule với generation thật. Preview server là source of truth; zero-rate vẫn count, response cũ/stale không được ghi đè kết quả mới.
Ví dụ thực tế: Chọn Morning Care Mon–Fri trong một tuần có 5 ngày mở, rate
$5. → Preview báo5 occurrences / $25; chọn thêm weekend không hợp lệ. → Toolbar cộng$25vào total, không nhân theo số checkbox một cách độc lập.
"Subscription invoice chứa phí Enrichment"
Đây là sai boundary. Enrichment thuộc TermBookingLine và Booking Billing/Reconciliation; Subscription Billing chỉ dùng số tiền schedule cùng Add-on/Discount theo EnrollmentBillingSchedule. Kiểm tra EnrollmentInvoiceLog/InvoiceLine của đúng schedule, không cộng TermBookingLine vào invoice định kỳ.
Ví dụ thực tế: Booking có 5 occurrence Enrichment
$25, schedule Subscription$5. → Booking reconciliation xử lý$25; Subscription invoice vẫn$5. → Nếu invoice là$30, dừng phát hành và kiểm tra projection/service boundary.
"Manage Enrichments không cho bỏ một occurrence"
Occurrence historical, invoiced, credited, attended hoặc bị Booking lock là immutable. Request có occurrence duplicate, ngoài scope, ngoài Term hoặc không eligible phải fail atomic; không được mutate những ngày hợp lệ khác. Thay đổi đã invoiced phải qua adjustment flow của Booking/Finance.
Ví dụ thực tế: Staff muốn bỏ ngày đã invoice và thêm ngày tuần sau. → Manage Enrichments trả lock/adjustment-required. → Không sửa invoice trực tiếp và không deactive một phần selection âm thầm.
"Rollover alert biến mất sau khi link term"
Cố ý: alert rollover bị mute cho tới khi tính năng rollover sẵn sàng. Mute là notification-only — không đổi state Subscription/Booking/Enrollment/Billing/Invoice/Term Planner/queue/worker. Link-term success vẫn hoàn tất + refresh linked-term như cũ; error handling giữ nguyên.
"Quyền chọn start/end của khách"
Phụ thuộc setting gói (Booking Start Selection, Booking End Selection):
| Setting | Khách được chọn? |
|---|---|
Staff Only (mặc định, 0) | Không |
Staff & Customer | Có |
Staff luôn chọn được cả start lẫn end.
Ví dụ thực tế: Trung tâm muốn khách tự chọn ngày bắt đầu gói để chủ động lịch. → Mở quyền chọn mốc cho phụ huynh thay vì chỉ staff. → Đặt
Booking Start Selection=Staff & Customer; khách thấy và chọn được ngày start.
"Gói Published không sửa được giá/add-on"
Đây là guard cố ý: Published Subscription khóa price, billing settings, linked term, billing schedule và add-on ở cả UI lẫn API. Request trộn field locked với field editable cũng reject toàn bộ; unpublish, sửa, rồi publish lại.
Ví dụ thực tế: Admin đổi tên và đổi giá trong một lần Save. → Không để name đổi còn giá không đổi gây hiểu nhầm. → Cả request bị reject; đổi tên riêng hoặc unpublish trước khi đổi giá.
"Cần thêm tuần cho enrollment đã book"
Dùng Extend, không xóa/rebook. Chọn các schedule liền kề ở đầu/cuối trong cùng Term; schedule đã qua ngày hiện tại bị loại và server cũng reject nếu gửi thủ công. Phần mới là period riêng, chỉ phần mới tạo billing; Extend không backdate.
Ví dụ thực tế: Enrollment đã confirm đến 30/06, admin muốn thêm 07/07. → Giữ nguyên history và invoice cũ. → Extend tạo period mới cho 07/07; chọn 15/06 (đã qua) thì không được phép.
"Duplicate hay Rollover?"
- Duplicate tạo Subscription config mới ở Site khác trong cùng BU, Unpublished/Unbookable, không copy customer data.
- Rollover chuyển enrollment runtime sang linked target Term, không tạo Subscription và không ghi finance trực tiếp. Individual/Batch dùng cùng rule; chạy lại không tạo bản trùng.
Ví dụ thực tế: Muốn mở cùng gói ở Site mới → Duplicate. Muốn chuyển bé đang học sang Term kế → Rollover. → Chọn nhầm action không làm lẫn config template với enrollment runtime.
"Flyer không hiện hoặc bị mất sau Save"
Flyer chỉ gắn ở màn hình edit. Giới hạn là một PDF tối đa 5 trang/10 MB hoặc tối đa 5 ảnh JPEG/PNG, mỗi ảnh tối đa 2 MB. Payload không có field flyer nghĩa là giữ nguyên; [] mới là clear. Public Step 1 ẩn section nếu manifest không hợp lệ và bỏ qua ảnh lỗi, không chặn booking.
Ví dụ thực tế: Client cũ save tên gói nhưng không biết field Flyer. → Không xóa poster đã upload. → Backend phân biệt field bị omit với danh sách rỗng và giữ manifest hiện tại.