@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
127 lines (123 loc) • 4.27 kB
JavaScript
;
// 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
};
}
exports.buildPaymentInstructions = buildPaymentInstructions;
exports.buildVietQrImageUrl = buildVietQrImageUrl;
exports.buildVietQrPayload = buildVietQrPayload;
exports.crc16ccitt = crc16ccitt;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map