UNPKG

@gpmpay/sdk

Version:

Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.

57 lines (43 loc) • 1.98 kB
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.