PRD: Fare system & pricing
| Module | Sản phẩm (CORE-05) | PRD ID | PRD-FAR-001 |
| Status | Shipped | Owner | Product squad |
| Date | 2026-06-03 | Version | v1.0 |
| Packages | @nx/pricing · @nx/commerce · @nx/sale · apps/client | URD | FAR · VAR |
TL;DR
Cho mỗi variant bán được một fare set hạng nhất - một container giá gồm giá mặc định cùng các override và discount theo khoảng ngày, theo tầng số lượng và theo ngữ cảnh - và một pricing engine theo snapshot resolve giá mà một dòng giỏ hàng phải trả tại thời điểm bán. Một CDC seam tự động provision fare set cho từng variant. Kết quả: giá POS phân tầng, xác định, thay cho một giá vô hướng duy nhất cho mỗi variant.
1. Bối cảnh & Vấn đề
Variant cần có giá, nhưng một giá vô hướng duy nhất là không đủ cho POS. Cùng một variant có thể mang một giá gốc cộng với các override và discount theo khoảng ngày, theo tầng số lượng và theo ngữ cảnh (channel, thời gian, thời lượng dịch vụ), và giá áp dụng phải resolve một cách xác định tại thời điểm bán. Hiện tại danh mục (commerce) có variant nhưng chưa có container giá hạng nhất, và chưa có engine để tính giá mà một dòng giỏ hàng thực sự phải trả - nếu không, logic giá sẽ rò rỉ vào mọi kênh bán và trôi dạt theo thời gian.
Phần tăng trưởng này xây phía giá của danh mục: một fare set liên kết một-một với mỗi variant, các fare bên trong (một fare mặc định cộng với các override/discount group có context rule), một pricing engine theo snapshot resolve fare áp dụng tại thời điểm bán, và một CDC seam để khi tạo hoặc cập nhật một product variant trong commerce sẽ tự động provision và làm giàu fare set của nó trong pricing.
2. Mục tiêu & Không làm
Mục tiêu
- Fare-set CRUD: mỗi variant mang đúng một fare set với ít nhất một fare mặc định (URD-FAR-001…002).
- Fare phân tầng: khoảng ngày hiệu lực, cửa sổ số lượng min/max, và các fare group phân cấp cha-con với context rule (channel, thời gian, số lượng, thời lượng dịch vụ) (URD-FAR-005…007).
- Pricing engine theo snapshot: fare calculator + tax calculator + một pricing v2 endpoint resolve fare áp dụng tại thời điểm bán (override → discount → default) (URD-FAR-003…004).
- CDC seam: tạo/cập nhật product variant trong commerce cascade sang pricing để tạo và làm giàu fare set của variant (gồm cả bundle / frequently-bought-together link).
- Fare UI ở client: panel fare set, wizard giá mặc định, chỉnh sửa tier/group, và chọn rule (channel / thời gian / thời lượng dịch vụ) hiển thị trong màn chỉnh sửa sản phẩm.
Không làm (Non-Goals)
- Tính toán discount khuyến mãi tại thời điểm pricing - chỉ có CRUD khuyến mãi; discount engine vẫn tắt (thuộc về Promotions).
- Engine chuyển đổi đơn vị và một endpoint đọc "resolved price" độc lập cho variant (URD-VAR-011/012 - Planned).
- Import danh mục hàng loạt qua CSV.
3. Success Metrics
| Chỉ số | Mục tiêu / tín hiệu |
|---|---|
| Độ phủ fare set | 100% variant mang đúng một fare set với ít nhất một fare mặc định |
| Tính xác định khi resolve | Cùng một dòng giỏ hàng + ngữ cảnh luôn resolve ra cùng một fare (override → discount → default) |
| Độ tin cậy của seam | Tạo/cập nhật variant luôn provision/làm giàu fare set; không variant nào thiếu fare set |
| Pricing tại thời điểm bán | Sale-order preview trả về giá cho mọi variant, gồm cả bundle FBT |
4. Personas & Use Case
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Owner | Đặt giá gốc và các override/discount phân tầng cho từng variant; kiểm soát giá thay đổi theo channel, thời gian, số lượng |
| Cashier / Employee | Được áp đúng giá một cách tự động tại điểm bán |
| Sale channel (hệ thống) | Resolve giá mà một dòng giỏ hàng phải trả theo ngữ cảnh bán |
Kịch bản chính: owner định nghĩa giá mặc định và các tier của variant trong màn chỉnh sửa sản phẩm → fare set được provision/làm giàu qua CDC seam → tại thời điểm bán pricing engine resolve fare áp dụng cho dòng giỏ hàng theo ngữ cảnh → sale order preview và tính theo giá đã resolve.
5. User Stories
- Là một owner, tôi muốn mỗi variant mang một fare set với giá mặc định, để không có gì bị bán mà không có giá.
- Là một owner, tôi muốn thêm các fare theo khoảng ngày và theo tầng số lượng, để giá thay đổi cho khung khuyến mãi và mua số lượng lớn.
- Là một owner, tôi muốn các fare group có context rule (channel, thời gian, thời lượng dịch vụ), để cùng một variant có giá khác nhau theo channel hoặc thời gian.
- Là một cashier, tôi muốn giá đúng được resolve tự động khi thêm variant vào giỏ, để không bao giờ phải nhập giá thủ công.
- Là một sale channel, tôi muốn một endpoint pricing preview nhận ngữ cảnh bán, để một dòng order hiển thị giá nó thực sự phải trả (gồm cả bundle FBT).
- Là một owner, tôi muốn việc tạo hoặc cập nhật variant tự động provision fare set của nó, để pricing không bao giờ trễ so với danh mục.
6. Functional Requirements
| # | Yêu cầu | URD ref |
|---|---|---|
| FR-1 | Mỗi variant có đúng một fare set; một fare set chứa ít nhất một fare mặc định | URD-FAR-001..002 |
| FR-2 | Số tiền fare phải bằng 0 hoặc dương | URD-FAR-003 |
| FR-3 | Tại thời điểm bán, engine resolve fare áp dụng: override → discount → default | URD-FAR-004 |
| FR-4 | Fare hỗ trợ khoảng ngày hiệu lực và cửa sổ số lượng min/max | URD-FAR-005..006 |
| FR-5 | Fare hỗ trợ group phân cấp cha-con (OVERRIDE / DISCOUNT) với context rule (channel, thời gian, số lượng, thời lượng dịch vụ) | URD-FAR-007 |
| FR-6 | Pricing engine theo snapshot: fare calculator + tax calculator + pricing v2 endpoint tính giá dòng từ một pricing snapshot | URD-FAR-004 |
| FR-7 | Sale-order pricing preview nhận pricing context (serviceTime, channel) và tính giá FBT qua orderProductVariantIds | URD-FAR-004 |
| FR-8 | CDC seam: tạo/cập nhật variant trong commerce provision và làm giàu fare set (gồm bundle / FBT link) | URD-VAR-003/004 |
Nội dung yêu cầu đầy đủ và tiêu chí chấp nhận nằm trong URD Sản phẩm. PRD này tham chiếu, không lặp lại.
7. Non-Functional Requirements
| Khía cạnh | Yêu cầu |
|---|---|
| Toàn vẹn dữ liệu | Mỗi variant luôn mang đúng một fare set với ít nhất một fare; số tiền fare bằng 0 hoặc dương |
| Tính xác định | Resolve fare là xác định cho một dòng + ngữ cảnh cho trước (override → discount → default; cửa sổ ngày/số lượng lọc ứng viên trước) |
| Tenancy & authz | Mọi thao tác theo phạm vi merchant (x-merchant-id); kiểm soát bằng permission của product/pricing |
| Độ chính xác | Phép tính tiền/số lượng dùng float(value, 4) |
| Tính nhất quán | Provision variant + fare set là một phần của variant aggregate một thao tác duy nhất; CDC seam đồng bộ fare set theo trạng thái variant |
| i18n | Nhãn fare ở phần hiển thị là song ngữ ({ en, vi }) |
8. UX & Luồng
Màn hình chính (trong apps/client): panel fare set, wizard giá mặc định, chỉnh sửa tier/group, và chọn rule (channel / thời gian / thời lượng dịch vụ), hiển thị trong màn chỉnh sửa sản phẩm.
9. Dữ liệu & Domain
| Entity | Vai trò |
|---|---|
FareSet | Container giá liên kết một-một với một variant; chứa các fare của variant |
Fare | Một mục giá - default, override, hoặc discount; mang số tiền, khoảng ngày, cửa sổ số lượng |
| Fare group | Phân cấp cha-con (OVERRIDE / DISCOUNT) với context rule (channel, thời gian, số lượng, thời lượng dịch vụ) |
| Pricing snapshot | Đầu vào bất biến cho calculator để resolve fare áp dụng tại thời điểm bán |
Chỉ mang tính khái niệm - schema đầy đủ và bất biến nằm trong mô hình domain pricing fares.
10. Phụ thuộc & Giả định
Phụ thuộc
- Variants (URD-VAR) - một fare set được provision cho mỗi variant qua aggregate create/update.
- CDC seam Commerce → Pricing (
@nx/commerce→@nx/pricing) - Kafka CDC của product variant kích hoạt provision fare set. - Sale orders (
@nx/sale) - tiêu thụ endpoint pricing preview với ngữ cảnh bán.
Giả định
- Mỗi variant tồn tại trong commerce trước khi fare set của nó được làm giàu (CDC seam sắp xếp thứ tự này).
- Sale channel cung cấp pricing context (serviceTime, channel) và
orderProductVariantIdscho pricing FBT.
11. Rủi ro & Câu hỏi mở
| Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Fare set có thể trễ so với variant vừa tạo/cập nhật | CDC seam provision/làm giàu fare set ở mọi thay đổi variant |
| Resolve nhập nhằng khi nhiều fare cùng đủ điều kiện | Cố định thứ tự ưu tiên override → discount → default; cửa sổ ngày/số lượng lọc ứng viên trước |
| Discount khuyến mãi chưa áp tại thời điểm pricing | Ngoài phạm vi phần này; discount engine tắt, chỉ có CRUD khuyến mãi |
| Chưa có endpoint đọc resolved-price độc lập cho variant | Planned (URD-VAR-011); hiện pricing được lộ qua sale-order preview |
12. Release Plan & Launch Criteria
| Khía cạnh | Kế hoạch |
|---|---|
| Phase | P2 - xem feature catalog của URD |
| Rollout | Toàn bộ merchant; không feature flag |
| Migration | Không (fare set được provision cho mỗi variant qua CDC seam) |
| Launch criteria | Tạo/cập nhật variant provision một fare set; engine resolve override → discount → default; sale-order preview tính giá dòng (gồm FBT); số tiền fare bằng 0 hoặc dương |
| Monitoring | Kiểm tra variant-thiếu-fare-set, kết quả resolve fare, tỷ lệ lỗi pricing-preview |
13. FAQ
Giá áp dụng được chọn thế nào? Engine lọc các fare theo khoảng ngày và cửa sổ số lượng trước, rồi resolve theo thứ tự ưu tiên: một override group thắng; nếu không thì discount đủ điều kiện thấp nhất; nếu không nữa thì fare mặc định (giá gốc).
Một variant có thể tồn tại mà không có fare set không? Không - mỗi variant mang đúng một fare set với ít nhất một fare mặc định, được provision qua CDC seam.
Khuyến mãi có được áp tại thời điểm pricing không? Chưa - CRUD khuyến mãi đã có, nhưng discount calculation engine đang tắt, nên discount khuyến mãi không được áp tự động.
Pricing chạy ở đâu tại thời điểm bán? Sale order gọi endpoint pricing preview với pricing context (serviceTime, channel); bundle FBT được tính giá qua orderProductVariantIds.
OVERRIDE và DISCOUNT fare group khác nhau thế nào? OVERRIDE group thay thế hẳn giá; DISCOUNT group giảm giá. Cả hai đều có thể mang rule channel / thời gian / số lượng / thời lượng dịch vụ.
Tài liệu liên quan
- URD: Sản phẩm - Fares / Pricing · Variants
- Xây trên: Variants
- PRD liên quan: Promotions
- Module: Sản phẩm - tổng quan + truy vết
- Developer: pricing fares · @nx/pricing · @nx/commerce