@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
57 lines (43 loc) • 1.98 kB
text/typescript
import { createNextWebhookHandler } from '@gpmpay/sdk/webhooks';
// Signature verification uses node:crypto, so keep this off the edge runtime.
export const runtime = 'nodejs';
// Deliveries must never be served from a cache.
export const dynamic = 'force-dynamic';
/**
* Replace with a real table plus a unique constraint on the transaction id.
* An in-memory Set does not survive a restart or scale past one instance.
*/
const processed = new Set<string>();
function claim(transactionId: string): boolean {
if (processed.has(transactionId)) return false;
processed.add(transactionId);
return true;
}
export const POST = createNextWebhookHandler({
secret: process.env.GPMPAY_WEBHOOK_SECRET!,
onEvent: async (event) => {
const { id: transactionId, transferType, transferAmount, content } =
event.payload;
// GPM Pay retries on 10s/30s/2m/10m/1h/6h — up to 6 attempts. The same
// transaction WILL arrive more than once.
if (!claim(transactionId)) return;
// You receive every transaction on the account, outgoing ones included.
if (transferType !== 'in') return;
// The code you minted in createPayment(). Match with a regex, not `===`:
// banks normalize the content and some prepend their own prefix.
const code = /ORD(\w+)/.exec(content)?.[0];
if (!code) {
console.log(`unmatched transfer ${transferAmount} — "${content}"`);
return;
}
// Nothing checks the amount for you — this comparison is the whole point.
// const cart = await db.carts.findByPaymentCode(code);
// if (!cart || cart.total !== transferAmount) return;
// await db.carts.markPaid(code);
// await sendConfirmationEmail(code);
console.log(`${code} paid ${transferAmount}`);
},
});
// The handler answers 200 as soon as the signature checks out, and 401 with a
// `reason` when it does not — so a wrong secret is obvious in GPM Pay's
// delivery history rather than silently swallowed.