PRD: Tích điểm
| Module | Khách hàng thân thiết (EXT-01) | PRD ID | PRD-PTS-001 |
| Status | Planned | Owner | Loyalty squad |
| Date | 2026-03-23 | Version | v0.1 |
| Packages | @nx/sale · @nx/core | URD | PTS |
TL;DR
Cho phép merchant tự động biến mỗi đơn đã thanh toán và gắn với khách hàng thành điểm thưởng - số dư của khách tăng ngay khi thanh toán hoàn tất, theo tỷ lệ quy đổi riêng cho từng merchant. Mỗi lần cộng được ghi nhận một lần cho mỗi đơn dưới dạng một point transaction kiểm toán được, nên khách quay lại thấy điểm tích lũy mà không cần nhập tay, còn merchant có một sổ điểm xác minh được.
1. Context & Problem
Module Khách hàng thân thiết biến các lần mua hoàn tất thành điểm để khách quay lại thường xuyên hơn. Trước khi có tích điểm, đơn đã thanh toán không để lại dấu vết loyalty nào: không có sổ điểm, không tích lũy số dư, và không có liên kết từ đơn tới điểm mà nó sinh ra. Khách không được thưởng cho chi tiêu, còn merchant không có nền tảng để xây dựng đổi điểm, hạng thành viên hay phần thưởng.
Tích điểm là phần đầu tiên (tính năng PTS, giai đoạn P1): một đơn gắn với khách hàng khi hoàn tất thanh toán sẽ tự động cộng điểm vào số dư của khách, một lần, theo tỷ lệ quy đổi riêng cho từng merchant. Nó xây dựng trên định danh khách hàng sẵn có (nơi giữ số dư) và luồng đơn hàng/thanh toán (nơi báo hiệu hoàn tất), và là điều kiện tiên quyết cho mọi năng lực loyalty sau này.
2. Goals & Non-Goals
Goals
- Cộng điểm tự động khi một đơn gắn với khách hàng hoàn tất thanh toán, không cần nhập tay.
- Làm cho lần cộng idempotent theo từng đơn để một payload thanh toán gửi lại hay trùng lặp không bao giờ cộng đôi.
- Đọc tỷ lệ quy đổi từ cấu hình riêng cho từng merchant, dùng giá trị mặc định khi chưa đặt.
- Ghi mỗi lần cộng dưới dạng một
PointTransaction(loạiEARN) và tăng số dư điểm của khách trong một thao tác duy nhất. - Cung cấp một API liệt kê point transaction chỉ đọc (find / findById / findOne / count), scope theo merchant.
Non-Goals
- Đổi điểm, hạng thành viên và danh mục phần thưởng - thuộc tính năng
RDM(giai đoạn P2, URD-RDM). - Quỹ điểm dùng chung / liên merchant (Non-Goal của URD).
- Gửi tin nhắn marketing khi tích điểm (thuộc Marketing).
- Cộng điểm cho đơn ẩn danh (ràng buộc C-01 - chỉ khách đã định danh mới tích lũy).
3. Success Metrics
| Metric | Mục tiêu / tín hiệu |
|---|---|
| Độ phủ | 100% đơn đã thanh toán gắn với khách hàng đều cộng điểm (khi tỷ lệ dương) |
| Idempotency | Không cộng đôi - nhiều nhất một transaction EARN cho mỗi đơn |
| Độ chính xác | pointBalance bằng tổng các transaction EARN của khách |
| Độ trễ | Việc cộng hoàn tất trong luồng xác nhận thanh toán mà không làm chậm việc hoàn tất đơn |
4. Personas & Use Cases
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Customer | Tự động tích điểm khi mua hàng và thấy số dư tăng lên |
| Owner | Đặt tỷ lệ tích và tin rằng mỗi đơn đã thanh toán đều tích lũy đúng |
| Manager | Xem hoạt động điểm và số dư của thành viên |
Core scenarios: một đơn gắn với khách hàng hoàn tất thanh toán → hệ thống đọc tỷ lệ quy đổi của merchant → tính điểm từ tổng đơn → ghi một transaction EARN và tăng số dư của khách, một lần cho mỗi đơn.
5. User Stories
- Là một customer, tôi muốn điểm được cộng tự động vào số dư khi đơn được thanh toán, để được thưởng cho chi tiêu mà không cần làm gì.
- Là một owner, tôi muốn tỷ lệ tích được cấu hình riêng cho từng merchant, để kiểm soát cách chi tiêu quy đổi thành điểm.
- Là một owner, tôi muốn một payload thanh toán trùng lặp không bao giờ cộng đôi, để sổ điểm luôn đáng tin.
- Là một manager, tôi muốn liệt kê các point transaction của một khách, để xem số dư của họ được tích lũy như thế nào.
6. Functional Requirements
| # | Yêu cầu | URD ref |
|---|---|---|
| FR-1 | Cộng điểm khi một đơn gắn với khách hàng hoàn tất thanh toán; cùng một logic chạy trên cả hai đường hoàn tất thanh toán (order-payment và check-payment) | URD-PTS-001 |
| FR-2 | Việc tích là idempotent theo từng đơn - dừng sớm nếu đơn đã được cộng điểm | URD-PTS-002 |
| FR-3 | Tỷ lệ quy đổi đọc từ cấu hình riêng cho từng merchant (POINT_CONVERSION_RATE); dùng mặc định khi chưa đặt | URD-PTS-003 |
| FR-4 | points = floor(orderTotal / conversionRate); tỷ lệ không dương hoặc điểm bằng không thì bỏ qua (ghi log, không cộng) | URD-PTS-001 |
| FR-5 | Trong một transaction: chèn một PointTransaction (loại EARN, với points, conversionRate, saleOrderId) và tăng pointBalance của khách; roll back khi lỗi | URD-PTS-001..002 |
| FR-6 | API liệt kê point transaction chỉ đọc (find / findById / findOne / count), scope theo merchant | URD-PTS-001 |
Toàn văn yêu cầu và tiêu chí chấp nhận nằm trong URD Khách hàng thân thiết. PRD này tham chiếu chúng thay vì lặp lại.
7. Non-Functional Requirements
| Area | Yêu cầu |
|---|---|
| Data integrity | Lệnh chèn PointTransaction và việc tăng pointBalance được ghi trong một transaction - không thay đổi số dư mà thiếu một bản ghi sổ điểm tương ứng |
| Idempotency | Nhiều nhất một lần cộng EARN cho mỗi đơn, đảm bảo bằng kiểm tra tồn tại trên saleOrderId |
| Tenancy & authz | Mọi thao tác scope theo merchant (x-merchant-id); API liệt kê được gác bởi permission của sale (JWT / Basic auth) |
| Độ chính xác | Tính toán tiền / điểm dùng float(value, 4); điểm được làm tròn xuống đơn vị nguyên |
| Performance | Việc cộng chạy bên trong luồng xác nhận thanh toán mà không làm chậm việc hoàn tất đơn |
| i18n | Nhãn/trạng thái hướng người dùng là song ngữ ({ en, vi }) |
8. UX & Flows
Tích điểm không có UI riêng - nó chạy phía server khi thanh toán hoàn tất. Số dư đã tích hiện ra trong hồ sơ khách hàng (apps/client); point transaction truy cập được qua API liệt kê chỉ đọc.
9. Data & Domain
| Entity | Vai trò |
|---|---|
PointTransaction | Bản ghi sổ chỉ thêm - loại EARN, points, conversionRate, saleOrderId, scope theo merchant |
Customer.pointBalance | Số dư đang chạy, được tăng trong một thao tác duy nhất cùng mỗi lần cộng |
Configuration (POINT_CONVERSION_RATE) | Dòng tỷ lệ riêng cho từng merchant (group SYSTEM, principal = merchant, status ACTIVATED); dùng mặc định khi vắng |
Chỉ ở mức khái niệm - toàn bộ schema và bất biến nằm trong tài liệu customer-points lập trình viên và domain model của sale.
10. Dependencies & Assumptions
Phụ thuộc vào
- Định danh khách hàng (Khách hàng) - giữ
pointBalancemà lần cộng làm tăng. - Đơn hàng / thanh toán (Đơn hàng) - đơn hoàn tất trên đường thanh toán là điều kiện kích hoạt tích điểm.
- Cấu hình riêng cho từng merchant (
@nx/core) - cung cấpPOINT_CONVERSION_RATE.
Giả định
- Đơn được gắn với một khách hàng đã định danh (đơn ẩn danh không tích, C-01).
- Có tỷ lệ quy đổi được cấu hình, hoặc áp dụng mặc định.
- Các webhook hoàn tất thanh toán kích hoạt đáng tin cho mỗi đơn đã thanh toán.
11. Risks & Open Questions
| Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Payload thanh toán trùng lặp / gửi lại có thể cộng đôi | Idempotent theo từng đơn - kiểm tra tồn tại trên saleOrderId trước khi cộng (C-02) |
| Lần cộng và việc tăng số dư có thể lệch nhau khi lỗi một phần | Cả hai ghi trong một transaction; roll back cùng nhau khi lỗi |
| Tỷ lệ cấu hình sai (không dương) có thể cộng giá trị rác | Tỷ lệ không dương hoặc điểm bằng không thì bỏ qua và ghi log, không bao giờ cộng |
| Hoàn tiền / hủy sau khi đã tích | Mở: định nghĩa đường thu hồi / đảo điểm đã tích |
12. Release Plan & Launch Criteria
| Aspect | Kế hoạch |
|---|---|
| Phase | P1 (nền tảng) - xem feature catalog của URD |
| Rollout | Tất cả merchant; không có feature flag |
| Migration | Không (entity PointTransaction mới; tỷ lệ quy đổi lấy từ cấu hình) |
| Launch criteria | Đơn gắn khách hàng đã thanh toán cộng điểm một lần; số dư khớp sổ EARN; payload trùng lặp không cộng đôi; tỷ lệ đọc theo từng merchant |
| Monitoring | Lượng cộng điểm theo merchant, phát hiện cộng đôi (đơn có >1 EARN), kiểm tra nhất quán số dư so với sổ điểm |
13. FAQ
Điểm được cộng vào lúc nào? Khi một đơn gắn với khách hàng hoàn tất thanh toán - cùng một logic chạy dù xác nhận đến qua đường order-payment hay check-payment.
Cùng một đơn có cộng điểm hai lần được không? Không - việc tích là idempotent theo từng đơn. Nếu đã tồn tại một transaction EARN cho đơn, lần cộng bị bỏ qua.
Tỷ lệ tích đến từ đâu? Từ cấu hình POINT_CONVERSION_RATE riêng cho từng merchant; áp dụng mặc định khi chưa đặt.
Điểm được tính như thế nào? points = floor(orderTotal / conversionRate). Tỷ lệ không dương hoặc kết quả bằng không thì bỏ qua (ghi log, không cộng).
Đơn ẩn danh có tích điểm không? Không - chỉ khách đã định danh mới tích lũy (ràng buộc C-01).
Đã đổi điểm được chưa? Chưa trong phần này. Đổi điểm, hạng thành viên và danh mục phần thưởng là tính năng RDM riêng (giai đoạn P2, URD-RDM).
References
- URD: Khách hàng thân thiết - Tích điểm
- PRD liên quan: Đổi điểm & Hạng (
RDM, kế hoạch) - xem URD-RDM - Module: Khách hàng thân thiết - tổng quan + truy vết
- Developer: @nx/sale customer-points