PRD: Bộ máy Fare & Tax Pricing
| Module | Định giá (CORE-14) | PRD ID | PRD-FARE-001 |
| Status | Shipped | Owner | Pricing squad |
| Date | 2026-06-05 | Version | v1.0 |
| Packages | @nx/pricing | URD | FARE · TAX |
TL;DR
Cung cấp cho merchant một bộ máy duy nhất để biến mọi biến thể sản phẩm thành số tiền: nó chọn fare thắng cho từng dòng (giá gốc, override, hoặc discount theo quy tắc), cộng thêm đúng các loại thuế (inclusive/exclusive, gộp, cấp dòng hàng vs cấp đơn hàng), và trả về một pricing snapshot bất biến duy nhất. Luồng bán hàng yêu cầu kết quả đó khi thanh toán thay vì tự cài đặt lại logic định giá, nhờ vậy mọi dòng hàng và đơn hàng đều được định giá nhất quán và có thể truy vết.
1. Context & Problem
Trước đây giá bị gắn cứng trên sản phẩm hoặc tính ngẫu hứng khi thanh toán, không có một nơi nhất quán cho thuế, giá theo quy tắc, hay một bảng phân rã có thể truy vết. Merchant cần một bộ máy duy nhất chọn đúng fare cho từng biến thể, tính thuế chính xác (inclusive/exclusive, gộp, cấp dòng hàng vs cấp đơn hàng), và trả về một kết quả định giá nhất quán. Luồng bán hàng phải yêu cầu kết quả đó khi thanh toán mà không tự cài đặt lại logic định giá - một yêu cầu bắt buộc để báo cáo giá vốn, thuế và biên lợi nhuận đáng tin cậy cho bài toán sổ sách HKD/SME mà KICKO hướng tới.
Bộ máy này xây việc chọn fare và tính thuế trên nền danh mục biến thể sản phẩm và scope theo từng merchant của commerce, đồng thời mở một endpoint mô phỏng để luồng bán hàng sử dụng.
2. Goals & Non-Goals
Goals
- Chọn fare thắng cho từng biến thể: default, OVERRIDE (child hợp lệ đầu tiên), hoặc DISCOUNT (child hợp lệ rẻ nhất), kèm đánh giá quy tắc (attribute, operator, value; logic AND).
- Tính thuế theo thứ tự ưu tiên với các chế độ percentage/fixed/combined, cách xử lý inclusive/exclusive, thuế gộp tax-on-tax, và phạm vi cấp dòng hàng vs cấp đơn hàng.
- Mở một endpoint mô phỏng trả về một pricing snapshot bất biến - fare và thuế áp dụng theo từng dòng cộng với tổng đơn hàng.
- Tự động seed một fare set + default fare khi một biến thể sản phẩm được tạo (CDC), để mọi biến thể đều có thể định giá ngay lập tức.
- Áp dụng tax rate mặc định khi chưa cấu hình tax set nào.
Non-Goals
- Áp dụng giảm giá khuyến mãi khi thanh toán - calculator chưa được nối trong increment này (thuộc về feature Promotions).
- Định giá đa tiền tệ.
- Lưu đơn hàng, phát hành hóa đơn, hay thay đổi tồn kho.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Độ chính xác định giá | Giá khi thanh toán khớp với fare + thuế đã cấu hình cho các giỏ hàng lấy mẫu; không sai lệch so với tính tay |
| Độ phủ fare | 100% biến thể sản phẩm có thể định giá ngay (tự seed fare set + default fare) |
| Tính toàn vẹn snapshot | Mỗi đơn hàng đã định giá tạo ra một v2 snapshot bất biến với bảng phân rã fare/thuế theo từng dòng |
| Độ chính xác số học | Tính tiền khớp đến 4 chữ số thập phân xuyên suốt |
4. Personas & Use Cases
| Persona | Mục tiêu trong feature này |
|---|---|
| Owner / Manager | Đặt giá gốc và giá có điều kiện theo từng biến thể, gắn thuế, kiểm soát cách chọn giá |
| Cashier | Nhận kết quả định giá đúng và nhất quán khi thanh toán mà không phải cấu hình gì |
| System | Tự seed fare set + default fare khi một biến thể xuất hiện, để không có gì không định giá được |
Core scenarios: owner đặt một base fare và các child fare tùy chọn theo quy tắc + thuế cho từng biến thể → khi thanh toán luồng bán hàng gọi endpoint mô phỏng → bộ máy chọn fare thắng cho từng dòng, tính thuế cấp dòng hàng và cấp đơn hàng theo thứ tự ưu tiên → trả về một pricing snapshot bất biến với tổng theo từng dòng và theo đơn hàng.
5. User Stories
- Là một owner, tôi muốn đặt giá gốc cho từng biến thể và các giá child có điều kiện theo quy tắc, để đúng giá được áp dụng tự động.
- Là một owner, tôi muốn chọn xem child fare khớp đầu tiên thắng (OVERRIDE) hay child hợp lệ rẻ nhất thắng (DISCOUNT), để tôi kiểm soát chiến lược giá của mình.
- Là một owner, tôi muốn gắn thuế percentage/fixed/gộp ở cấp dòng hàng hoặc cấp đơn hàng, inclusive hoặc exclusive, để tổng phản ánh đúng cách xử lý thuế.
- Là một cashier, tôi muốn việc thanh toán trả về một kết quả định giá nhất quán cho giỏ hàng, để tôi không phải nhập lại hay tính lại giá.
- Là một cashier, tôi muốn một bảng phân rã có thể truy vết cho mọi fare và thuế đã áp dụng, để một mức giá bị tranh chấp có thể được giải thích.
- Là system, tôi muốn một fare set + default fare được tạo tự động khi một biến thể xuất hiện, để mọi biến thể đều có thể định giá ngay.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Mỗi biến thể sản phẩm có đúng một fare set được kích hoạt; owner có thể đặt một default (base) fare | URD-FARE-001 · URD-FARE-003 |
| FR-2 | Tự seed một fare set + default fare khi một biến thể sản phẩm được tạo (CDC) | URD-FARE-002 |
| FR-3 | Parent fare mang chiến lược OVERRIDE hoặc DISCOUNT; child fare mang giá có điều kiện và quy tắc (logic AND) | URD-FARE-004..006 |
| FR-4 | OVERRIDE chọn child hợp lệ đầu tiên; DISCOUNT chọn child hợp lệ rẻ nhất; default fare là phương án dự phòng | URD-FARE-007..009 |
| FR-5 | Owner có thể tạo tax type và một tax set, thêm thuế qua aggregate update | URD-TAX-001..002 |
| FR-6 | Một thuế có thể là percentage, fixed, hoặc combined; thuế áp dụng theo thứ tự ưu tiên tăng dần | URD-TAX-003..004 |
| FR-7 | Một thuế có thể exclusive (cộng thêm lên trên) hoặc inclusive (tính ngược ra); thuế gộp tính trên tổng đang chạy | URD-TAX-005..006 |
| FR-8 | Thuế cấp dòng hàng áp theo từng dòng; thuế cấp đơn hàng áp cho các tax set ở scope merchant | URD-TAX-008 |
| FR-9 | Áp dụng tax rate mặc định khi chưa cấu hình tax set nào | URD-TAX-009 |
| FR-10 | Endpoint mô phỏng trả về một v2 pricing snapshot bất biến (fare + thuế áp dụng theo từng dòng, cộng với tổng đơn hàng); v1 trả về một kết quả phẳng | URD-CON-004 |
| FR-11 | Giá trị tiền dùng độ chính xác 4 chữ số thập phân xuyên suốt tính toán | URD-CON-003 |
Toàn văn requirement và tiêu chí nghiệm thu nằm trong Pricing URD. PRD này tham chiếu chúng thay vì lặp lại.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Một v2 pricing snapshot đã tính là bất biến sau khi tạo ra; bảng phân rã không bị sửa về sau |
| Tenancy & authz | Mọi fare, thuế, và bản ghi đều được scope theo từng merchant (merchant header) và dùng soft-delete; endpoint yêu cầu xác thực |
| Precision | Tính tiền dùng float(value, 4); thuế inclusive không bao giờ làm tăng tổng |
| Performance / scale | Mô phỏng định giá cả giỏ hàng trong một lần gọi; v1 và v2 dùng chung logic fare/thuế lõi nên các sửa lỗi áp cho cả hai |
| Consistency | Đúng một fare set được kích hoạt cho mỗi biến thể; việc tự seed giữ cho mọi biến thể đều định giá được |
| i18n | Nhãn hiển thị cho người dùng là song ngữ ({ en, vi }) |
8. UX & Flows
Các màn hình cấu hình (fare, fare set, thuế, tax set) nằm trong back office hướng tới owner; kết quả định giá được luồng bán hàng dùng không giao diện khi thanh toán. Cả /simulation (v1, phẳng) và /simulation-v2 (snapshot) đều đang chạy, với v2 là chuẩn.
9. Data & Domain
| Entity | Vai trò |
|---|---|
FareSet | Bộ chứa tất cả fare cho một biến thể sản phẩm; đúng một bộ được kích hoạt cho mỗi biến thể |
Fare | Một bản ghi giá - default (base), hoặc parent/child tạo thành một nhóm chọn |
Rule | Một điều kiện (attribute, operator, value) trên một child fare; tất cả quy tắc phải qua (AND) |
TaxSet | Bộ chứa thuế cho một biến thể (cấp dòng hàng) hoặc một merchant (cấp đơn hàng) |
Tax | Một khoản percentage/fixed/combined với priority, cửa sổ thời gian, các cờ inclusive/exclusive/gộp |
TaxType | Một danh mục (vd VAT) - toàn hệ thống hoặc scope theo merchant |
| Pricing snapshot | Kết quả v2 bất biến - fare và thuế áp dụng theo từng dòng, cộng với tổng đơn hàng |
Chỉ ở mức khái niệm - schema và bất biến đầy đủ nằm trong pricing domain model.
10. Dependencies & Assumptions
Phụ thuộc vào
- Product (biến thể) - fare, tax set, và giá vốn đều khóa theo biến thể sản phẩm; CDC của biến thể kích hoạt việc tự seed.
- Commerce / Merchant - fare, thuế, và bản ghi đều được scope theo từng merchant.
- Luồng CDC biến thể sản phẩm - yếu tố kích hoạt việc tự seed một fare set + default fare.
Giả định
- Mỗi biến thể sản phẩm phát một CDC event khi tạo để fare set của nó có thể được seed.
- Tồn tại một tax rate mặc định của merchant để áp dụng khi chưa cấu hình tax set nào.
- Luồng bán hàng gọi endpoint mô phỏng khi thanh toán thay vì tự tính giá.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Thứ tự giảm giá khuyến mãi so với thuế cấp đơn hàng trong pipeline v2 | Mở: xác định calculator giảm giá chạy trước hay sau thuế cấp đơn hàng khi nó được nối |
| Hai phiên bản mô phỏng đang chạy (v1 + v2) có thể trôi lệch | v1 và v2 dùng chung logic fare/thuế lõi; sửa lỗi áp cho cả hai. Mở: ngừng v1 khi mọi consumer chuyển sang v2 |
| Thiếu cấu hình thuế sẽ khiến một dòng không bị tính thuế | Áp dụng tax rate mặc định khi chưa cấu hình tax set nào (FR-9) |
| Biến thể được tạo mà không có fare sẽ không định giá được | Tự seed một fare set + default fare khi biến thể CDC (FR-2) |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (Fares + Taxes) - xem URD feature catalog |
| Rollout | Mọi merchant; không feature flag. Cả /simulation (v1) và /simulation-v2 đang chạy, v2 là chuẩn |
| Migration | Không (entity mới; fare set tự seed từ CDC biến thể sản phẩm) |
| Launch criteria | Giá khi thanh toán khớp với fare + thuế đã cấu hình cho các giỏ hàng lấy mẫu; v2 snapshot tạo ra cho mỗi đơn; toán thuế inclusive/exclusive/gộp đã kiểm chứng |
| Monitoring | Tỉ lệ sai lệch định giá so với tính tay, độ phủ seed fare theo từng merchant, lỗi tạo snapshot |
13. FAQ
Bộ máy chọn fare thắng như thế nào? Nó đánh giá các child fare theo quy tắc của chúng. Với một parent OVERRIDE, child hợp lệ đầu tiên thắng; với một parent DISCOUNT, child hợp lệ rẻ nhất thắng; nếu không child nào hợp lệ, default fare được dùng.
Điều gì xảy ra nếu một biến thể không cấu hình thuế? Một tax rate mặc định được áp dụng để dòng hàng không bao giờ bị bỏ sót thuế.
Thuế inclusive vs exclusive - khác nhau ở đâu? Thuế exclusive được cộng thêm lên trên giá; thuế inclusive được tính ngược ra khỏi giá, nên một thuế inclusive không bao giờ làm tăng tổng.
Khác biệt giữa mô phỏng v1 và v2 là gì? v1 (/simulation) trả về một kết quả định giá phẳng; v2 (/simulation-v2) trả về một snapshot bất biến với bảng phân rã fare và thuế áp dụng theo từng dòng cộng với tổng đơn hàng. v2 là chuẩn.
Bộ máy này có áp khuyến mãi khi thanh toán không? Chưa - entity khuyến mãi và CRUD đã có, nhưng calculator giảm giá chưa được nối vào pipeline định giá trong increment này.
References
- URD: Định giá - Fares & Fare Sets · Tax Computation
- Liên quan: Cost Tracking · Promotions & Rules
- Module: Định giá - tổng quan + truy vết
- Developer: @nx/pricing · domain model · Fare System · Tax System