@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
76 lines (65 loc) • 2.61 kB
text/typescript
'use server';
import { GpmPay } from '@gpmpay/sdk';
import { GpmPayPermissionError } from '@gpmpay/sdk';
import {
buildPaymentInstructions,
type PaymentInstructions as SdkPaymentInstructions,
} from '@gpmpay/sdk/vietqr';
// Module scope: constructed once per server instance. Throws at import time if
// GPMPAY_API_TOKEN is missing — which is what you want, so a misconfigured
// deploy fails immediately rather than at the first checkout.
const client = new GpmPay({ apiToken: process.env.GPMPAY_API_TOKEN! });
/**
* Only these fields cross to the browser. Never the client, never the token.
*
* Derived from the SDK's own return type rather than re-declared, so a change
* to `buildPaymentInstructions` breaks this build instead of silently drifting.
*/
export type CheckoutPayment = SdkPaymentInstructions & { code: string };
/**
* Mint the payment code and build the QR.
*
* GPM Pay has no "create order" endpoint — the code is yours, derived from the
* cart so a double-submitted form produces the *same* code rather than a second
* pending payment. Store it against the cart before returning.
*/
export async function createPayment(
cartId: string,
amountVnd: number,
): Promise<CheckoutPayment> {
const amount = Math.round(amountVnd); // must be an integer number of VND
// Keep it A-Z0-9 and short: VietQR truncates the description to 25 chars and
// some banks prepend their own prefix to the transfer content.
const code = `ORD${cartId}`;
try {
const account = await client.bankAccounts.retrieve(
process.env.GPMPAY_BANK_ACCOUNT_ID!,
);
const instructions = buildPaymentInstructions({
bankAccount: account,
amount,
transferContent: code,
});
// await db.carts.update(cartId, { paymentCode: code, status: 'AWAITING_PAYMENT' });
return { code, ...instructions };
} catch (error) {
if (error instanceof GpmPayPermissionError) {
console.error(
`GPM Pay token rejected (${error.reason}): ${error.missingScope ?? 'n/a'}`,
);
throw new Error('Payment is misconfigured — please contact support.');
}
throw error;
}
}
/**
* Let the page poll your own database while it waits.
*
* There is nothing to poll on the GPM Pay side: payment status lives in your
* system, written by the webhook route. Polling GPM Pay's transaction list
* would work but costs a round-trip per check and races with the webhook.
*/
export async function getPaymentStatus(code: string) {
// return db.carts.findByPaymentCode(code);
return { code, status: 'PENDING' as const, paidAt: null };
}