Payment Flows
Starting a payment always follows the same pattern: build the gateway's payment data, call the gateway, then act on the typed result it returns. Each flow has its own result class with exactly the fields that flow needs.
| Result | What you do | Returned by |
|---|---|---|
RedirectPayment | Redirect the customer to payment.url | kbzPay().pwa(), waveMoney().initiate() |
FormPayment | Redirect to this.payments.autoSubmitUrl(payment) | ayaPay().initiate(), cyberSource().initiate() |
QrPayment | Show the QR to the customer | kbzPay().qr(), yomaMmqr().initiate(), yomaMmqr().renewQr() |
AppPayment | Return the signed payload to your mobile app | kbzPay().app() |
The samples run in a controller that injects MyanmarPaymentsService as this.payments. The customer finishing on the gateway's side is never proof of payment. Fulfill orders from the verified callback or a status check.
Redirect Payments
Here is the flow with the KBZ Pay PWA; Wave Money works the same way with its own payment page.
The gateway hosts its own payment page. Send the customer there.
import { MyanmarPaymentsService } from '@laranex/nestjs-myanmar-payments';
import { Controller, Param, Post, Redirect } from '@nestjs/common';
import { OrdersService } from '../orders/orders.service';
@Controller('checkout')
export class CheckoutController {
constructor(
private readonly payments: MyanmarPaymentsService,
private readonly orders: OrdersService,
) {}
@Post(':id/kbz-pay')
@Redirect()
async kbzPay(@Param('id') id: string): Promise<{ url: string }> {
const order = await this.orders.findOrFail(id);
const payment = await this.payments.kbzPay().pwa({
orderId: `ORDER_${order.id}`,
amount: 10000,
callbackUrl: 'https://shop.test/payments/kbz/callback',
});
return { url: payment.url };
}
}payment.gatewayReference holds the gateway's id for the attempt (KBZ prepay_id, Wave transaction_id).
Form Payments
Here is the flow with AYA Pay; CyberSource works the same way with its hosted checkout.
The gateway expects the customer's browser to POST a signed form. The package hosts a page that renders the form and submits it immediately, so a redirect is enough:
const payment = this.payments.ayaPay().initiate(data);
return { url: this.payments.autoSubmitUrl(payment) }; // with @Redirect()autoSubmitUrl() returns an encrypted, expiring link to the module's form route. See Configuration.
To render the form yourself, for example with your own loading state, return toHtml() or use action, fields and enctype:
import { Header, Param, Post } from '@nestjs/common';
@Post(':id/aya-pay')
@Header('Content-Type', 'text/html; charset=utf-8')
async ayaPay(@Param('id') id: string): Promise<string> {
// ...
return this.payments.ayaPay().initiate(data).toHtml();
}Post the fields unchanged: they are signed. fields is a list in signing order; payment.values() returns them as an object and payment.field(name) reads one.
QR Payments
Here is the flow with Yoma MMQR, whose QR expires after 120 seconds; a KBZ Pay QR follows the same steps without renewals.
Gateways return QR codes in two shapes:
| Property | Gateway | Use it as |
|---|---|---|
qrString | KBZ Pay | A payload: encode it into a QR image with any QR library |
qrImage | Yoma MMQR | A base64 image: display it as is, e.g. with qrImageDataUri() |
const payment = await this.payments.yomaMmqr().initiate(data);Pass payment.qrImageDataUri() and payment.expiresAt to your view. expiresAt is set when the gateway limits how long the QR is payable, and reference holds the id used for status checks (Yoma refLabel).
App Payments
Here is the flow with the KBZ Pay mobile SDK.
The KBZ Pay mobile SDK needs a signed order string. Return it to your app, which passes it to KBZPay.startPay():
const payment = await this.payments.kbzPay().app(data);
// JSON: orderId, orderInfo, sign, signType
return payment;Returning the AppPayment from a handler serializes it with toJSON(), which leaves out raw. The SDK's own result only means the payment screen closed; rely on the callback or kbzPay().status().
See Results for every property.