Payment

canonical term: payment | reference format: PAY_{YYMMDD}_NNNN This document explains how Payments work at House of Legends — how money moves from guest to House of Legends, the OnePay integration, and the payment lifecycle.

What Is a Payment?

A Payment is the financial record of a guest’s transaction for a reservation. It captures:
  • How much — amount in VND
  • Who paid — linked to a reservation
  • How — payment provider and transaction ID
  • Whether it succeeded — status
  • When — timestamps for initiation and confirmation
Every reservation has exactly one payment (after checkout). A payment belongs to one reservation.

The Payment Flow

Here is what happens when a guest completes booking:
Guest clicks "Pay" in the Payment section

Frontend → POST /api/payments/create

Backend creates payment record with status PENDING

Frontend redirects guest to OnePay hosted checkout page

Guest completes or abandons payment at OnePay

OnePay → IPN callback → Backend updates payment to PAID

OnePay → QueryDR polling as fallback reconciliation

Guest redirected back to confirmation page
The critical thing: the payment is not confirmed until OnePay sends the IPN callback. If the guest closes the browser mid-checkout, the payment may or may not have succeeded — the system reconciles this via QueryDR polling.

Payment Verification Is Production-Only

Payment flow changes must be verified against the real production OnePay integration. Here is why:
  • Localhost cannot receive OnePay IPN callbacks
  • ngrok URLs are not whitelisted by OnePay
  • Mocked callbacks do not test the actual OnePay IPN endpoint
  • QueryDR reconciliation behavior can only be observed in production
Local development: Payment flow is developed diagnostically using redirect URLs and logs. Production verification is required before claiming a payment flow is validated.

Payment Statuses

StatusMeaning
PENDINGCheckout initiated, guest at OnePay, funds not yet captured
PAIDIPN received or QueryDR confirmed — funds captured
FAILEDOnePay declined the card, or timeout
REFUNDEDFull or partial refund issued

The PENDING Problem

A PENDING payment means OnePay has not yet confirmed the outcome. This can happen if:
  • Guest abandoned checkout mid-way
  • OnePay API timed out
  • IPN callback was delayed
The system uses QueryDR polling to reconcile PENDING payments that have not received an IPN. QueryDR asks OnePay directly: “did this transaction succeed?”

The Three Identifiers of a Payment

IdentifierExamplePurpose
id (ULID)01ARZ3NDEKTSV4RRFFQ69G5FAVMachine identity — joins, API
referencePAY_260603_0001Internal ops identifier
receiptReferenceRCP_260603_0001Guest-facing receipt identifier

Key Fields

id: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
reference: "PAY_260603_0001"
reservationId: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
amount: 5720000
currency: VND
provider: ONEPAY
providerTransactionId: "ONEPAY_TXN_123456789"
status: PAID
receiptReference: "RCP_260603_0001"
paidAt: 2026-06-01T14:32:00Z
createdAt: 2026-06-01T14:30:00Z
FieldTypeNotes
idULIDMachine identity
referencestringInternal ops identifier
reservationIdULIDFK to parent reservation
amountnumberAmount in VND
currencystringAlways VND
providerstringONEPAY
providerTransactionIdstringOnePay’s transaction reference
statusenumPENDING, PAID, FAILED, REFUNDED
receiptReferencestringGuest receipt ID
paidAtdatetimeWhen confirmed
createdAtdatetimeWhen checkout initiated

Refunds

A payment can be fully or partially refunded. Refunds are tracked separately:
refundId: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
paymentId: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
amount: 960000
reason: "Guest requested cancellation before show"
status: REFUNDED
refundedAt: 2026-06-02T10:00:00Z
The payment’s status transitions to REFUNDED when a refund is issued.

What the Frontend Must Not Do

Frontend must never recalculate payment amounts. All pricing is computed by the backend and stored in the reservation’s priceSummary. The frontend displays the totals but does not compute them. This means:
  • Never recalculate total from line items on the frontend
  • Never trust a payment amount from the frontend — always verify against backend
  • Never store payment state derived from frontend calculations

See Also