UNPKG

@gpmpay/sdk

Version:

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

122 lines (119 loc) • 4.15 kB
// src/vietqr/crc16.ts function crc16ccitt(input) { let crc = 65535; for (let i = 0; i < input.length; i++) { crc ^= input.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { crc = crc & 32768 ? crc << 1 ^ 4129 : crc << 1; crc &= 65535; } } return crc.toString(16).toUpperCase().padStart(4, "0"); } // src/core/errors.ts var BRAND = /* @__PURE__ */ Symbol.for("gpmpay.sdk.error"); var GpmPayError = class extends Error { code; /** @internal Cross-realm brand — `instanceof` breaks across CJS/ESM copies. */ [BRAND] = true; constructor(message, code) { super(message); this.name = new.target.name; this.code = code; Object.setPrototypeOf(this, new.target.prototype); } /** * Prefer this over `instanceof` when a dual CJS/ESM install could put two * copies of the class in one process. */ static isGpmPayError(value) { return typeof value === "object" && value !== null && value[BRAND] === true; } }; var GpmPayConfigError = class extends GpmPayError { constructor(message, code = "invalid_argument") { super(message, code); } }; // src/vietqr/vietqr.ts var DESCRIPTION_MAX_LENGTH = 25; var tlv = (id, value) => id + value.length.toString().padStart(2, "0") + value; function isDynamic(amount) { return amount !== void 0 && amount !== null && amount !== ""; } function trimDescription(description) { return description === void 0 ? void 0 : description.slice(0, DESCRIPTION_MAX_LENGTH); } function buildVietQrPayload(input) { const { bankBin, accountNumber, amount, description, serviceCode = "QRIBFTTA" } = input; const beneficiaryOrg = tlv("00", bankBin) + tlv("01", accountNumber); const merchantAccountInfo = tlv("00", "A000000727") + tlv("01", beneficiaryOrg) + tlv("02", serviceCode); const dynamic = isDynamic(amount); const trimmedDesc = trimDescription(description); const additionalData = trimmedDesc ? tlv("08", trimmedDesc) : ""; const parts = [ tlv("00", "01"), tlv("01", dynamic ? "12" : "11"), tlv("38", merchantAccountInfo), tlv("53", "704"), ...dynamic ? [tlv("54", String(amount))] : [], tlv("58", "VN"), ...additionalData ? [tlv("62", additionalData)] : [] ]; const base = parts.join("") + "6304"; return base + crc16ccitt(base); } function buildVietQrImageUrl(input) { const { bankBin, accountNumber, amount, template = "compact" } = input; const trimmedDesc = trimDescription(input.description); const url = new URL( `https://img.vietqr.io/image/${bankBin}-${accountNumber}-${template}.png` ); if (isDynamic(amount)) url.searchParams.set("amount", String(amount)); if (trimmedDesc) url.searchParams.set("addInfo", trimmedDesc); if (input.accountName !== void 0) { url.searchParams.set("accountName", input.accountName); } return url.toString(); } function buildPaymentInstructions(request) { const account = request.bankAccount; const bank = account.bank; if (bank === void 0 || bank.bin === "") { throw new GpmPayConfigError( "buildPaymentInstructions: bankAccount.bank.bin is required to build a VietQR payload. Fetch the account with its `bank` relation, e.g. `await client.bankAccounts.retrieve(id)`." ); } const bankBin = bank.bin; const base = { bankBin, accountNumber: account.accountNumber, amount: request.amount, description: request.transferContent }; if (request.serviceCode !== void 0) base.serviceCode = request.serviceCode; const imageInput = { ...base, accountName: account.ownerName }; if (request.template !== void 0) imageInput.template = request.template; return { qrPayload: buildVietQrPayload(base), qrImageUrl: buildVietQrImageUrl(imageInput), amount: Number(request.amount), transferContent: request.transferContent, bankName: bank.shortName === "" ? bank.name : bank.shortName, bankBin, accountNumber: account.accountNumber, accountName: account.ownerName }; } export { buildPaymentInstructions, buildVietQrImageUrl, buildVietQrPayload, crc16ccitt }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map