Skip to content

PRD: Payment lifecycle & providers

ModulePayment & Transaction (CORE-08)PRD IDPRD-PAY-001
StatusShippedOwnerPayment squad
Date2026-05-27Versionv1.0
Packages@nx/payment · @nx/sale · @nx/finance · @nx/coreURDPAY · PRV

TL;DR

Lets a merchant collect payments through a payment provider and watch them resolve live - a dedicated @nx/payment package ingests provider results, pushes status to the cashier over WebSocket, and notifies the sale the instant a payment succeeds. Owners connect VNPAY QR MMS / PhonePOS credentials per merchant, stored encrypted and masked. The result: a fast, decoupled payment loop that any subscriber can react to, with no more cashier polling and no shared-credential leakage between merchants.

1. Context & Problem

Payment results originally flowed through an in-process event emitter that tightly coupled sale to the money-queue path, while the cashier polled for QR payment status. The status loop was slow and the sale ↔ payment boundary brittle: a payment outcome could not be consumed by an arbitrary subscriber, and a provider's redelivered notification risked double-applying. There was also no per-merchant home for provider credentials, so connecting VNPAY QR MMS or PhonePOS could not be done safely or scoped to one merchant.

This increment carves out a dedicated @nx/payment package, replaces the emitter/polling pattern with webhooks plus a WebSocket push, and wires per-merchant provider credentials through a configuration controller so an owner can connect a provider per merchant.

2. Goals & Non-Goals

Goals

  • A standalone @nx/payment package that ingests provider results and updates payment status.
  • Webhook subscriptions per merchant replacing the sale↔money-queue event emitter; payment-success fan-out runs on Kafka.
  • Live payment status to the cashier over WebSocket, replacing polling.
  • Idempotent application of a provider result so a redelivered notification has no duplicate effect.
  • Per-merchant provider credentials (VNPAY QR MMS, PhonePOS) stored encrypted, masked in responses, and surfaced in BO/client settings.

Non-Goals

  • Structured refund / reversal back through the original provider - Planned (URD-PAY-006, URD §7).
  • SoftPOS / NFC tap-to-pay and additional e-wallets (Momo, ZaloPay) - URD Non-Goals.
  • Accounts/wallets, vouchers/ledger, categories (WAL/VCH/CAT) - owned by Wallets, Vouchers & Ledger.
  • Checkout / split-payment UX - owned by the Orders / sale module.

3. Success Metrics

MetricTarget / signal
Status latencyCashier sees pending → paid/failed/expired in near-real-time (no polling delay)
IdempotencyRedelivered provider results produce zero duplicate effects
Decouplingpayment-success consumers (sale, finance, inventory) react via Kafka without a direct sale↔mq-pay coupling
Provider isolationZero cross-merchant credential reuse; credentials always masked in responses
CASH convergenceCASH and provider paths both fire the webhook, so downstream status is uniform

4. Personas & Use Cases

PersonaGoal in this feature
CashierTake a payment and see it resolve live, without manual refresh
OwnerConnect a payment provider per merchant; trust that money events post downstream
Sale / subscriberBe notified the instant a payment succeeds so the order can settle

Core scenarios: an owner connects VNPAY QR MMS / PhonePOS for a merchant → the cashier takes a payment → the provider reports a result → payment status updates idempotently → the cashier sees it live over WebSocket and the sale is notified via webhook / payment-success on Kafka.

5. User Stories

  • As a cashier, I want the payment status to update live, so that I don't have to poll or refresh to know whether a QR payment succeeded.
  • As a subscriber (the sale), I want to be notified the moment a payment succeeds, so that I can settle the order without owning the provider path.
  • As an owner, I want a redelivered provider result to be applied only once, so that a flaky network never double-charges or double-posts.
  • As an owner, I want to connect VNPAY QR MMS / PhonePOS per merchant, so that each merchant collects with its own credentials.
  • As an owner, I want provider credentials stored encrypted and masked, so that no one - including another merchant - can read or reuse them.

6. Functional Requirements

#RequirementURD ref
FR-1The payment package ingests provider results and updates the payment's status (pending → paid / failed / expired)URD-PAY-001
FR-2On success, the system notifies subscribers (e.g. the sale settling the order) via webhook; fan-out runs on Kafka payment-successURD-PAY-002 · URD-PAY-005
FR-3Payment status is broadcast live to the cashier over WebSocket, replacing pollingURD-PAY-003
FR-4A provider result is applied only once even if redelivered (idempotent)URD-PAY-004
FR-5An owner subscribes webhook endpoints by event type, with retries on delivery failureURD-PAY-005
FR-6The CASH path also fires the webhook so downstream status converges with provider paymentsURD-PAY-002
FR-7An owner connects a provider (VNPAY QR MMS, PhonePOS) per merchant via a configuration controllerURD-PRV-001
FR-8Provider credentials are stored encrypted and never shown in full (masked in responses)URD-PRV-002
FR-9Credentials are scoped per merchant so one merchant cannot use another'sURD-PRV-003

Full requirement text and acceptance criteria live in the Payment & Transaction URD. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
IdempotencyA provider result carries a key so redelivery is a no-op; no duplicate status change or downstream post
DecouplingNo direct sale↔mq-pay coupling; subscribers react to webhook / Kafka payment-success only
Real-timeStatus reaches the cashier over a WebSocket push, not a polling loop
Tenancy & authzAll operations scoped per merchant (x-merchant-id); provider config is owner-gated
SecurityProvider credentials encrypted at rest, masked in responses (URD constraint C-03)
i18nUser-facing labels/statuses are bilingual ({ en, vi })

8. UX & Flows

Key screens: provider-credential configuration in apps/bo settings (moved from apps/client), and the live QR-payment status surface in sale-renderer (WebSocket-driven, no polling).

9. Data & Domain

EntityRole
PaymentThe payment record whose status the provider result drives (pending → paid / failed / expired)
Webhook subscriptionPer-merchant outbound endpoint config, by event type, with retry on failure
Payment configurationPer-merchant provider credentials (VNPAY QR MMS, PhonePOS), encrypted + masked
payment-success eventThe Kafka fan-out consumed by sale / finance / inventory

Conceptual only - full schema and invariants in the payment domain model.

10. Dependencies & Assumptions

Depends on

  • Orders / sale (@nx/sale) - a sale order triggers the payment; the sale is the primary payment-success subscriber.
  • Finance (@nx/finance) - consumes payment-success to auto-post money downstream.
  • Core (@nx/core) - shared event/Kafka seam and configuration plumbing.
  • A payment provider (VNPAY QR MMS, PhonePOS) - the external source of payment results.

Assumptions

  • The merchant has connected at least one provider before collecting via that provider.
  • Kafka is available for the payment-success fan-out.
  • The cashier client maintains a WebSocket connection for live status.

11. Risks & Open Questions

Risk / questionMitigation / status
Redelivered provider notification double-appliesResult applied idempotently via a key; redelivery is a no-op
Sale and payment re-coupling over timeBoundary is webhook / Kafka only; no in-process emitter back
CASH vs provider status divergenceCASH path also fires the webhook so downstream status converges
Credential leakage across merchantsEncrypted at rest, masked in responses, scoped per merchant (C-03)
Structured provider refundDeferred - Planned (URD-PAY-006, URD §7)

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 - both PAY and PRV (per URD §5 feature catalog)
RolloutAll merchants; provider config gated on the owner connecting a provider
MigrationNew @nx/payment package; payment-success seam moved BullMQ → Kafka
Launch criteriaProvider result → status update → live WebSocket push → sale notified verified end-to-end; redelivery proven idempotent; credentials encrypted + masked
MonitoringWebhook delivery / retry rate, payment-success consumer lag, idempotency-rejection count, WebSocket connection health

13. FAQ

Why a separate @nx/payment package? To decouple the sale from the money-queue path. Payment results now flow over webhooks and Kafka, so any subscriber can react without a tight in-process coupling.

How does the cashier see status without polling? A WebSocket push broadcasts each status change live to the cashier, replacing the old polling loop.

What happens if the provider sends the same result twice? Nothing the second time - the result is applied idempotently, so a redelivered notification has no duplicate effect.

Does a CASH payment skip the webhook? No - the CASH path also fires the webhook so downstream status converges with provider payments.

Can another merchant use my provider credentials? No - credentials are encrypted, masked in responses, and scoped per merchant.

Can I refund through the provider here? Not in this increment - structured provider refund is Planned (URD-PAY-006, URD §7).

References

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