Skip to content

PRD: Vòng đời thanh toán & nhà cung cấp

ModuleThanh toán & Giao dịch (CORE-08)PRD IDPRD-PAY-001
StatusShippedOwnerPayment squad
Date2026-05-27Versionv1.0
Packages@nx/payment · @nx/sale · @nx/finance · @nx/coreURDPAY · 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/payment riê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-success chạ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

MetricMục tiêu / tín hiệu
Độ trễ trạng tháiThu ngân thấy pending → paid/failed/expired gần như tức thời (không độ trễ polling)
IdempotencyKế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ờiCá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ấpKhô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ụ CASHCả đườ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

PersonaMục tiêu trong tính năng này
Thu ngânThu 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
OwnerKế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

#RequirementURD ref
FR-1Package 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-2Khi 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-successURD-PAY-002 · URD-PAY-005
FR-3Trạng thái thanh toán được phát trực tiếp đến thu ngân qua WebSocket, thay thế pollingURD-PAY-003
FR-4Mộ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-5Owner subscribe các webhook endpoint theo loại sự kiện, có retry khi gửi thất bạiURD-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ấpURD-PAY-002
FR-7Owner kết nối một nhà cung cấp (VNPAY QR MMS, PhonePOS) theo từng merchant qua một configuration controllerURD-PRV-001
FR-8Thô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-9Thô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ácURD-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

AreaRequirement
IdempotencyMộ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ờiKhông ràng buộc trực tiếp sale↔mq-pay; subscribers chỉ phản ứng qua webhook / Kafka payment-success
Real-timeTrạng thái đến thu ngân qua một WebSocket push, không phải vòng polling
Tenancy & authzMọ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ậtThô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)
i18nNhã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

EntityVai trò
PaymentBả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 subscriptionCấu hình endpoint outbound theo từng merchant, theo loại sự kiện, có retry khi thất bại
Payment configurationThô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-successFan-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à subscriber payment-success chí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ỏiGiảm thiểu / trạng thái
Notification gửi lại từ nhà cung cấp bị áp dụng hai lầnKế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 gianRanh 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 merchantMã 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ấpHoãn lại - Planned (URD-PAY-006, URD §7)

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 - cả PAYPRV (theo §5 feature catalog của URD)
RolloutTấ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
MigrationPackage @nx/payment mới; seam payment-success chuyển BullMQ → Kafka
Launch criteriaKế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
MonitoringTỷ 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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.