PRD: Vòng đời thanh toán & nhà cung cấp
| Module | Thanh toán & Giao dịch (CORE-08) | PRD ID | PRD-PAY-001 |
| Status | Shipped | Owner | Payment squad |
| Date | 2026-05-27 | Version | v1.0 |
| Packages | @nx/payment · @nx/sale · @nx/finance · @nx/core | URD | PAY · PRV |
TL;DR
Cho phép merchant thu tiền qua một nhà cung cấp thanh toán và theo dõi kết quả trực tiếp - một package
@nx/paymentriêng ingest kết quả từ nhà cung cấp, đẩy trạng thái đến thu ngân qua WebSocket, và thông báo cho sale ngay khi thanh toán thành công. Owner kết nối thông tin xác thực VNPAY QR MMS / PhonePOS theo từng merchant, lưu mã hóa và che một phần. Kết quả: một vòng thanh toán nhanh, tách rời (decoupled) mà bất kỳ subscriber nào cũng có thể phản ứng, không còn việc thu ngân phải polling và không còn rò rỉ thông tin xác thực giữa các merchant.
1. Context & Problem
Kết quả thanh toán ban đầu đi qua một event emitter trong tiến trình (in-process) khiến sale bị ràng buộc chặt với đường money-queue, trong khi thu ngân phải polling để lấy trạng thái thanh toán QR. Vòng lấy trạng thái chậm và ranh giới sale ↔ payment dễ vỡ: một kết quả thanh toán không thể được tiêu thụ bởi một subscriber tùy ý, và một notification bị gửi lại từ nhà cung cấp có nguy cơ bị áp dụng hai lần. Ngoài ra cũng chưa có nơi lưu thông tin xác thực nhà cung cấp theo từng merchant, nên việc kết nối VNPAY QR MMS hay PhonePOS không thể làm an toàn hoặc giới hạn theo một merchant.
Increment này tách ra một package @nx/payment riêng, thay thế kiểu emitter/polling bằng webhook cộng với một WebSocket push, và nối thông tin xác thực nhà cung cấp theo từng merchant qua một configuration controller để owner có thể kết nối một nhà cung cấp cho mỗi merchant.
2. Goals & Non-Goals
Goals
- Một package
@nx/paymentđộc lập ingest kết quả từ nhà cung cấp và cập nhật trạng thái thanh toán. - Webhook subscriptions theo từng merchant thay thế event emitter sale↔money-queue; fan-out
payment-successchạy trên Kafka. - Trạng thái thanh toán trực tiếp đến thu ngân qua WebSocket, thay thế polling.
- Áp dụng kết quả từ nhà cung cấp một cách idempotent để một notification gửi lại không gây hiệu ứng trùng lặp.
- Thông tin xác thực nhà cung cấp theo từng merchant (VNPAY QR MMS, PhonePOS) lưu mã hóa, che một phần trong response, và hiển thị trong cài đặt BO/client.
Non-Goals
- Hoàn tiền / đảo có cấu trúc trở lại nhà cung cấp gốc - Planned (
URD-PAY-006, URD §7). - SoftPOS / NFC chạm-để-trả và thêm các ví điện tử (Momo, ZaloPay) - Non-Goals của URD.
- Tài khoản/ví, vouchers/sổ cái, danh mục (
WAL/VCH/CAT) - thuộc PRD Ví, Vouchers & Sổ cái. - UX thanh toán / chia nhỏ thanh toán (split-payment) - thuộc module Orders / sale.
3. Success Metrics
| Metric | Mục tiêu / tín hiệu |
|---|---|
| Độ trễ trạng thái | Thu ngân thấy pending → paid/failed/expired gần như tức thời (không độ trễ polling) |
| Idempotency | Kết quả từ nhà cung cấp gửi lại không tạo hiệu ứng trùng lặp nào |
| Tách rời | Các consumer payment-success (sale, finance, inventory) phản ứng qua Kafka mà không cần ràng buộc trực tiếp sale↔mq-pay |
| Cô lập nhà cung cấp | Không tái sử dụng thông tin xác thực giữa các merchant; thông tin xác thực luôn được che trong response |
| Hội tụ CASH | Cả đường CASH và đường nhà cung cấp đều fire webhook, nên trạng thái phía sau đồng nhất |
4. Personas & Use Cases
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Thu ngân | Thu một khoản và thấy nó được giải quyết trực tiếp, không phải refresh thủ công |
| Owner | Kết nối một nhà cung cấp thanh toán theo từng merchant; tin rằng các sự kiện tiền được ghi xuống phía sau |
| Sale / subscriber | Được thông báo ngay khi thanh toán thành công để đơn được tất toán |
Core scenarios: owner kết nối VNPAY QR MMS / PhonePOS cho một merchant → thu ngân thu một khoản → nhà cung cấp báo kết quả → trạng thái thanh toán cập nhật idempotent → thu ngân thấy trực tiếp qua WebSocket và sale được thông báo qua webhook / payment-success trên Kafka.
5. User Stories
- Là một thu ngân, tôi muốn trạng thái thanh toán cập nhật trực tiếp, để tôi không phải polling hay refresh mới biết một thanh toán QR đã thành công hay chưa.
- Là một subscriber (sale), tôi muốn được thông báo ngay khi thanh toán thành công, để tôi tất toán đơn mà không phải sở hữu đường đi của nhà cung cấp.
- Là một owner, tôi muốn một kết quả gửi lại từ nhà cung cấp chỉ được áp dụng một lần, để một mạng chập chờn không bao giờ tính phí hay ghi sổ hai lần.
- Là một owner, tôi muốn kết nối VNPAY QR MMS / PhonePOS theo từng merchant, để mỗi merchant thu tiền bằng thông tin xác thực của chính nó.
- Là một owner, tôi muốn thông tin xác thực nhà cung cấp được lưu mã hóa và che một phần, để không ai - kể cả một merchant khác - đọc hay tái sử dụng được.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Package thanh toán ingest kết quả từ nhà cung cấp và cập nhật trạng thái thanh toán (pending → paid / failed / expired) | URD-PAY-001 |
| FR-2 | Khi thành công, hệ thống thông báo cho subscribers (ví dụ sale tất toán đơn) qua webhook; fan-out chạy trên Kafka payment-success | URD-PAY-002 · URD-PAY-005 |
| FR-3 | Trạng thái thanh toán được phát trực tiếp đến thu ngân qua WebSocket, thay thế polling | URD-PAY-003 |
| FR-4 | Một kết quả từ nhà cung cấp chỉ được áp dụng một lần dù bị gửi lại (idempotent) | URD-PAY-004 |
| FR-5 | Owner subscribe các webhook endpoint theo loại sự kiện, có retry khi gửi thất bại | URD-PAY-005 |
| FR-6 | Đường CASH cũng fire webhook để trạng thái phía sau hội tụ với thanh toán qua nhà cung cấp | URD-PAY-002 |
| FR-7 | Owner kết nối một nhà cung cấp (VNPAY QR MMS, PhonePOS) theo từng merchant qua một configuration controller | URD-PRV-001 |
| FR-8 | Thông tin xác thực nhà cung cấp lưu mã hóa và không bao giờ hiển thị đầy đủ (che trong response) | URD-PRV-002 |
| FR-9 | Thông tin xác thực giới hạn theo từng merchant để một merchant không dùng được của merchant khác | URD-PRV-003 |
Toàn văn requirement và acceptance criteria nằm trong URD Thanh toán & Giao dịch. PRD này tham chiếu chứ không lặp lại.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Idempotency | Một kết quả từ nhà cung cấp mang một key để gửi lại là no-op; không thay đổi trạng thái hay ghi sổ trùng lặp |
| Tách rời | Không ràng buộc trực tiếp sale↔mq-pay; subscribers chỉ phản ứng qua webhook / Kafka payment-success |
| Real-time | Trạng thái đến thu ngân qua một WebSocket push, không phải vòng polling |
| Tenancy & authz | Mọi thao tác giới hạn theo từng merchant (x-merchant-id); cấu hình nhà cung cấp do owner kiểm soát |
| Bảo mật | Thông tin xác thực nhà cung cấp mã hóa khi lưu, che trong response (constraint C-03 của URD) |
| i18n | Nhãn/trạng thái hiển thị cho người dùng là song ngữ ({ en, vi }) |
8. UX & Flows
Các màn hình chính: cấu hình thông tin xác thực nhà cung cấp trong cài đặt apps/bo (đã chuyển từ apps/client), và bề mặt trạng thái thanh toán QR trực tiếp trong sale-renderer (do WebSocket đẩy, không polling).
9. Data & Domain
| Entity | Vai trò |
|---|---|
| Payment | Bản ghi thanh toán mà kết quả từ nhà cung cấp điều khiển trạng thái (pending → paid / failed / expired) |
| Webhook subscription | Cấu hình endpoint outbound theo từng merchant, theo loại sự kiện, có retry khi thất bại |
| Payment configuration | Thông tin xác thực nhà cung cấp theo từng merchant (VNPAY QR MMS, PhonePOS), mã hóa + che |
Sự kiện payment-success | Fan-out trên Kafka được tiêu thụ bởi sale / finance / inventory |
Chỉ ở mức khái niệm - schema đầy đủ và bất biến nằm trong domain model của payment.
10. Dependencies & Assumptions
Depends on
- Orders / sale (
@nx/sale) - một đơn sale kích hoạt thanh toán; sale là subscriberpayment-successchính. - Finance (
@nx/finance) - tiêu thụpayment-successđể tự ghi sổ tiền xuống phía sau. - Core (
@nx/core) - seam sự kiện/Kafka chung và phần hạ tầng cấu hình. - Một nhà cung cấp thanh toán (VNPAY QR MMS, PhonePOS) - nguồn bên ngoài của kết quả thanh toán.
Assumptions
- Merchant đã kết nối ít nhất một nhà cung cấp trước khi thu qua nhà cung cấp đó.
- Kafka sẵn sàng cho fan-out
payment-success. - Client của thu ngân duy trì một kết nối WebSocket để nhận trạng thái trực tiếp.
11. Risks & Open Questions
| Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Notification gửi lại từ nhà cung cấp bị áp dụng hai lần | Kết quả áp dụng idempotent qua một key; gửi lại là no-op |
| Sale và payment ràng buộc lại theo thời gian | Ranh giới chỉ là webhook / Kafka; không có emitter trong tiến trình quay lại |
| Trạng thái CASH vs nhà cung cấp lệch nhau | Đường CASH cũng fire webhook nên trạng thái phía sau hội tụ |
| Rò rỉ thông tin xác thực giữa các merchant | Mã hóa khi lưu, che trong response, giới hạn theo từng merchant (C-03) |
| Hoàn tiền có cấu trúc qua nhà cung cấp | Hoãn lại - Planned (URD-PAY-006, URD §7) |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 - cả PAY và PRV (theo §5 feature catalog của URD) |
| Rollout | Tất cả merchant; cấu hình nhà cung cấp gắn với việc owner kết nối một nhà cung cấp |
| Migration | Package @nx/payment mới; seam payment-success chuyển BullMQ → Kafka |
| Launch criteria | Kết quả nhà cung cấp → cập nhật trạng thái → WebSocket push trực tiếp → sale được thông báo, đã kiểm chứng end-to-end; gửi lại được chứng minh idempotent; thông tin xác thực mã hóa + che |
| Monitoring | Tỷ lệ gửi/retry webhook, độ trễ consumer payment-success, số lần idempotency từ chối, sức khỏe kết nối WebSocket |
13. FAQ
Vì sao tách package @nx/payment riêng? Để tách sale khỏi đường money-queue. Kết quả thanh toán giờ đi qua webhook và Kafka, nên bất kỳ subscriber nào cũng phản ứng được mà không cần ràng buộc chặt trong tiến trình.
Thu ngân thấy trạng thái mà không polling bằng cách nào? Một WebSocket push phát mỗi thay đổi trạng thái trực tiếp đến thu ngân, thay thế vòng polling cũ.
Chuyện gì xảy ra nếu nhà cung cấp gửi cùng một kết quả hai lần? Không gì cả ở lần thứ hai - kết quả được áp dụng idempotent, nên một notification gửi lại không gây hiệu ứng trùng lặp.
Thanh toán CASH có bỏ qua webhook không? Không - đường CASH cũng fire webhook để trạng thái phía sau hội tụ với thanh toán qua nhà cung cấp.
Một merchant khác có dùng được thông tin xác thực nhà cung cấp của tôi không? Không - thông tin xác thực được mã hóa, che trong response, và giới hạn theo từng merchant.
Tôi có hoàn tiền qua nhà cung cấp ở đây được không? Không trong increment này - hoàn tiền có cấu trúc qua nhà cung cấp là Planned (URD-PAY-006, URD §7).
References
- URD: Thanh toán & Giao dịch - Payment Lifecycle · Provider Credentials
- Related PRD: Ví, Vouchers & Sổ cái
- Module: Thanh toán & Giao dịch - overview + traceability
- Developer: @nx/payment · @nx/finance