Booking — 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.
Các tình huống thực tế và cách hệ thống phản ứng.
Double-booking (trùng lịch)
- Cùng attendee không được book cùng TermProduct trùng ngày/giờ.
- Check ở create, submit và confirm.
- Admin có thể bỏ qua bằng
IgnoreDoubleBooking = true. - Record type
Subscription/WaitingListkhông tính là conflict normal booking.
Ví dụ thực tế: Phụ huynh đặt 2 con vào cùng buổi sáng Thứ 2. → Tránh ghi nhầm cùng một trẻ hai lần. → Hệ thống chặn con thứ 2 ở bước submit, trừ khi admin bật
IgnoreDoubleBooking.
Subscription Enrichment tạo Booking line thế nào?
Subscription Booking không tạo normal TermBookingOrder. Khi confirm, occurrence đã được preview và server revalidate sẽ tạo một TermBookingLine có giá/item authoritative cùng một TermAttendance. Base Subscription session vẫn giữ line zero-priced; Enrichment không đi vào TermBookingExtra.
Ví dụ thực tế: Chọn Morning Care Mon–Fri, rate
$5. → Có 5 line Enrichment$5và 5 attendance tương ứng. → Booking total là$25ngoài base session, không tạo extra giả.
Subscription Enrichment bị duplicate sau retry
Đối chiếu semantic key TermBookingId + TermProductId + Date, không chỉ tên Product. Operation/replay guard phải converge về tối đa một active line và một attendance cho mỗi key; tổng Booking không tăng.
Ví dụ thực tế: Queue confirm timeout rồi worker chạy lại. → Key ngày 03/10 đã tồn tại. → Lần retry bỏ qua line cũ và không invoice occurrence lần hai.
Manage Enrichments không sửa được occurrence
Occurrence quá khứ, đã invoiced, credited, attended hoặc bị Booking lock là locked. Request chứa một ngày duplicate, ngoài Term, ngoài BU/site/attendee/subscription scope hoặc invalid thì fail atomic; không partial add/remove các ngày còn lại.
Ví dụ thực tế: Staff bỏ occurrence đã invoiced trong cùng request với một ngày tương lai. → Bảo toàn audit và số đã thu. → API trả validation/adjustment-required, không mutate cả request.
Chiều cao / cân nặng (measurement)
- Course có thể cấu hình ngưỡng height/weight. Attendee ngoài ngưỡng → selection invalid, chặn submit.
- Thiếu giá trị bắt buộc (vd weight) → submit fail cho selection đó.
- Admin override từng rule: tắt Height vẫn để Weight chặn, và ngược lại.
Ví dụ thực tế: Lớp bơi đặt ngưỡng cân nặng tối thiểu; trẻ chưa khai weight. → Đảm bảo trẻ đủ điều kiện an toàn trước khi vào lớp. → Submit fail cho selection đó tới khi nhập weight, hoặc admin tắt riêng rule Weight.
Booking Cut-off (chọn session quá khứ)
- Mặc định không chọn được session đã qua mốc cut-off.
- Admin tắt Booking Cut-off trong Booking Rule Options → chọn được session quá khứ (mọi category: Term Care, Holiday, Class).
Ví dụ thực tế: Trẻ đã đi học buổi Thứ 3 nhưng quên đăng ký, cần book bù. → Cho ops ghi nhận buổi đã diễn ra để tính phí/điểm danh. → Admin tắt Booking Cut-off, chọn lại session quá khứ đó rồi confirm.
Pay Later & timeout auto-cancel
- PayNow / Deposit / Direct Debit có thể arm timeout: quá hạn chưa thanh toán → tự hủy.
- PayLater không timeout nhưng phải qua validate eligibility lúc submit.
Ví dụ thực tế: Phụ huynh chọn Deposit rồi rời đi không thanh toán. → Tránh giữ chỗ vô thời hạn làm nghẽn capacity. → Hết hạn timeout, booking tự hủy, slot mở lại cho người khác.
Capacity
- Giới hạn theo
TermProduct.CapacityvàTermProgramSetCapacity(tuần giới hạn). ValidateCapacityForConfirmBookingchạy trước confirm.
Ví dụ thực tế: Lớp còn 1 chỗ, hai phụ huynh cùng bấm confirm gần như đồng thời. → Không cho vượt sĩ số lớp. →
ValidateCapacityForConfirmBookingchặn người confirm sau, báo hết chỗ.
Add-on capacity (bắt buộc kèm add-on)
- Add-on (vd bus route) có capacity riêng; order line chạm đủ → chặn confirm với
FULLY_ADDON_CAPACITY_BOOKING_FOUND, trừ khi admin bậtIgnoreDoubleBooking. - Khác với session capacity: đây là capacity của add-on đi kèm booking, không phải sĩ số session.
Ví dụ thực tế: Booking mới kèm tuyến bus đã đủ trẻ đăng ký. → Tránh nhận thêm trẻ vượt tải xe. → Confirm bị chặn ở bước build plan; admin bật
IgnoreDoubleBookingnếu cố ý cho phép.
Confirm chỉ đúng khi order ở trạng thái cho phép
- Chỉ
TypeId ∈ {Booking, TrialSession}vàStatusId ∈ {Submitted, Submitted_Change, Attendance_Approved, Quote}mới confirm được; các order khác làm confirm fail. Confirm_Booking→Approved,Confirm_Attendance→Attendance_Approved(bỏ qua Billing Difference),Confirm_Invoice→Quote.
Ví dụ thực tế: Admin bấm Confirm trên order
WaitingListhoặc booking đãCanceled. → Tránh tạo runtime booking từ trạng thái không hợp lệ. → Confirm bị chặn, booking không đổi trạng thái.
Quote → Accept
- Thay vì confirm ngay, có thể
GenerateQuotes(statusQuote) → kháchAcceptQuote(Accept) → confirm (Approved).
Ví dụ thực tế: Admin chốt chương trình holiday cho gia đình nhưng để họ tự duyệt giá. → Cho khách xem và đồng ý báo giá trước khi cam kết thanh toán. →
GenerateQuotesgửi báo giá, kháchAcceptQuote, sau đó mới confirm.
Cancel → credit note
- Cancel booking đã confirm: status →
Canceled, đảo invoice bằng credit note, deactivateTermAttendance— kể cả khi một số line trước đó đã rờiApproved.
Ví dụ thực tế: Gia đình hủy khóa đã đóng tiền và điểm danh. → Hoàn tiền đúng sổ sách và bỏ các buổi không còn hiệu lực. → Booking về
Canceled, hệ thống phát credit note đảo invoice và deactivateTermAttendance.
Stale attendance (điểm danh "mồ côi")
- Có quy trình dry-run phát hiện
TermAttendancecòn active nhưngTermBookingcha đãCanceled(trả về count + sample, không sửa dữ liệu) để ops sửa an toàn.
Ví dụ thực tế: Một lỗi cũ để lại điểm danh của booking đã hủy, gây sai báo cáo có mặt. → Phát hiện dữ liệu rác trước khi tự ý xóa. → Dry-run liệt kê count + vài bản ghi mẫu để ops kiểm tra rồi xử lý tay.
Combined edit (gộp booking)
- Category có
CombinedBookingOptionId = 1: edit một booking sẽ nạp các booking cùngActionLogIdxuyên course trong category. Bỏ một booking khỏi nhóm khi reconfirm → cancel + deactivate attendance của nó. Chi tiết: Combine booking rule.
Ví dụ thực tế: Phụ huynh đổi lịch một con vốn book chung nhóm gộp nhiều khóa. → Sửa cả nhóm đồng bộ thay vì lệch từng booking. → Edit một booking nạp các booking cùng
ActionLogId; bỏ một booking khỏi nhóm sẽ cancel + deactivate attendance của nó.
Confirm Attendance (Submit & Confirm Attendance)
- Submit xong → gọi confirm với action type
Confirm_Attendancengay → statusAttendance_Approved(chưa phải confirmed đầy đủ), không mở Billing Difference preview. - Submit thất bại, hoặc không trả đủ
TermBookingOrderIds→ báo lỗi, không confirm.
Ví dụ thực tế: Admin cần chốt điểm danh cho booking đã đi học nhưng chưa chốt tiền. → Tránh bắt xem chênh lệch tài chính vô nghĩa. →
Submit & Confirm Attendancesubmit rồi chốtAttendance_Approvedngay, không preview.
Edit booking đã confirm (Billing Difference)
Flow đầy đủ, điều kiện và token nằm ở Edit Booking & Preview Billing Diff. Phần này chỉ giữ các case dễ nhầm trong vận hành.
Edit một phần booking dùng chung BillingId
- Scope của Preview là attendee thuộc
TermBookingOrderIdsmà lần Submit hiện tại trả về. - Các booking group khác cùng
BillingIdđược gom thànhOther booking groups; dòng này mangIsOutsideSelectedScope = truevà không làm tăng số attendee đã chọn. Summary.Differencevẫn là tổng toàn scope; breakdown không phải phép tính độc lập theo attendee.
Ví dụ thực tế: Booking Manager có hai nhóm cùng tên
After School Caress; nhóm trên có 1 attendee, nhóm dưới có 2 attendee và chỉ nhóm dưới được edit. → Confirm popup liệt kê 2 attendee. → Preview hiển thị 2 dòng riêng, một dòngOther booking groupscho nhóm trên, và tổng cuối cùng vẫn reconcile với toàn bộ billing record.
Phân biệt estimate trên form và billing impact trong Preview
Estimated fee per attendeeở cuối booking form là con số tham khảo theo attendee của form hiện tại.- Con số
Difference/Net billing impactở Preview là tác động ròng của toàn bộ billing scope:new invoices − credit notes − scheduled-delete invoices. - Vì khác scope và khác đơn vị, hai con số có thể khác nhau mà không tự động là lỗi tính tiền. Khi quyết định Confirm, dùng net impact và các dòng invoice/credit note trong Preview làm nguồn chính thức.
Ví dụ thực tế: Form hiển thị
$65/attendee nhưng Preview hiển thị+$68. → Không kết luận Preview sai chỉ vì$68lớn hơn$65; trước hết kiểm tra Preview đang bao gồm invoice của attendee đã chọn, nhóm booking khác và bucket shared/unallocated nào.
Archived booking item và finance transaction
TermBookingLine/Extra.StatusId = Archivedlà item booking đã bỏ nhưng còn giá trị lịch sử; nếu còn active invoice/credit-note log chưa được credit đủ, khoản đó phải được đưa vào phép reverse/re-bill.Invoice/CreditNote.IsActive = falselà transaction đã deactivate/soft-archive; không tính vào current billed, paid/outstanding hoặc action của Preview.- Current baseline chỉ nhận finance status
Generated,Submitted,Approved,Paid.Void,Deleted,Keptlà trạng thái lịch sử/non-current; không cộng vào current billing total. - Credit note active và thuộc status hợp lệ được trừ khỏi current billed; vì vậy current billed có thể thấp hơn
Paid to date + Open invoice balance, và UI phải giải thích khoản credit hiện hữu.
Ví dụ thực tế: Một credit note
Submittedcòn active làm giảm current billed$1.50, trong khi invoice balance vẫn là$125.00. → Giữ credit note vì nó là khoản tín dụng hiện hành. → Preview hiển thị current net$123.50và không cảnh báo sai; credit noteVoidhoặc inactive thì không được tính.
Không có original transaction
- Không có invoice/credit note baseline → không dựng amount Preview đáng tin cậy.
- Chỉ mở direct-confirm rõ ràng khi error code được policy cho phép; không tạo
PreviewSnapshotToken.
Ví dụ thực tế: Booking chưa từng phát sinh invoice. → Không hiển thị chênh lệch giả. → Admin có thể direct-confirm theo policy, còn flow đã có token thì phải block nếu Preview trở nên unavailable.
Booking Manager bulk Confirm với edit booking
- Bulk
Confirm_Bookingchỉ queue selection toàn booking mới. - Có ít nhất một edit/pending-edit order → chặn toàn bộ selection trước lock và scheduler.
- Selection mixed new/edit không được tách tự động; admin confirm edit booking riêng qua Billing Difference.
- Bulk
Confirm_Attendancevẫn hoạt động vì không chốt billing.
Ví dụ thực tế: Admin chọn nhóm booking mới cùng một nhóm đang edit để Confirm. → Tránh queue booking mới rồi bỏ qua billing review của nhóm edit. → Hệ thống từ chối cả selection; admin confirm nhóm edit riêng qua Preview, còn booking mới có thể chọn lại để bulk Confirm.
Ví dụ thực tế: Admin giảm booking đã confirm từ $65 xuống $55. → Cần kiểm tra tác động trước khi chốt. → Submit im lặng, Preview trả
Difference = -$10, admin chọn mute rồi Confirm mới ghi billing.
Ví dụ thực tế: Preview bị tắt cho một business unit. → Vẫn phải hỏi mute một lần ở popup đầu. → Hệ thống submit rồi confirm trực tiếp với đúng lựa chọn notification đó.
Ví dụ thực tế: Booking chưa từng phát sinh invoice nên không có baseline để so sánh. → Không hiển thị chênh lệch giả. → Hệ thống mở direct-confirm rõ ràng, không yêu cầu
PreviewSnapshotToken.
Giữ điểm danh khi edit (roll call preserve)
- Edit confirm tạo lại
TermBookingLine+ thayTermAttendancevì giá/discount đổi → giữ roll call từ attendance cũ nếu nó cóRollCallStatusId > 0— kể cả khi bản cũ đã bị deactivate trong luồng. - Copy cả
RollCallHistory(action id, thời gian, signed-by, signature, absent reason, note); confirm retry không nhân đôi history. RollCallStatusId = 0(chưa điểm danh) → attendance mới bình thường, không copy. Session bị xóa/cancel → không copy roll call sang nơi khác.
Ví dụ thực tế: Booking đã điểm danh buổi Thứ 2 rồi admin sửa giá buổi đó, confirm tạo lại attendance. → Tránh mất dấu vết có mặt/ra về. → Attendance mới copy roll call + history từ bản cũ (invariant
INV-BOOK-27).
Submit & Confirm Attendance (admin)
- Admin Mode có action Submit & Confirm Attendance: submit trước, rồi gọi confirm bằng action type
Confirm_AttendancevớiTermBookingOrderIdssubmit trả về. - Kết quả booking ở
Attendance_Approved— chốt điểm danh, không confirm đầy đủ. - Không bị gate bởi Billing Difference (invariant
INV-BOOK-26). - Submit ok nhưng confirm attendance fail → báo lỗi để admin retry hoặc hoàn thành ở Booking Manager; booking vẫn ở
Submitted. Submit không trả vềTermBookingOrderIds→ báo lỗi, không gọi confirm.
Ví dụ thực tế: Lớp bơi đã diễn ra nhưng admin quên chốt buổi đầu, cần điểm danh ngay. → Không cần review chênh lệch phí vì action này không đổi tiền. →
Submit & Confirm Attendanceđưa booking vềAttendance_Approvedđể cán bộ điểm danh làm việc, không confirm đầy đủ.
Pending notification suppression (Submit & Confirm)
POST /BookingOrdersadmin mode vớiStatusId= Confirmed (Submit & Confirm) → không enqueue/ gửi pending-booking notification cho request đó.- Request submit không direct-confirm (booking còn pending) → pending-booking notification chạy như cũ.
- Đây là behavior backend-only: không đổi contract
POST /BookingOrders. - Mute Notifications (popup xác nhận admin) → suppress các job notification eligible của action:
- Submit → suppress submitted; Confirm → suppress confirmed.
- Submit & Confirm → submitted đã suppress sẵn (direct-confirm) + confirmed suppress.
- Cancel → suppress cancelled.
- Không chọn Mute → giữ nguyên hành vi notification hiện có.
Ví dụ thực tế: Admin
Submit & Confirmchốt nhanh hàng loạt nhưng không muốn spam phụ huynh. → Tránh email trạng thái trung gian không cần thiết. → Request direct-confirm không phát pending notification; nếu bật Mute Notifications, cả job confirmed cũng không được gửi.
"Click calendar event không thấy chi tiết"
Booking Calendar dùng popup read-only mới; popup chỉ hiện khi click event thật, không phải ô trống hoặc +N more. Nội dung gồm TPS, session, giờ và toàn bộ attendee; popup library mặc định bị suppress.
Ví dụ thực tế: Tile chỉ hiện
...và user cần biết attendee nào đã book. → Click tile event. → Popup hiện từng attendee trên dòng riêng, cùng format/timezone với tile.
"Special-day programme vẫn hiện để book"
Kiểm tra app đang ở Booking surface và session có được group đúng sessionGroupKey/bucket không. Special day programme phải không bookable; không dùng thiếu capacity để suy luận trạng thái này.
Ví dụ thực tế: Session đặc biệt trùng giờ với class thường nên bị lẫn row. → Không mở nhầm slot cho khách. → Matcher dùng product + time slot trong bucket, sau đó filter special-day programme khỏi booking.