PRD: Điểm thưởng khi đặt hàng
| Module | Đơn hàng (CORE-07) | PRD ID | PRD-PNT-001 |
| Status | Shipped | Owner | Orders squad |
| Date | 2026-03-23 | Version | v1.0 |
| Packages | @nx/sale · @nx/core | URD | PNT |
TL;DR
Cho phép merchant tự động tưởng thưởng khách hàng quay lại: khi một đơn hàng có gắn khách hàng được thanh toán đủ, khách hàng nhận điểm thưởng tính từ tổng đơn theo tỷ lệ quy đổi của từng merchant. Mỗi lần cộng điểm được ghi nhận một lần cho mỗi đơn vào một sổ chỉ-đọc và gộp vào số dư điểm của từng khách hàng - biến mỗi giao dịch hoàn tất thành sự trung thành đo lường được mà không cần thao tác thủ công của thu ngân.
1. Context & Problem
Đơn hàng đã có thể gắn với khách hàng (URD-ORD-014) và đạt trạng thái cuối COMPLETED khi thanh toán đủ (URD-ORD-011), nhưng không có gì được tích lũy trên mối quan hệ đó - không có cách nào để tưởng thưởng việc mua lại. Những merchant muốn một cơ chế loyalty đơn giản kiểu "chi tiêu X, nhận một điểm" buộc phải theo dõi ngoài hệ thống hoặc bỏ qua, điều này không khả thi với các cửa hàng bán lẻ và F&B quy mô HKD/SME mà KICKO hướng tới.
Tính năng này lấp khoảng trống đó: một đơn hàng có gắn khách hàng và được thanh toán đủ sẽ tự động cộng điểm thưởng cho khách hàng, được kích hoạt từ luồng thanh toán thành công nên thu ngân không cần làm thêm gì. Mỗi lần cộng điểm là idempotent cho mỗi đơn và được duy trì vừa dưới dạng một bản ghi sổ bất biến vừa là số dư đang chạy theo từng khách hàng.
2. Goals & Non-Goals
Goals
- Cộng điểm cho khách hàng khi đơn được thanh toán đủ và có khách hàng gắn kèm, móc vào cả luồng thanh toán thành công của sale-order lẫn sale-check.
- Tính điểm từ tổng đơn theo
POINT_CONVERSION_RATEcủa từng merchant, làm tròn xuống thành điểm nguyên. - Đảm bảo việc cộng điểm idempotent cho mỗi đơn - phát lại cùng một đơn đã hoàn tất không cộng thêm gì.
- Theo dõi một số dư điểm theo từng khách hàng từng merchant, được tăng cùng với lần insert vào sổ trong một transaction duy nhất.
- Ghi nhận mỗi lần cộng điểm thành một
PointTransaction(EARN) chỉ-đọc mang theo đơn, số điểm và tỷ lệ quy đổi.
Non-Goals
- Tiêu / dùng điểm - chỉ có loại transaction
EARNtrong đợt này. - Hoàn lại / đảo điểm khi refund / trả hàng (luồng refund bản thân nó là Non-Goal của module).
- Hạng, hết hạn, hệ số nhân, khuyến mãi, hay điểm thưởng theo chiến dịch.
- Một UI hướng khách hàng cho số dư hoặc sổ điểm.
3. Success Metrics
| Metric | Mục tiêu / tín hiệu |
|---|---|
| Độ phủ | Đơn có gắn khách hàng và thanh toán đủ tạo ra một lần cộng điểm (khi đã cấu hình tỷ lệ quy đổi) |
| Idempotency | Không có lần cộng trùng - tối đa một bản ghi sổ cho mỗi đơn |
| Tính toàn vẹn số dư | Số dư khách hàng luôn bằng tổng các bản ghi sổ EARN của khách đó |
| Mức áp dụng | Số merchant cấu hình POINT_CONVERSION_RATE và tích lũy điểm |
4. Personas & Use Cases
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Khách hàng | Tự động tích điểm trên mỗi lần mua hoàn tất |
| Thu ngân | Thu tiền như bình thường; điểm tự cộng mà không cần thao tác thêm |
| Chủ cửa hàng | Vận hành một chương trình loyalty kiểu tích điểm đơn giản, phạm vi theo từng merchant |
Core scenarios: thu ngân gắn khách hàng vào đơn → thu thanh toán đủ (trên đơn hoặc trên check cuối) → hệ thống tính floor(orderTotal / conversionRate) điểm, ghi một bản ghi sổ EARN và tăng số dư của khách hàng - một lần, kể cả khi sự kiện thanh toán bị phát lại.
5. User Stories
- Là một khách hàng, tôi muốn tích điểm khi đơn của tôi được thanh toán đủ, để các lần mua lại tạo nên số dư mà sau này tôi có thể được tưởng thưởng.
- Là một thu ngân, tôi muốn điểm tự động cộng khi thanh toán, để tôi không phải nhớ một bước loyalty thủ công.
- Là một chủ cửa hàng, tôi muốn đặt một tỷ lệ quy đổi cho từng merchant, để tôi kiểm soát mức chi tiêu đổi được một điểm.
- Là một chủ cửa hàng, tôi muốn một đơn nhất định chỉ cộng điểm một lần, để một sự kiện thanh toán bị phát lại không thể thổi phồng số dư.
- Là một chủ cửa hàng, tôi muốn một sổ chỉ-đọc các lần cộng điểm, để mọi điểm trong số dư khách hàng đều truy ngược được về một đơn.
6. Functional Requirements
| # | Yêu cầu | URD ref |
|---|---|---|
| FR-1 | Cộng điểm cho khách hàng khi đơn trở thành thanh toán đủ và có khách hàng gắn kèm, kích hoạt từ cả luồng thanh toán thành công của sale-order lẫn sale-check | URD-PNT-001 |
| FR-2 | Tính điểm là floor(orderTotal / conversionRate); bỏ qua khi kết quả ≤ 0 | URD-PNT-001 |
| FR-3 | Đọc cấu hình POINT_CONVERSION_RATE của từng merchant; bỏ qua khi không có config hoặc tỷ lệ không dương; mặc định 1000 | URD-PNT-001 |
| FR-4 | Việc cộng điểm idempotent cho mỗi đơn - dừng sớm nếu đơn đã có lần cộng điểm | URD-PNT-002 |
| FR-5 | Ghi nhận lần cộng điểm thành một PointTransaction (EARN) mang theo saleOrderId, points và conversionRate | URD-PNT-001 |
| FR-6 | Tăng số dư điểm của khách hàng và insert bản ghi sổ trong một transaction duy nhất; rollback khi lỗi | URD-PNT-003 |
| FR-7 | Phơi bày sổ ở chế độ chỉ-đọc (find / findById / findOne / count), scope theo merchant; lần cộng điểm không bao giờ được ghi bởi client | URD-PNT-001..003 |
Toàn văn yêu cầu và tiêu chí chấp nhận nằm trong Orders URD. PRD này tham chiếu chúng thay vì lặp lại.
7. Non-Functional Requirements
| Lĩnh vực | Yêu cầu |
|---|---|
| Toàn vẹn dữ liệu | Lần insert sổ và lần tăng số dư được ghi trong một transaction - không có thay đổi số dư mà thiếu bản ghi sổ tương ứng, và ngược lại |
| Idempotency | Tối đa một lần cộng cho mỗi đơn bất kể sự kiện thanh toán thành công kích hoạt bao nhiêu lần |
| Tính bất biến | Sổ chỉ được ghi qua luồng thanh toán thành công và chỉ-đọc qua API; các bản ghi không bị client chỉnh sửa |
| Tenancy & authz | Mọi thao tác scope theo merchant (x-merchant-id); đọc sổ được gác bởi permission PointTransaction* |
| Độ chính xác | Tổng đơn đọc bằng float(value, 4); điểm là số nguyên |
| i18n | Nhãn/trạng thái hướng người dùng song ngữ ({ en, vi }) |
8. UX & Flows
Luồng cộng điểm không có UI riêng - nó chạy phía server theo sự kiện thanh toán thành công. Sổ chỉ-đọc và số dư theo từng khách hàng được tiêu thụ bởi các màn hình Orders/Khách hàng trong apps/client.
9. Data & Domain
| Entity | Vai trò |
|---|---|
PointTransaction | Bản ghi sổ bất biến (type EARN) - saleOrderId, points, conversionRate, scope theo khách hàng & merchant |
Customer.pointBalance | Số dư đang chạy theo từng khách hàng từng merchant, được tăng một thao tác duy nhất với mỗi lần cộng điểm |
Configuration (POINT_CONVERSION_RATE) | Tỷ lệ quy đổi của từng merchant (group SYSTEM, principal MERCHANT, ACTIVATED); mặc định 1000 |
SaleOrder | Nguồn của tổng đơn và liên kết khách hàng dẫn dắt việc cộng điểm |
Chỉ ở mức khái niệm - toàn bộ schema và bất biến nằm trong sale domain model.
10. Dependencies & Assumptions
Phụ thuộc vào
- Vòng đời Sale Order (URD-ORD-011) - lần cộng điểm kích hoạt tại bước chuyển sang thanh toán đủ.
- Liên kết khách hàng (URD-ORD-014) - chỉ đơn có gắn khách hàng mới tích điểm.
- Tách hoá đơn (check splitting) (URD-CHK) - thanh toán thành công của sale-check là luồng kích hoạt thứ hai.
Configuration(@nx/core) - cung cấp tỷ lệ quy đổi của từng merchant.
Giả định
- Đơn mang một tổng đã chốt tại thời điểm thanh toán thành công.
- Một merchant muốn tích điểm sẽ cấu hình
POINT_CONVERSION_RATE; nếu thiếu, lần cộng điểm bị bỏ qua (mặc định1000chỉ áp dụng khi hàng config tồn tại nhưng không có giá trị).
11. Risks & Open Questions
| Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Cộng trùng do sự kiện thanh toán bị phát lại | Dừng sớm idempotency theo khóa saleOrderId |
| Sổ và số dư có thể lệch khi lỗi cục bộ | Cả hai được ghi trong một transaction; rollback khi lỗi |
| Refund / trả hàng sau khi đã cộng điểm | Ngoài phạm vi - chưa có đảo điểm; refund là Non-Goal của module |
| Chưa có luồng redemption | Chấp nhận trong đợt này; chỉ có EARN, redemption là đợt tương lai |
| Merchant chưa cấu hình tỷ lệ thì không tích được điểm | Theo thiết kế - bỏ qua khi thiếu config hoặc tỷ lệ không dương |
12. Release Plan & Launch Criteria
| Khía cạnh | Kế hoạch |
|---|---|
| Phase | P2 - xem URD feature catalog |
| Rollout | Mọi merchant; không feature flag (vô hiệu cho đến khi merchant đặt tỷ lệ quy đổi) |
| Migration | Bảng PointTransaction mới (sale schema) và cột customer.point_balance |
| Tiêu chí ra mắt | Thanh toán đủ trên đơn có gắn khách hàng cộng đúng số điểm đã làm tròn; phát lại không cộng thêm; số dư bằng tổng sổ; đọc scope theo merchant và chỉ-đọc |
| Giám sát | Lượng cộng điểm theo từng merchant, tỷ lệ bỏ qua do idempotency, kiểm tra nhất quán số-dư-vs-sổ |
13. FAQ
Điểm được cộng chính xác khi nào? Khi một đơn có gắn khách hàng trở thành thanh toán đủ - qua luồng thanh toán thành công của sale-order hoặc của sale-check.
Một đơn tích được bao nhiêu điểm? floor(orderTotal / conversionRate), trong đó tỷ lệ quy đổi là POINT_CONVERSION_RATE của merchant (mặc định 1000). Phần dư dưới một điểm bị bỏ.
Nếu merchant chưa cấu hình tỷ lệ thì sao? Không cộng điểm - lần cộng bị bỏ qua khi thiếu config hoặc tỷ lệ không dương.
Sự kiện thanh toán phát lại có cộng điểm gấp đôi không? Không - việc cộng điểm idempotent cho mỗi đơn; lần thử thứ hai cho cùng một đơn bị dừng sớm.
Khách hàng có thể tiêu hoặc đổi điểm không? Không trong đợt này - chỉ có loại transaction EARN. Redemption, hạng và hết hạn là việc tương lai.
Client có thể ghi vào sổ không? Không - API của sổ là chỉ-đọc; lần cộng điểm chỉ được ghi bởi luồng thanh toán thành công.
References
- URD: Đơn hàng - Loyalty Points (vùng PNT)
- Liên quan: Sale Order · Check Splitting
- Module: Đơn hàng - tổng quan + traceability
- Developer: @nx/sale · domain model