Finance — Khái niệm cốt lõi
Core objects
| Khái niệm | Nghĩa |
|---|---|
| Billing | Aggregate gom nghĩa vụ tính tiền từ booking/subscription; sở hữu payment option, split context, dữ liệu sinh invoice. |
| BillingPeriod | Kỳ tài chính hoặc giai đoạn dùng để nhóm line, nhất là subscription billing schedule. |
| Invoice | Chứng từ phải thu gửi customer/account; có header, line, tax/discount, status và scheduler xử lý. |
| InvoiceLine | Dòng phí cụ thể: session, add-on, discount, adjustment hoặc metadata liên quan booking. |
| CreditNote | Chứng từ điều chỉnh ngược invoice khi giảm tiền, hủy hoặc chỉnh booking sau khi invoice đã phát hành/paid. |
| FinanceTransaction | Projection cho query invoice/credit-note/payment trạng thái hiện tại; giúp list/search nhanh và ổn định. |
| InvoiceScheduler | Job/scheduler phát hành hoặc xử lý invoice ở thời điểm phù hợp. |
| Preview Billing Difference | Read-only computation cho edit booking: tính invoice/credit-note dự kiến mà không persist. |
| Accounting Provider | Nhà cung cấp kế toán như Xero; nhận invoice/credit-note/payment sync từ AIMY. |
Billing vs Invoice
Billing là context tính tiền; Invoice là chứng từ customer thấy, finance vận hành. Một billing có thể tạo nhiều invoice khi split rule tách theo org, attendee, category hoặc kỳ.
Invoice vs FinanceTransaction
Invoice là record nghiệp vụ gốc. FinanceTransaction là projection/read model cho search, list, sync booking number, allocation, trạng thái tổng hợp. Lệch giữa hai bên → source of truth vẫn là record gốc; projection cần refresh hoặc reconcile.
Subscription Enrichment: hai charge path không được trộn
Subscription có hai loại tiền cần tách rõ:
| Nguồn | Finance xử lý | Không làm |
|---|---|---|
| Base schedule + Add-on | SubscriptionBillingService đọc EnrollmentBillingSchedule và tạo Subscription invoice. | Không đọc amount từ TermBookingLine Enrichment. |
| Enrichment occurrence | Booking Billing/Reconciliation đọc priced TermBookingLine, tạo invoice/credit log theo Booking. | Không ghi ngược amount vào Subscription schedule hoặc Subscription invoice. |
Mỗi Enrichment occurrence phải có một Booking line authoritative theo TermBookingId + TermProductId + Date; cùng occurrence không được tính ở cả hai charge path. InvoiceLine của Subscription có thể vẫn chứa base price line hợp lệ, kể cả unit price bằng zero, nhưng không được có reference đến Enrichment Booking line.
Ví dụ thực tế: Schedule Subscription là $5, Booking có 5 Enrichment occurrence $25. → Subscription invoice là $5; Booking reconciliation xử lý $25. → Tổng hai chứng từ đúng boundary, không biến thành một Subscription invoice $30.
Credit note
Credit note xuất hiện khi edit/cancel giảm nghĩa vụ tài chính sau khi invoice phát hành hoặc paid. Không phải "xóa invoice"; là chứng từ điều chỉnh giữ lịch sử tài chính audit được.
Billing generation — cách sinh chứng từ
Billing chứa nghĩa vụ tính tiền; billing generation là pipeline biến booking/enrollment đã confirm thành chứng từ (Invoice, CreditNote). Nó tách 3 lớp (invariant INV-BILLING-CORE-001):
| Lớp | Làm gì |
|---|---|
| Prepare | Gom booking/order/line/extra/discount/split thành input chuẩn. |
| Calculation core | Tính invoice, line, tax, discount, split group, total, credit-note impact — chỉ trong memory, không persist. |
| Persistence | Lưu invoice/credit note, finance log, scheduler, SaveChangesAsync. |
Preview Billing Diff và real confirm dùng chung core → projected output khớp real output trong các trạng thái eligible/parity-safe. Trạng thái không thể bảo đảm parity trả structured error, không trả amount ước lượng. Quy tắc xương sống (mỗi rule kèm ví dụ):
- Gate confirm — confirm chỉ chốt sau khi billing regeneration thành công; billing fail → confirm fail, không ghi side effect (INV-CONFIRM-BILLING-001).
Ví dụ thực tế: Phụ huynh confirm booking Pay Now nhưng cấu hình DD hỏng → booking không sang
Approved, trả failure → admin sửa cấu hình rồi confirm lại. - Frequency override — Pay Now →
One_Off, Direct Debit →WeeklyquaResolveBillingFrequencyOverride; real path và preview dùng chung helper này.Ví dụ thực tế: Khách chọn Direct Debit khi checkout → invoice tách theo tuần để DD thu đúng kỳ.
- Net difference —
Difference = new invoices − new credit notes − scheduled-delete invoices; backend là nguồn giá trị chính thức.Ví dụ thực tế: Invoice mới $55 thay invoice cũ $65 → Preview trừ invoice scheduled-delete và hiển thị đúng tác động ròng.
- Billing scope & attendee breakdown — Preview tính một lần trên toàn billing scope rồi phân bổ kết quả cho attendee đã submit; booking group khác cùng
BillingIdvàoOther booking groups, khoản không rõ attribution vàoShared / Unallocated. Các row phải reconcile vớiSummary, không phải phép tính độc lập.Ví dụ thực tế: Một BillingId có nhóm 1 attendee và nhóm 2 attendee nhưng chỉ nhóm 2 được edit → Preview tách 2 attendee đang edit, gom nhóm còn lại, nhưng tổng net impact vẫn bao quát toàn billing record.
- Whole-scope net impact —
Summary.Difference/Net billing impactlànew invoices − credit notes − scheduled-delete invoices; đây là số dùng để Confirm, không phảiEstimated fee per attendeecủa booking form.Ví dụ thực tế: Form hiển thị
$65/attendee còn Preview hiển thị+$68→ hai số khác scope/semantic; không chia+$68cho số attendee để suy ra giá từng người. - Unsupported or ambiguous state — mixed retained + Generated invoices vẫn được preview khi retained logs nhất quán và có thể reconstruct; context không đầy đủ, log retained không mapping được hoặc scope không hợp lệ phải trả structured error và không hiển thị amount ước lượng.
Ví dụ thực tế: Billing có invoice retained và Generated nhưng line log map được rõ ràng → Preview tiếp tục và scheduled-delete Generated chưa thanh toán. Nếu log retained để lại residual không giải thích được → block thay vì đoán amount.
- Deposit — confirm theo deposit vẫn sinh invoice full amount; settlement nằm ngoài billing generation (INV-DEP-015).
Ví dụ thực tế: Khách đặt cọc 20% → invoice vẫn ghi đủ toàn bộ; phần cọc Payment xử lý, không sửa invoice.
- Approve / Skip subscription — approve sinh invoice qua
UpdateBillingByIdAsync; skip với invoice đã Paid/Sent/Synced thì phát credit note, chưa phát hành thì regenerate.Ví dụ thực tế: Admin approve kỳ Tháng 5 → tạo TermBooking + invoice + log Approved; skip kỳ đã thu → credit note đảo.
- Price line bắt buộc — subscription generation cần
SubscriptionPriceId+ ≥1 price line active; thiếu → fail trước persist, không fallback từ schedule amount (INV-SUB-BM-04).Ví dụ thực tế: Gói Weekly chưa khai price line → generate fail với structured validation; khai xong mới ra invoice, không bao giờ sinh hóa đơn trống.
Invoice email
Invoice email là đường gửi invoice cho customer qua Notification template, sẵn có bất kể accounting provider (Xero hay AIMY Accounting) (INV-INVOICE-EMAIL-001).
- Composer — mở trước khi schedule; hiển thị số lượng invoice, recipient, From, Email Reply To, Subject, Body; Subject/Body chỉnh được trước Send (INV-INVOICE-EMAIL-002).
- Single & batch — batch nhiều site → mỗi site một editor riêng theo template + Email Reply To của site; cancel/close composer → không schedule gì.
- Không PDF attachment — email không đính kèm invoice PDF; không có control "Include Invoice PDF", legacy payload cũng không khôi phục được attachment (INV-INVOICE-EMAIL-003).
- Placeholders —
,,; link HTTPS, dùng opaque guid/token, không lộ numeric id (INV-INVOICE-EMAIL-005). - Customer link — View/Download yêu cầu authenticate + authorize theo account/BU; unauthorized trả not-found/forbidden, không lộ metadata (INV-INVOICE-LINK-001).
Ví dụ thực tế: Admin mở invoice AIN-0042 Approved trong BU Xero, chọn Email → composer hiện template site, chỉnh Subject/Body rồi Send → email không PDF; customer đăng nhập mới mở link View/Download.
Invoice reminders
Reminder là nhắc nợ tự động gửi qua đúng pipeline Invoice Email, cấu hình theo business unit.
| Quy tắc | Nội dung |
|---|---|
| Giới hạn | ≤3 rule active/BU; add/edit/delete; disable giữ rule, chặn run mới |
| Loại rule | OnInvoiceDate (no offset), BeforeDueDate(N), AfterDueDate(N) với offset 0–365 |
| Trùng target | 2 rule không trùng effective target (kể cả 0 days before/0 days after) |
| Target date | Invoice.Date; DueDate ∓ N theo local calendar/timezone BU; không backfill trước ngày enable |
| Eligibility | active, cùng BU, AmountDue > 0, không trong DD collection flow, LockTypeId null |
| Template | render theo Invoice Email template site tại thời điểm gửi; không lưu sender/subject/attachment riêng |
| Idempotent | 1 logical send per (invoice, rule); Invoice.Sent chỉ true sau khi NotificationJob tạo |
Ví dụ thực tế: Admin tạo rule
AfterDueDate(7); invoice AIN-0031 còn nợ, không trong DD collection flow → evaluator chạy due+7 theo giờ BU → gửi reminder bằng template Invoice Email hiện tại, ghi 1 entry history trên Account + Invoice Transaction.
Invoice fully-paid notification
Invoice_Fully_Paid là EntityEvent chuyên biệt phát khi invoice chuyển sang FinanceStatus.Paid (INV-INVOICE-PAID-NOTIF-001).
- Trigger — mọi path set
Invoice.StatusId = Paid(payment hoặc credit-note allocation làmAmountDue = 0) phát event sau commit, chỉ khi có transition thật. - Ngoại lệ — invoice
TotalAmount = 0đánh Paid vì không có gì thu → không phát; invoice import/sync sẵn Paid → không phát. - Hint — event mang
NotificationEventTypeId = PaymentSuccessConfirmation(810); worker tạo NotificationJob qua hint path, không gọi trực tiếpCreateNotificationJobAsync(INV-INVOICE-PAID-NOTIF-002). - Template — default từ NotificationOption, site overwrite qua NotificationSetting theo
OrgId; email disabled ở site → không gửi (INV-INVOICE-PAID-NOTIF-003). - Recipient — primary contact của account; thiếu email → job complete không delivery, không throw (INV-INVOICE-PAID-NOTIF-004).
Ví dụ thực tế: Phụ huynh trả nốt số dư invoice AIN-0027 →
AmountDue = 0→ commit rồi publishInvoice_Fully_Paid→ worker tạo PaymentSuccessConfirmation job; email về primary contact nếu site bật kênh email.
Invoice viewed tracking
Invoice.Viewed (boolean) lưu trạng thái customer đã truy cập invoice thành công (INV-INVOICE-VIEW-001).
- Mặc định
false; chỉ settruekhi customer authenticated + authorized và View/Download PDF thành công (INV-INVOICE-VIEW-002). - Monotonic — không reset bởi resend email, edit, paid hay sync; truy cập lặp idempotent.
- Không set — admin mở Invoice Details, email schedule/delivery, request unauthorized, hoặc PDF fail.
- Hiển thị — Invoice Details API trả
Viewed; Booking Manager chỉ hiện tagViewedkhi state true (INV-INVOICE-VIEW-003).
Ví dụ thực tế: Customer mở link View Invoice AIN-0012 lần đầu → set
Viewed = true→ admin thấy tagViewed; resend email không mất tag.
Invoice lock & tag "Processing"
Invoice đang trong luồng xử lý payment có LockTypeId != null được đánh dấu "processing" trong Invoice Manager.
- List — dưới Invoice Number hiện tag
ProcessingkhiLockTypeId != null; không thay thế hay mutate status (INV-INV-MGR-LOCK-001). - Hover — tooltip đúng nội dung
Payment in processing(INV-INV-MGR-LOCK-002). - Details — title hiện
Payment in processingsau Invoice Status;Add PaymentvàApply Creditvisible nhưng disabled (INV-INV-MGR-LOCK-003/005). - Display-only — tag không tạo invoice status mới, không đổi payment/lock lifecycle (INV-INV-MGR-LOCK-004).
Ví dụ thực tế: Payment đang xử lý, invoice AIN-0090 có
LockTypeId != null→ list hiện tag Processing dưới số hóa đơn; details thấyAdd Payment/Apply Creditdisabled nhưng trạng thái invoice vẫn nguyên.
Closure Day credit và balance audit
- Credit basis là tổng
TermBookingInvoiceLog.Amounttrên invoice đãSubmitted/Approved/Paid, trừ credit-note log còn hiệu lực; không dùngTermBookingLine.TotalAmount. - Credit note giữ tax rate, currency và line-amount type của invoice nguồn. Mixed treatment tách component/ header; mapping lịch sử mơ hồ fail trước mutation. Converted line clear amount/discount/promo và không được billing regeneration đưa trở lại.
GET api/ClosureDay/balance-audit?closureDayIds=...là read-only, tối đa 50 closure day/request, scope BU + permissionSETUP_CLOSURE_DAY; trả booking total, active line total và invoiced-minus-credited.- Cancel-and-credit dùng transaction serializable; credit note và line zero commit cùng nhau trước khi publish Xero event.
Ví dụ thực tế: Line closure-day trị giá $100 nhưng mới invoiced $60 và đã credit $10. → Chỉ credit thêm $50; balance audit cho thấy $60 − $10, không tạo credit từ $100.
Invoice grouping và Booking Info
Invoice subscription có thể gộp nhiều attendee theo split rule, nhưng InvoiceLine vẫn giữ attendee ownership. Trong Invoice Manager, Booking ID gắn với billing được mở Booking Info read-only; view này load tất cả booking active của billing, kiểm tra BU/customer scope và không cho sửa staff note.
Ví dụ thực tế: Một invoice có booking của nhiều program. → Không mở nhầm invoice/term riêng. → Click Booking ID để xem một hierarchy Billing → program → booking, rồi Go to Booking Manager với Billing filter.
Ownership ngắn gọn
| Owner | Sở hữu |
|---|---|
| Billing | Payment option trên billing, split context, dữ liệu chuẩn bị sinh invoice. |
| Invoice | Header/line/status/lock của invoice. |
| CreditNote | Header/line/allocation của credit note. |
| FinanceTransaction | Projection/search state, có thể refresh từ nguồn gốc. |
| Payment | Thu tiền và provider transaction; không sở hữu invoice lifecycle. |