aba-payway-sdk-unofficial 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -326,6 +326,49 @@ var ABAPayWay = class {
326
326
  return failure(err instanceof Error ? err.message : "Unknown error");
327
327
  }
328
328
  }
329
+ async closeTransaction(transactionId) {
330
+ const failure = (error, code, message) => ({
331
+ success: false,
332
+ transactionId: transactionId ?? "",
333
+ code,
334
+ message: message ?? error,
335
+ error
336
+ });
337
+ if (!transactionId || !transactionId.trim()) {
338
+ return failure("transactionId is required");
339
+ }
340
+ const reqTime = getABATimestamp();
341
+ const hash = await hmacSha512Base64(
342
+ `${reqTime}${this.config.merchantId}${transactionId}`,
343
+ this.config.apiKey
344
+ );
345
+ const body = JSON.stringify({
346
+ req_time: reqTime,
347
+ merchant_id: this.config.merchantId,
348
+ tran_id: transactionId,
349
+ hash
350
+ });
351
+ const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/close-transaction`;
352
+ try {
353
+ const response = await fetch(url, {
354
+ method: "POST",
355
+ headers: { "Content-Type": "application/json" },
356
+ body
357
+ });
358
+ const parsed = await parseAbaJson(response);
359
+ if (parsed.error) return failure(parsed.error);
360
+ const status = readAbaStatus(parsed.data);
361
+ if (!status.ok) return failure(status.message, status.code || void 0, status.message);
362
+ return {
363
+ success: true,
364
+ transactionId: parsed.data?.status?.tran_id ?? transactionId,
365
+ code: status.code,
366
+ message: status.message
367
+ };
368
+ } catch (err) {
369
+ return failure(err instanceof Error ? err.message : "Unknown error");
370
+ }
371
+ }
329
372
  /**
330
373
  * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`
331
374
  * header. Returns false for anything it cannot positively verify — a bad
@@ -390,6 +433,7 @@ function mapPaymentStatus(raw) {
390
433
  if (normalized === "APPROVED") return "APPROVED";
391
434
  if (normalized === "DECLINED") return "DECLINED";
392
435
  if (normalized === "REFUNDED") return "REFUNDED";
436
+ if (normalized === "CANCELLED" || normalized === "CANCELED") return "CANCELLED";
393
437
  if (normalized === "PENDING") return "PENDING";
394
438
  return "ERROR";
395
439
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/hash.ts","../src/response.ts","../src/utils.ts","../src/client.ts","../src/khqr.ts"],"sourcesContent":["export { ABAPayWay } from \"./client\";\nexport { generateKHQR } from \"./khqr\";\nexport { generateABAHash } from \"./hash\";\nexport {\n generateTransactionId,\n getABATimestamp,\n formatPhoneForABA,\n getQRExpiration,\n encodeItemsForABA,\n encodeReturnDeeplinkForABA,\n} from \"./utils\";\nexport type {\n ABAConfig,\n PurchaseItem,\n PaymentOption,\n ReturnDeeplink,\n PurchaseRequest,\n PurchaseResponse,\n StatusResponse,\n PaymentStatus,\n KHQROptions,\n HashParams,\n} from \"./types\";\n","import type { HashParams } from \"./types\";\n\n/**\n * Base64 of an HMAC-SHA512 digest — the one primitive ABA uses everywhere,\n * both for request signatures and for pushback verification. Kept in one\n * place so the two callers cannot drift apart.\n */\nexport async function hmacSha512Base64(message: string, secret: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-512\" },\n false,\n [\"sign\"]\n );\n const signature = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(message));\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\nexport async function generateABAHash(\n params: HashParams,\n publicKey: string\n): Promise<string> {\n // ABA requires parameters concatenated in this EXACT order\n const hashString = [\n params.req_time,\n params.merchant_id,\n params.tran_id,\n params.amount ?? \"\",\n params.items ?? \"\",\n params.shipping ?? \"\",\n params.ctid ?? \"\",\n params.pwt ?? \"\",\n params.firstname ?? \"\",\n params.lastname ?? \"\",\n params.email ?? \"\",\n params.phone ?? \"\",\n params.type ?? \"\",\n params.payment_option ?? \"\",\n params.return_url ?? \"\",\n params.cancel_url ?? \"\",\n params.continue_success_url ?? \"\",\n params.return_deeplink ?? \"\",\n params.currency ?? \"\",\n params.custom_fields ?? \"\",\n params.return_params ?? \"\",\n ].join(\"\");\n\n return hmacSha512Base64(hashString, publicKey);\n}\n","/**\n * ABA wraps every reply in a status envelope, and it ships two shapes.\n *\n * Legacy (`check-transaction`, older purchase):\n * { \"status\": 6, \"description\": \"tran_id not found\" }\n *\n * v3 (`check-transaction-2`, current purchase):\n * { \"status\": { \"code\": \"00\", \"message\": \"Success!\", ... }, \"description\": \"success\" }\n *\n * Note the success code is the *string* `\"00\"` in v3 and the *number* `0` in\n * the legacy shape, while failures are plain numbers (1, 5, 6, 21) in both.\n * A bare `data.status !== 0` therefore reads every v3 success as a failure,\n * which is what made a working sandbox key look broken. Normalise both here\n * so the client only ever branches on `ok`.\n */\n\nexport interface AbaStatus {\n /** Status code as a string, e.g. \"00\" for success, \"21\" for an expired key. */\n readonly code: string;\n readonly ok: boolean;\n readonly message: string;\n}\n\nexport function readAbaStatus(data: unknown): AbaStatus {\n const body = (data ?? {}) as Record<string, unknown>;\n const raw = body.status;\n\n let code: string;\n let message: string;\n\n if (raw !== null && typeof raw === \"object\") {\n const nested = raw as Record<string, unknown>;\n code = nested.code == null ? \"\" : String(nested.code);\n message = firstString(nested.message, body.description);\n } else {\n code = raw == null ? \"\" : String(raw);\n message = firstString(body.description, body.message);\n }\n\n return { code, ok: isSuccessCode(code), message: message || \"Unknown error\" };\n}\n\n/**\n * Success is 0, however ABA spells it: `0`, `\"0\"`, or v3's `\"00\"`. Guard the\n * empty string explicitly — `Number(\"\")` is 0, so a missing status would\n * otherwise read as success.\n */\nfunction isSuccessCode(code: string): boolean {\n if (code === \"\") return false;\n return Number(code) === 0;\n}\n\nfunction firstString(...values: unknown[]): string {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return \"\";\n}\n","import type { PurchaseItem, ReturnDeeplink } from \"./types\";\n\nexport function generateTransactionId(): string {\n const timestamp = Date.now().toString(36).toUpperCase();\n const random = Math.random().toString(36).substring(2, 6).toUpperCase();\n return `EA${timestamp}${random}`.substring(0, 20);\n}\n\n// ABA expects req_time in UTC (YYYYMMDDHHmmss). Using the server's local clock\n// makes the request look expired from any machine that is not set to UTC.\nexport function getABATimestamp(): string {\n const now = new Date();\n const pad = (n: number) => n.toString().padStart(2, \"0\");\n return (\n now.getUTCFullYear().toString() +\n pad(now.getUTCMonth() + 1) +\n pad(now.getUTCDate()) +\n pad(now.getUTCHours()) +\n pad(now.getUTCMinutes()) +\n pad(now.getUTCSeconds())\n );\n}\n\nexport function formatPhoneForABA(phone: string): string {\n if (!phone) return \"\";\n const cleaned = phone.replace(/[\\s-]/g, \"\");\n if (cleaned.startsWith(\"+855\")) return \"0\" + cleaned.slice(4);\n if (cleaned.startsWith(\"855\")) return \"0\" + cleaned.slice(3);\n return cleaned;\n}\n\nexport function getQRExpiration(minutes = 3): Date {\n return new Date(Date.now() + minutes * 60 * 1000);\n}\n\n/**\n * Base64 for arbitrary text. `btoa` alone throws on anything outside Latin-1,\n * and item names here can be Khmer or Chinese, so encode to UTF-8 bytes first.\n */\nexport function toBase64Utf8(text: string): string {\n const bytes = new TextEncoder().encode(text);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * ABA wants `items` as base64-encoded JSON. An array is encoded; a string is\n * passed through unchanged so existing callers keep working.\n */\nexport function encodeItemsForABA(\n items: string | readonly PurchaseItem[] | undefined\n): string {\n if (items == null) return \"\";\n if (typeof items === \"string\") return items;\n return toBase64Utf8(JSON.stringify(items));\n}\n\n/**\n * ABA wants `return_deeplink` as base64-encoded JSON naming the schemes that\n * bring the payer back from ABA Mobile to your app:\n *\n * { \"ios_scheme\": \"myapp://order/42\", \"android_scheme\": \"myapp://order/42\" }\n *\n * Without it the payer finishes in ABA Mobile and is stranded there. Encode an\n * object; pass a string through unchanged, so callers who already base64'd it\n * themselves keep working.\n */\nexport function encodeReturnDeeplinkForABA(\n deeplink: string | ReturnDeeplink | undefined\n): string {\n if (deeplink == null) return \"\";\n if (typeof deeplink === \"string\") return deeplink;\n return toBase64Utf8(JSON.stringify(deeplink));\n}\n","import type {\n ABAConfig,\n PurchaseRequest,\n PurchaseResponse,\n StatusResponse,\n PaymentStatus,\n} from \"./types\";\nimport { generateABAHash, hmacSha512Base64 } from \"./hash\";\nimport { readAbaStatus } from \"./response\";\nimport {\n getABATimestamp,\n formatPhoneForABA,\n encodeItemsForABA,\n encodeReturnDeeplinkForABA,\n} from \"./utils\";\n\n/**\n * ABA does not always answer with JSON. Bad credentials get an HTML error page,\n * and a gateway hiccup gets plain text. Parsing blindly turns those into\n * \"Unexpected token '<'\", which tells the operator nothing about the real\n * problem, so read the body as text and report what actually came back.\n */\nasync function parseAbaJson(response: Response): Promise<{ data?: any; error?: string }> {\n const text = await response.text();\n try {\n return { data: JSON.parse(text) };\n } catch {\n const snippet = text.trim().slice(0, 200);\n // A wrong merchant_id makes ABA render the checkout page instead of\n // answering the API, so HTML here almost always means bad credentials.\n if (response.ok) {\n return {\n error:\n `ABA PayWay returned a non-JSON response (HTTP ${response.status}). ` +\n `This usually means the merchant ID or API key is wrong. Response starts: ${snippet}`,\n };\n }\n return { error: `HTTP ${response.status}: ${snippet}` };\n }\n}\n\n/**\n * ABA hands merchants the full purchase URL (\"API Url\" on the credential\n * sheet), but the SDK builds endpoint paths itself. Accept either and keep\n * only the origin so pasting the sheet value verbatim works.\n */\nfunction normalizeBaseUrl(baseUrl: string): string {\n const trimmed = baseUrl.trim().replace(/\\/+$/, \"\");\n const marker = \"/api/payment-gateway\";\n const at = trimmed.indexOf(marker);\n return at === -1 ? trimmed : trimmed.slice(0, at);\n}\n\nexport class ABAPayWay {\n readonly config: Readonly<ABAConfig>;\n\n constructor(config: ABAConfig) {\n if (!config.merchantId) throw new Error(\"merchantId is required\");\n if (!config.apiKey) throw new Error(\"apiKey is required\");\n if (!config.baseUrl) throw new Error(\"baseUrl is required\");\n this.config = Object.freeze({ ...config, baseUrl: normalizeBaseUrl(config.baseUrl) });\n }\n\n async createPurchase(request: PurchaseRequest): Promise<PurchaseResponse> {\n const reqTime = getABATimestamp();\n const amount = request.amount.toFixed(2);\n const phone = request.phone ? formatPhoneForABA(request.phone) : \"\";\n // Encode once. The hash and the body must carry the identical string or\n // ABA rebuilds a different signature and rejects the request.\n const items = encodeItemsForABA(request.items);\n // Same rule as `items`: encode once, then reuse for both the hash and the\n // body. Encoding twice risks two different strings and a \"Wrong Hash.\"\n const returnDeeplink = encodeReturnDeeplinkForABA(request.returnDeeplink);\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n shipping: \"\",\n ctid: \"\",\n pwt: \"\",\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n type: \"\",\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n hash,\n });\n\n // Body-only fields. ABA does not hash `payment_gate` or `view_type`, so\n // they are appended after `hash` is built — adding them to hashParams\n // would produce a signature ABA cannot rebuild. Both are omitted entirely\n // when unset, matching the rule that ABA rejects fields it did not expect\n // (an empty `shipping` is what makes it answer \"Wrong shipping price\").\n if (request.paymentGate !== undefined) {\n body.set(\"payment_gate\", String(request.paymentGate));\n }\n if (request.viewType) body.set(\"view_type\", request.viewType);\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/purchase`;\n\n const failure = (error: string, errorCode?: string): PurchaseResponse => ({\n success: false,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n // The Checkout service answers 302 to the hosted payment page. Let it\n // be followed and the body is that page's HTML, which parses as\n // neither JSON nor an error worth reporting — so stop at the redirect\n // and read the address out of it instead.\n redirect: \"manual\",\n });\n\n // A 302 is success for the Checkout service flow: the payer finishes on\n // the page ABA points at. There is no JSON body to read, so hand back\n // the URL and let the caller poll checkStatus for the outcome.\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get(\"location\");\n if (!location) {\n return failure(\n `ABA PayWay redirected (HTTP ${response.status}) without a Location header.`,\n );\n }\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: new URL(location, this.config.baseUrl).toString(),\n };\n }\n\n // A rejected request comes back as HTTP 403 with the reason in a JSON\n // status envelope (\"Wrong Hash.\", \"End of API lifetime\"). Parse it\n // rather than dumping the raw body, so callers get a real error code.\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n const data = parsed.data;\n\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: data.checkout_url,\n abapayDeeplink: data.abapay_deeplink,\n appStoreUrl: data.app_store,\n playStoreUrl: data.play_store,\n // v3 answers in camelCase; the older API used snake_case.\n qrString: data.qrString ?? data.qr_string,\n qrImage: data.qrImage ?? data.qr_image,\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n async checkStatus(transactionId: string): Promise<StatusResponse> {\n const reqTime = getABATimestamp();\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n hash,\n });\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/check-transaction-2`;\n\n const failure = (error: string, errorCode?: string): StatusResponse => ({\n success: false,\n transactionId,\n status: \"ERROR\",\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n // v3 nests the transaction detail under `data`; the legacy shape put\n // these fields at the top level.\n const detail = parsed.data.data ?? parsed.data;\n\n const amount = toNumber(detail.total_amount ?? detail.amount);\n // `payment_currency` is an empty string until the payment settles.\n const currency = firstNonEmpty(detail.payment_currency, detail.currency);\n\n return {\n success: true,\n transactionId,\n status: mapPaymentStatus(detail.payment_status ?? detail.description),\n amount,\n currency,\n paymentTime: firstNonEmpty(detail.transaction_date, detail.payment_datetime),\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n /**\n * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`\n * header. Returns false for anything it cannot positively verify — a bad\n * signature, a malformed body, a missing key — so a caller can branch on\n * the result without a try/catch.\n *\n * `payload` may be the raw request body or the already-parsed object.\n * Unlike most gateways, ABA's scheme rebuilds the signature from the parsed\n * values rather than the raw bytes, so re-serialising the body on the way\n * in cannot invalidate it. Frameworks that hand you a parsed body are fine.\n *\n * `secret` defaults to `webhookSecret`, then to `apiKey` — ABA does not\n * issue a separate pushback secret, it signs with the merchant API key.\n */\n async verifyWebhook(\n payload: string | Record<string, unknown>,\n signature: string,\n secret?: string\n ): Promise<boolean> {\n try {\n const key = secret ?? this.config.webhookSecret ?? this.config.apiKey;\n if (!key || !signature) return false;\n\n const body: unknown = typeof payload === \"string\" ? JSON.parse(payload) : payload;\n // A JSON array or scalar has no keys to sort; only an object can be a\n // pushback body, and Array.isArray must be checked because arrays are\n // objects too.\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) return false;\n\n const expected = await hmacSha512Base64(\n buildPushbackHashBase(body as Record<string, unknown>),\n key\n );\n\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) return false;\n\n let mismatch = 0;\n for (let i = 0; i < expected.length; i++) {\n mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n return mismatch === 0;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * ABA signs pushback differently from API requests. A request hash uses a\n * fixed, documented field order (see `hash.ts`); a pushback hash instead\n * sorts the body's keys ascending and concatenates their **values** — no\n * keys, no separator — which is PHP's `ksort()` then `$b4hash .= $value`.\n *\n * Sorting the whole body rather than a fixed list means a field ABA adds\n * later is included automatically, so never narrow this to the known five\n * (`tran_id`, `apv`, `status`, `return_params`, `merchant_ref`).\n */\nfunction buildPushbackHashBase(body: Record<string, unknown>): string {\n // Default sort is by UTF-16 code unit, which matches PHP's byte-wise\n // string comparison for ABA's ASCII field names.\n return Object.keys(body)\n .sort()\n .map((key) => stringifyPushbackValue(body[key]))\n .join(\"\");\n}\n\n/**\n * ABA's reference implementation is PHP string concatenation, so values are\n * coerced the way PHP would: a missing value contributes nothing, and only a\n * nested array or object is JSON-encoded first.\n *\n * Every documented pushback field is a string — `return_params` arrives\n * already JSON-encoded as a string — so the object branch is defensive. Note\n * that PHP's `json_encode` escapes `/` as `\\/` while `JSON.stringify` does\n * not, so a nested object ABA adds later could disagree here.\n */\nfunction stringifyPushbackValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n // PHP casts true to \"1\" and false to the empty string.\n if (typeof value === \"boolean\") return value ? \"1\" : \"\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\nfunction toNumber(value: unknown): number | undefined {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = parseFloat(value);\n return Number.isNaN(parsed) ? undefined : parsed;\n }\n return undefined;\n}\n\nfunction firstNonEmpty(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return undefined;\n}\n\nfunction mapPaymentStatus(raw: string | undefined): PaymentStatus {\n const normalized = (raw ?? \"\").toUpperCase();\n if (normalized === \"APPROVED\") return \"APPROVED\";\n if (normalized === \"DECLINED\") return \"DECLINED\";\n if (normalized === \"REFUNDED\") return \"REFUNDED\";\n if (normalized === \"PENDING\") return \"PENDING\";\n return \"ERROR\";\n}\n","import type { KHQROptions } from \"./types\";\n\nconst DEFAULT_HEADER_COLOR = \"#bc271a\";\nconst QR_API_BASE = \"https://quickchart.io/qr\";\nconst CARD_SHELL_PATH = \"M189.868 10.8675H27.8677C18.4788 10.8675 10.8677 18.4787 10.8677 27.8675V287.867C10.8677 297.256 18.4788 304.867 27.8677 304.867H189.868C199.257 304.867 206.868 297.256 206.868 287.867V27.8675C206.868 18.4787 199.257 10.8675 189.868 10.8675Z\";\nconst KHQR_HEADER_BACKGROUND_PATH = \"M178.91 0C188.299 0.00000824649 195.91 7.61117 195.91 17V36.3516H196V54L178.582 37H0V17C0.00000103088 7.61116 7.61116 0 17 0H178.91Z\";\nconst KHQR_HEADER_MARK_PATHS = [\n \"M104.488 17.1027V20.5948H100.95C100.596 20.5948 100.331 20.3329 100.331 19.9836V17.1027C100.331 16.7535 100.596 16.4916 100.95 16.4916H103.781C104.223 16.4043 104.488 16.7535 104.488 17.1027Z\",\n \"M120.944 18.5H119.175C119.175 16.4047 117.494 14.746 115.371 14.746C113.69 14.746 112.274 15.7936 111.743 17.365C111.655 17.7143 111.566 18.1507 111.566 18.5V23.9999H111.478C110.505 23.9999 109.797 23.2142 109.797 22.3412V18.5C109.797 17.0159 110.416 15.5317 111.566 14.4841C112.628 13.5238 113.955 13 115.371 13C118.467 13 120.944 15.4444 120.944 18.5Z\",\n \"M120.945 24H118.467L117.848 23.3889L116.521 22.0794L114.663 20.2461H117.14L120.945 24Z\",\n \"M105.107 22.2539H99.7994C99.18 22.2539 98.6492 21.7301 98.6492 21.119V15.8809C98.6492 15.2698 99.18 14.746 99.7994 14.746H105.107C105.727 14.746 106.257 15.2698 106.257 15.8809V21.119L108.027 22.865V14.6587C108.027 13.6984 107.231 13 106.346 13H98.6492C97.6756 13 96.9683 13.7857 96.9683 14.6587V22.2539C96.9683 23.2142 97.7642 23.9126 98.6492 23.9126H106.877L105.107 22.2539Z\",\n \"M83.6093 23.9999H81.1318L76.0005 18.8492V23.9999H73.9658V13H76.0005V17.8888L80.9553 13H83.3436L78.0356 18.2381L83.6093 23.9999Z\",\n \"M92.898 13H94.8446V23.9999H92.898V19.1984H87.2358V23.9999H85.2012V13H87.2358V17.6269H92.898V13Z\",\n].map((path) => `<path d=\"${path}\" fill=\"#ffffff\"/>`).join(\"\");\n\nfunction formatAmount(amount: number, currency: string): string {\n if (currency === \"KHR\") {\n return amount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 }) + \" KHR\";\n }\n return \"$\" + amount.toFixed(2);\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nexport async function generateKHQR(options: KHQROptions): Promise<string> {\n const {\n emvData,\n amount,\n currency,\n merchantName,\n headerColor = DEFAULT_HEADER_COLOR,\n } = options;\n\n let qrSvgContent = \"\";\n try {\n const qrUrl = `${QR_API_BASE}?text=${encodeURIComponent(emvData)}&size=280&margin=1&format=svg`;\n const qrResponse = await fetch(qrUrl);\n if (qrResponse.ok) qrSvgContent = await qrResponse.text();\n } catch { /* fallback: empty QR area */ }\n\n // Keep the fetched QR's own <svg> wrapper intact (only strip a leading XML\n // declaration) — quickchart draws the QR modules in a small unit grid (e.g.\n // viewBox=\"0 0 43 43\") and relies on its own width/height=\"280\" to scale\n // that up. Stripping the wrapper loses that scale, rendering the QR at\n // native module size (a few percent of the intended 280px).\n const innerQr = qrSvgContent.replace(/<\\?xml[^>]*\\?>/g, \"\");\n\n const formattedAmount = formatAmount(amount, currency);\n const safeMerchantName = escapeXml(merchantName);\n\n // The QR itself is clipped and translated in two nested <g>s rather than\n // one: putting both `clip-path` and `transform` on the same element shifts\n // the clip rect's coordinates by that same transform, clipping away most\n // of the actual QR pattern.\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 218 316\" width=\"218\" height=\"316\" role=\"img\" aria-label=\"PayWay KHQR payment card\">\n <defs>\n <filter id=\"card-shadow\" x=\"0\" y=\"0\" width=\"217.735\" height=\"315.735\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"5.43375\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect1_dropShadow\" result=\"shape\"/>\n </filter>\n <clipPath id=\"card-clip\"><path d=\"${CARD_SHELL_PATH}\"/></clipPath>\n <clipPath id=\"qr-clip\"><rect id=\"qr-area\" x=\"37\" y=\"145\" width=\"144\" height=\"144\"/></clipPath>\n </defs>\n <rect width=\"218\" height=\"316\" fill=\"#ffffff\"/>\n <g filter=\"url(#card-shadow)\">\n <path d=\"${CARD_SHELL_PATH}\" fill=\"#ffffff\"/>\n </g>\n <g clip-path=\"url(#card-clip)\">\n <g id=\"khqr-header-logo\" transform=\"translate(11,11)\" aria-label=\"KHQR logo\">\n <path d=\"${KHQR_HEADER_BACKGROUND_PATH}\" fill=\"${headerColor}\"/>\n ${KHQR_HEADER_MARK_PATHS}\n </g>\n <text x=\"51\" y=\"81\" fill=\"#111111\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"10\">${safeMerchantName}</text>\n <text x=\"51\" y=\"108\" fill=\"#000000\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"20\" font-weight=\"500\">${formattedAmount}</text>\n <line x1=\"11\" y1=\"124\" x2=\"207\" y2=\"124\" stroke=\"#8a8a8a\" stroke-width=\"1\" stroke-dasharray=\"4 5\"/>\n <rect x=\"37\" y=\"145\" width=\"144\" height=\"144\" fill=\"#ffffff\"/>\n <g clip-path=\"url(#qr-clip)\">\n <g transform=\"translate(37,145) scale(0.5142857143)\">\n ${innerQr || '<svg width=\"280\" height=\"280\"><rect width=\"280\" height=\"280\" fill=\"#f5f5f5\"/><text x=\"140\" y=\"145\" text-anchor=\"middle\" fill=\"#777777\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"16\">QR Code</text></svg>'}\n </g>\n </g>\n <g aria-label=\"KHQR brand mark\">\n <circle cx=\"109\" cy=\"217\" r=\"17\" fill=\"#ffffff\"/>\n <circle cx=\"109\" cy=\"217\" r=\"13\" fill=\"${headerColor}\"/>\n <path d=\"M102 213h3v-3h8v3h3v8h-3v3h-8v-3h-3z\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/>\n <path d=\"M109 213v8M105 217h8\" stroke=\"#ffffff\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </g>\n </g>\n</svg>`;\n\n return \"data:image/svg+xml;base64,\" + btoa(svg);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/hash.ts","../src/response.ts","../src/utils.ts","../src/client.ts","../src/khqr.ts"],"sourcesContent":["export { ABAPayWay } from \"./client\";\nexport { generateKHQR } from \"./khqr\";\nexport { generateABAHash } from \"./hash\";\nexport {\n generateTransactionId,\n getABATimestamp,\n formatPhoneForABA,\n getQRExpiration,\n encodeItemsForABA,\n encodeReturnDeeplinkForABA,\n} from \"./utils\";\nexport type {\n ABAConfig,\n PurchaseItem,\n PaymentOption,\n ReturnDeeplink,\n PurchaseRequest,\n PurchaseResponse,\n StatusResponse,\n CloseTransactionResponse,\n PaymentStatus,\n KHQROptions,\n HashParams,\n} from \"./types\";\n","import type { HashParams } from \"./types\";\n\n/**\n * Base64 of an HMAC-SHA512 digest — the one primitive ABA uses everywhere,\n * both for request signatures and for pushback verification. Kept in one\n * place so the two callers cannot drift apart.\n */\nexport async function hmacSha512Base64(message: string, secret: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-512\" },\n false,\n [\"sign\"]\n );\n const signature = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(message));\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\nexport async function generateABAHash(\n params: HashParams,\n publicKey: string\n): Promise<string> {\n // ABA requires parameters concatenated in this EXACT order\n const hashString = [\n params.req_time,\n params.merchant_id,\n params.tran_id,\n params.amount ?? \"\",\n params.items ?? \"\",\n params.shipping ?? \"\",\n params.ctid ?? \"\",\n params.pwt ?? \"\",\n params.firstname ?? \"\",\n params.lastname ?? \"\",\n params.email ?? \"\",\n params.phone ?? \"\",\n params.type ?? \"\",\n params.payment_option ?? \"\",\n params.return_url ?? \"\",\n params.cancel_url ?? \"\",\n params.continue_success_url ?? \"\",\n params.return_deeplink ?? \"\",\n params.currency ?? \"\",\n params.custom_fields ?? \"\",\n params.return_params ?? \"\",\n ].join(\"\");\n\n return hmacSha512Base64(hashString, publicKey);\n}\n","/**\n * ABA wraps every reply in a status envelope, and it ships two shapes.\n *\n * Legacy (`check-transaction`, older purchase):\n * { \"status\": 6, \"description\": \"tran_id not found\" }\n *\n * v3 (`check-transaction-2`, current purchase):\n * { \"status\": { \"code\": \"00\", \"message\": \"Success!\", ... }, \"description\": \"success\" }\n *\n * Note the success code is the *string* `\"00\"` in v3 and the *number* `0` in\n * the legacy shape, while failures are plain numbers (1, 5, 6, 21) in both.\n * A bare `data.status !== 0` therefore reads every v3 success as a failure,\n * which is what made a working sandbox key look broken. Normalise both here\n * so the client only ever branches on `ok`.\n */\n\nexport interface AbaStatus {\n /** Status code as a string, e.g. \"00\" for success, \"21\" for an expired key. */\n readonly code: string;\n readonly ok: boolean;\n readonly message: string;\n}\n\nexport function readAbaStatus(data: unknown): AbaStatus {\n const body = (data ?? {}) as Record<string, unknown>;\n const raw = body.status;\n\n let code: string;\n let message: string;\n\n if (raw !== null && typeof raw === \"object\") {\n const nested = raw as Record<string, unknown>;\n code = nested.code == null ? \"\" : String(nested.code);\n message = firstString(nested.message, body.description);\n } else {\n code = raw == null ? \"\" : String(raw);\n message = firstString(body.description, body.message);\n }\n\n return { code, ok: isSuccessCode(code), message: message || \"Unknown error\" };\n}\n\n/**\n * Success is 0, however ABA spells it: `0`, `\"0\"`, or v3's `\"00\"`. Guard the\n * empty string explicitly — `Number(\"\")` is 0, so a missing status would\n * otherwise read as success.\n */\nfunction isSuccessCode(code: string): boolean {\n if (code === \"\") return false;\n return Number(code) === 0;\n}\n\nfunction firstString(...values: unknown[]): string {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return \"\";\n}\n","import type { PurchaseItem, ReturnDeeplink } from \"./types\";\n\nexport function generateTransactionId(): string {\n const timestamp = Date.now().toString(36).toUpperCase();\n const random = Math.random().toString(36).substring(2, 6).toUpperCase();\n return `EA${timestamp}${random}`.substring(0, 20);\n}\n\n// ABA expects req_time in UTC (YYYYMMDDHHmmss). Using the server's local clock\n// makes the request look expired from any machine that is not set to UTC.\nexport function getABATimestamp(): string {\n const now = new Date();\n const pad = (n: number) => n.toString().padStart(2, \"0\");\n return (\n now.getUTCFullYear().toString() +\n pad(now.getUTCMonth() + 1) +\n pad(now.getUTCDate()) +\n pad(now.getUTCHours()) +\n pad(now.getUTCMinutes()) +\n pad(now.getUTCSeconds())\n );\n}\n\nexport function formatPhoneForABA(phone: string): string {\n if (!phone) return \"\";\n const cleaned = phone.replace(/[\\s-]/g, \"\");\n if (cleaned.startsWith(\"+855\")) return \"0\" + cleaned.slice(4);\n if (cleaned.startsWith(\"855\")) return \"0\" + cleaned.slice(3);\n return cleaned;\n}\n\nexport function getQRExpiration(minutes = 3): Date {\n return new Date(Date.now() + minutes * 60 * 1000);\n}\n\n/**\n * Base64 for arbitrary text. `btoa` alone throws on anything outside Latin-1,\n * and item names here can be Khmer or Chinese, so encode to UTF-8 bytes first.\n */\nexport function toBase64Utf8(text: string): string {\n const bytes = new TextEncoder().encode(text);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * ABA wants `items` as base64-encoded JSON. An array is encoded; a string is\n * passed through unchanged so existing callers keep working.\n */\nexport function encodeItemsForABA(\n items: string | readonly PurchaseItem[] | undefined\n): string {\n if (items == null) return \"\";\n if (typeof items === \"string\") return items;\n return toBase64Utf8(JSON.stringify(items));\n}\n\n/**\n * ABA wants `return_deeplink` as base64-encoded JSON naming the schemes that\n * bring the payer back from ABA Mobile to your app:\n *\n * { \"ios_scheme\": \"myapp://order/42\", \"android_scheme\": \"myapp://order/42\" }\n *\n * Without it the payer finishes in ABA Mobile and is stranded there. Encode an\n * object; pass a string through unchanged, so callers who already base64'd it\n * themselves keep working.\n */\nexport function encodeReturnDeeplinkForABA(\n deeplink: string | ReturnDeeplink | undefined\n): string {\n if (deeplink == null) return \"\";\n if (typeof deeplink === \"string\") return deeplink;\n return toBase64Utf8(JSON.stringify(deeplink));\n}\n","import type {\n ABAConfig,\n PurchaseRequest,\n PurchaseResponse,\n StatusResponse,\n CloseTransactionResponse,\n PaymentStatus,\n} from \"./types\";\nimport { generateABAHash, hmacSha512Base64 } from \"./hash\";\nimport { readAbaStatus } from \"./response\";\nimport {\n getABATimestamp,\n formatPhoneForABA,\n encodeItemsForABA,\n encodeReturnDeeplinkForABA,\n} from \"./utils\";\n\n/**\n * ABA does not always answer with JSON. Bad credentials get an HTML error page,\n * and a gateway hiccup gets plain text. Parsing blindly turns those into\n * \"Unexpected token '<'\", which tells the operator nothing about the real\n * problem, so read the body as text and report what actually came back.\n */\nasync function parseAbaJson(response: Response): Promise<{ data?: any; error?: string }> {\n const text = await response.text();\n try {\n return { data: JSON.parse(text) };\n } catch {\n const snippet = text.trim().slice(0, 200);\n // A wrong merchant_id makes ABA render the checkout page instead of\n // answering the API, so HTML here almost always means bad credentials.\n if (response.ok) {\n return {\n error:\n `ABA PayWay returned a non-JSON response (HTTP ${response.status}). ` +\n `This usually means the merchant ID or API key is wrong. Response starts: ${snippet}`,\n };\n }\n return { error: `HTTP ${response.status}: ${snippet}` };\n }\n}\n\n/**\n * ABA hands merchants the full purchase URL (\"API Url\" on the credential\n * sheet), but the SDK builds endpoint paths itself. Accept either and keep\n * only the origin so pasting the sheet value verbatim works.\n */\nfunction normalizeBaseUrl(baseUrl: string): string {\n const trimmed = baseUrl.trim().replace(/\\/+$/, \"\");\n const marker = \"/api/payment-gateway\";\n const at = trimmed.indexOf(marker);\n return at === -1 ? trimmed : trimmed.slice(0, at);\n}\n\nexport class ABAPayWay {\n readonly config: Readonly<ABAConfig>;\n\n constructor(config: ABAConfig) {\n if (!config.merchantId) throw new Error(\"merchantId is required\");\n if (!config.apiKey) throw new Error(\"apiKey is required\");\n if (!config.baseUrl) throw new Error(\"baseUrl is required\");\n this.config = Object.freeze({ ...config, baseUrl: normalizeBaseUrl(config.baseUrl) });\n }\n\n async createPurchase(request: PurchaseRequest): Promise<PurchaseResponse> {\n const reqTime = getABATimestamp();\n const amount = request.amount.toFixed(2);\n const phone = request.phone ? formatPhoneForABA(request.phone) : \"\";\n // Encode once. The hash and the body must carry the identical string or\n // ABA rebuilds a different signature and rejects the request.\n const items = encodeItemsForABA(request.items);\n // Same rule as `items`: encode once, then reuse for both the hash and the\n // body. Encoding twice risks two different strings and a \"Wrong Hash.\"\n const returnDeeplink = encodeReturnDeeplinkForABA(request.returnDeeplink);\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n shipping: \"\",\n ctid: \"\",\n pwt: \"\",\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n type: \"\",\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n hash,\n });\n\n // Body-only fields. ABA does not hash `payment_gate` or `view_type`, so\n // they are appended after `hash` is built — adding them to hashParams\n // would produce a signature ABA cannot rebuild. Both are omitted entirely\n // when unset, matching the rule that ABA rejects fields it did not expect\n // (an empty `shipping` is what makes it answer \"Wrong shipping price\").\n if (request.paymentGate !== undefined) {\n body.set(\"payment_gate\", String(request.paymentGate));\n }\n if (request.viewType) body.set(\"view_type\", request.viewType);\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/purchase`;\n\n const failure = (error: string, errorCode?: string): PurchaseResponse => ({\n success: false,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n // The Checkout service answers 302 to the hosted payment page. Let it\n // be followed and the body is that page's HTML, which parses as\n // neither JSON nor an error worth reporting — so stop at the redirect\n // and read the address out of it instead.\n redirect: \"manual\",\n });\n\n // A 302 is success for the Checkout service flow: the payer finishes on\n // the page ABA points at. There is no JSON body to read, so hand back\n // the URL and let the caller poll checkStatus for the outcome.\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get(\"location\");\n if (!location) {\n return failure(\n `ABA PayWay redirected (HTTP ${response.status}) without a Location header.`,\n );\n }\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: new URL(location, this.config.baseUrl).toString(),\n };\n }\n\n // A rejected request comes back as HTTP 403 with the reason in a JSON\n // status envelope (\"Wrong Hash.\", \"End of API lifetime\"). Parse it\n // rather than dumping the raw body, so callers get a real error code.\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n const data = parsed.data;\n\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: data.checkout_url,\n abapayDeeplink: data.abapay_deeplink,\n appStoreUrl: data.app_store,\n playStoreUrl: data.play_store,\n // v3 answers in camelCase; the older API used snake_case.\n qrString: data.qrString ?? data.qr_string,\n qrImage: data.qrImage ?? data.qr_image,\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n async checkStatus(transactionId: string): Promise<StatusResponse> {\n const reqTime = getABATimestamp();\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n hash,\n });\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/check-transaction-2`;\n\n const failure = (error: string, errorCode?: string): StatusResponse => ({\n success: false,\n transactionId,\n status: \"ERROR\",\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n // v3 nests the transaction detail under `data`; the legacy shape put\n // these fields at the top level.\n const detail = parsed.data.data ?? parsed.data;\n\n const amount = toNumber(detail.total_amount ?? detail.amount);\n // `payment_currency` is an empty string until the payment settles.\n const currency = firstNonEmpty(detail.payment_currency, detail.currency);\n\n return {\n success: true,\n transactionId,\n status: mapPaymentStatus(detail.payment_status ?? detail.description),\n amount,\n currency,\n paymentTime: firstNonEmpty(detail.transaction_date, detail.payment_datetime),\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n async closeTransaction(transactionId: string): Promise<CloseTransactionResponse> {\n const failure = (\n error: string,\n code?: string,\n message?: string\n ): CloseTransactionResponse => ({\n success: false,\n transactionId: transactionId ?? \"\",\n code,\n message: message ?? error,\n error,\n });\n\n if (!transactionId || !transactionId.trim()) {\n return failure(\"transactionId is required\");\n }\n\n const reqTime = getABATimestamp();\n const hash = await hmacSha512Base64(\n `${reqTime}${this.config.merchantId}${transactionId}`,\n this.config.apiKey\n );\n\n const body = JSON.stringify({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n hash,\n });\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/close-transaction`;\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined, status.message);\n\n return {\n success: true,\n transactionId: parsed.data?.status?.tran_id ?? transactionId,\n code: status.code,\n message: status.message,\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n /**\n * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`\n * header. Returns false for anything it cannot positively verify — a bad\n * signature, a malformed body, a missing key — so a caller can branch on\n * the result without a try/catch.\n *\n * `payload` may be the raw request body or the already-parsed object.\n * Unlike most gateways, ABA's scheme rebuilds the signature from the parsed\n * values rather than the raw bytes, so re-serialising the body on the way\n * in cannot invalidate it. Frameworks that hand you a parsed body are fine.\n *\n * `secret` defaults to `webhookSecret`, then to `apiKey` — ABA does not\n * issue a separate pushback secret, it signs with the merchant API key.\n */\n async verifyWebhook(\n payload: string | Record<string, unknown>,\n signature: string,\n secret?: string\n ): Promise<boolean> {\n try {\n const key = secret ?? this.config.webhookSecret ?? this.config.apiKey;\n if (!key || !signature) return false;\n\n const body: unknown = typeof payload === \"string\" ? JSON.parse(payload) : payload;\n // A JSON array or scalar has no keys to sort; only an object can be a\n // pushback body, and Array.isArray must be checked because arrays are\n // objects too.\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) return false;\n\n const expected = await hmacSha512Base64(\n buildPushbackHashBase(body as Record<string, unknown>),\n key\n );\n\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) return false;\n\n let mismatch = 0;\n for (let i = 0; i < expected.length; i++) {\n mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n return mismatch === 0;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * ABA signs pushback differently from API requests. A request hash uses a\n * fixed, documented field order (see `hash.ts`); a pushback hash instead\n * sorts the body's keys ascending and concatenates their **values** — no\n * keys, no separator — which is PHP's `ksort()` then `$b4hash .= $value`.\n *\n * Sorting the whole body rather than a fixed list means a field ABA adds\n * later is included automatically, so never narrow this to the known five\n * (`tran_id`, `apv`, `status`, `return_params`, `merchant_ref`).\n */\nfunction buildPushbackHashBase(body: Record<string, unknown>): string {\n // Default sort is by UTF-16 code unit, which matches PHP's byte-wise\n // string comparison for ABA's ASCII field names.\n return Object.keys(body)\n .sort()\n .map((key) => stringifyPushbackValue(body[key]))\n .join(\"\");\n}\n\n/**\n * ABA's reference implementation is PHP string concatenation, so values are\n * coerced the way PHP would: a missing value contributes nothing, and only a\n * nested array or object is JSON-encoded first.\n *\n * Every documented pushback field is a string — `return_params` arrives\n * already JSON-encoded as a string — so the object branch is defensive. Note\n * that PHP's `json_encode` escapes `/` as `\\/` while `JSON.stringify` does\n * not, so a nested object ABA adds later could disagree here.\n */\nfunction stringifyPushbackValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n // PHP casts true to \"1\" and false to the empty string.\n if (typeof value === \"boolean\") return value ? \"1\" : \"\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\nfunction toNumber(value: unknown): number | undefined {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = parseFloat(value);\n return Number.isNaN(parsed) ? undefined : parsed;\n }\n return undefined;\n}\n\nfunction firstNonEmpty(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return undefined;\n}\n\nfunction mapPaymentStatus(raw: string | undefined): PaymentStatus {\n const normalized = (raw ?? \"\").toUpperCase();\n if (normalized === \"APPROVED\") return \"APPROVED\";\n if (normalized === \"DECLINED\") return \"DECLINED\";\n if (normalized === \"REFUNDED\") return \"REFUNDED\";\n if (normalized === \"CANCELLED\" || normalized === \"CANCELED\") return \"CANCELLED\";\n if (normalized === \"PENDING\") return \"PENDING\";\n return \"ERROR\";\n}\n","import type { KHQROptions } from \"./types\";\n\nconst DEFAULT_HEADER_COLOR = \"#bc271a\";\nconst QR_API_BASE = \"https://quickchart.io/qr\";\nconst CARD_SHELL_PATH = \"M189.868 10.8675H27.8677C18.4788 10.8675 10.8677 18.4787 10.8677 27.8675V287.867C10.8677 297.256 18.4788 304.867 27.8677 304.867H189.868C199.257 304.867 206.868 297.256 206.868 287.867V27.8675C206.868 18.4787 199.257 10.8675 189.868 10.8675Z\";\nconst KHQR_HEADER_BACKGROUND_PATH = \"M178.91 0C188.299 0.00000824649 195.91 7.61117 195.91 17V36.3516H196V54L178.582 37H0V17C0.00000103088 7.61116 7.61116 0 17 0H178.91Z\";\nconst KHQR_HEADER_MARK_PATHS = [\n \"M104.488 17.1027V20.5948H100.95C100.596 20.5948 100.331 20.3329 100.331 19.9836V17.1027C100.331 16.7535 100.596 16.4916 100.95 16.4916H103.781C104.223 16.4043 104.488 16.7535 104.488 17.1027Z\",\n \"M120.944 18.5H119.175C119.175 16.4047 117.494 14.746 115.371 14.746C113.69 14.746 112.274 15.7936 111.743 17.365C111.655 17.7143 111.566 18.1507 111.566 18.5V23.9999H111.478C110.505 23.9999 109.797 23.2142 109.797 22.3412V18.5C109.797 17.0159 110.416 15.5317 111.566 14.4841C112.628 13.5238 113.955 13 115.371 13C118.467 13 120.944 15.4444 120.944 18.5Z\",\n \"M120.945 24H118.467L117.848 23.3889L116.521 22.0794L114.663 20.2461H117.14L120.945 24Z\",\n \"M105.107 22.2539H99.7994C99.18 22.2539 98.6492 21.7301 98.6492 21.119V15.8809C98.6492 15.2698 99.18 14.746 99.7994 14.746H105.107C105.727 14.746 106.257 15.2698 106.257 15.8809V21.119L108.027 22.865V14.6587C108.027 13.6984 107.231 13 106.346 13H98.6492C97.6756 13 96.9683 13.7857 96.9683 14.6587V22.2539C96.9683 23.2142 97.7642 23.9126 98.6492 23.9126H106.877L105.107 22.2539Z\",\n \"M83.6093 23.9999H81.1318L76.0005 18.8492V23.9999H73.9658V13H76.0005V17.8888L80.9553 13H83.3436L78.0356 18.2381L83.6093 23.9999Z\",\n \"M92.898 13H94.8446V23.9999H92.898V19.1984H87.2358V23.9999H85.2012V13H87.2358V17.6269H92.898V13Z\",\n].map((path) => `<path d=\"${path}\" fill=\"#ffffff\"/>`).join(\"\");\n\nfunction formatAmount(amount: number, currency: string): string {\n if (currency === \"KHR\") {\n return amount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 }) + \" KHR\";\n }\n return \"$\" + amount.toFixed(2);\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nexport async function generateKHQR(options: KHQROptions): Promise<string> {\n const {\n emvData,\n amount,\n currency,\n merchantName,\n headerColor = DEFAULT_HEADER_COLOR,\n } = options;\n\n let qrSvgContent = \"\";\n try {\n const qrUrl = `${QR_API_BASE}?text=${encodeURIComponent(emvData)}&size=280&margin=1&format=svg`;\n const qrResponse = await fetch(qrUrl);\n if (qrResponse.ok) qrSvgContent = await qrResponse.text();\n } catch { /* fallback: empty QR area */ }\n\n // Keep the fetched QR's own <svg> wrapper intact (only strip a leading XML\n // declaration) — quickchart draws the QR modules in a small unit grid (e.g.\n // viewBox=\"0 0 43 43\") and relies on its own width/height=\"280\" to scale\n // that up. Stripping the wrapper loses that scale, rendering the QR at\n // native module size (a few percent of the intended 280px).\n const innerQr = qrSvgContent.replace(/<\\?xml[^>]*\\?>/g, \"\");\n\n const formattedAmount = formatAmount(amount, currency);\n const safeMerchantName = escapeXml(merchantName);\n\n // The QR itself is clipped and translated in two nested <g>s rather than\n // one: putting both `clip-path` and `transform` on the same element shifts\n // the clip rect's coordinates by that same transform, clipping away most\n // of the actual QR pattern.\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 218 316\" width=\"218\" height=\"316\" role=\"img\" aria-label=\"PayWay KHQR payment card\">\n <defs>\n <filter id=\"card-shadow\" x=\"0\" y=\"0\" width=\"217.735\" height=\"315.735\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"5.43375\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect1_dropShadow\" result=\"shape\"/>\n </filter>\n <clipPath id=\"card-clip\"><path d=\"${CARD_SHELL_PATH}\"/></clipPath>\n <clipPath id=\"qr-clip\"><rect id=\"qr-area\" x=\"37\" y=\"145\" width=\"144\" height=\"144\"/></clipPath>\n </defs>\n <rect width=\"218\" height=\"316\" fill=\"#ffffff\"/>\n <g filter=\"url(#card-shadow)\">\n <path d=\"${CARD_SHELL_PATH}\" fill=\"#ffffff\"/>\n </g>\n <g clip-path=\"url(#card-clip)\">\n <g id=\"khqr-header-logo\" transform=\"translate(11,11)\" aria-label=\"KHQR logo\">\n <path d=\"${KHQR_HEADER_BACKGROUND_PATH}\" fill=\"${headerColor}\"/>\n ${KHQR_HEADER_MARK_PATHS}\n </g>\n <text x=\"51\" y=\"81\" fill=\"#111111\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"10\">${safeMerchantName}</text>\n <text x=\"51\" y=\"108\" fill=\"#000000\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"20\" font-weight=\"500\">${formattedAmount}</text>\n <line x1=\"11\" y1=\"124\" x2=\"207\" y2=\"124\" stroke=\"#8a8a8a\" stroke-width=\"1\" stroke-dasharray=\"4 5\"/>\n <rect x=\"37\" y=\"145\" width=\"144\" height=\"144\" fill=\"#ffffff\"/>\n <g clip-path=\"url(#qr-clip)\">\n <g transform=\"translate(37,145) scale(0.5142857143)\">\n ${innerQr || '<svg width=\"280\" height=\"280\"><rect width=\"280\" height=\"280\" fill=\"#f5f5f5\"/><text x=\"140\" y=\"145\" text-anchor=\"middle\" fill=\"#777777\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"16\">QR Code</text></svg>'}\n </g>\n </g>\n <g aria-label=\"KHQR brand mark\">\n <circle cx=\"109\" cy=\"217\" r=\"17\" fill=\"#ffffff\"/>\n <circle cx=\"109\" cy=\"217\" r=\"13\" fill=\"${headerColor}\"/>\n <path d=\"M102 213h3v-3h8v3h3v8h-3v3h-8v-3h-3z\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/>\n <path d=\"M109 213v8M105 217h8\" stroke=\"#ffffff\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </g>\n </g>\n</svg>`;\n\n return \"data:image/svg+xml;base64,\" + btoa(svg);\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -109,7 +109,14 @@ interface PurchaseResponse {
109
109
  readonly error?: string;
110
110
  readonly errorCode?: string;
111
111
  }
112
- type PaymentStatus = "PENDING" | "APPROVED" | "DECLINED" | "REFUNDED" | "ERROR";
112
+ type PaymentStatus = "PENDING" | "APPROVED" | "DECLINED" | "REFUNDED" | "CANCELLED" | "ERROR";
113
+ interface CloseTransactionResponse {
114
+ readonly success: boolean;
115
+ readonly transactionId: string;
116
+ readonly code?: string;
117
+ readonly message?: string;
118
+ readonly error?: string;
119
+ }
113
120
  interface StatusResponse {
114
121
  readonly success: boolean;
115
122
  readonly transactionId: string;
@@ -158,6 +165,7 @@ declare class ABAPayWay {
158
165
  constructor(config: ABAConfig);
159
166
  createPurchase(request: PurchaseRequest): Promise<PurchaseResponse>;
160
167
  checkStatus(transactionId: string): Promise<StatusResponse>;
168
+ closeTransaction(transactionId: string): Promise<CloseTransactionResponse>;
161
169
  /**
162
170
  * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`
163
171
  * header. Returns false for anything it cannot positively verify — a bad
@@ -200,4 +208,4 @@ declare function encodeItemsForABA(items: string | readonly PurchaseItem[] | und
200
208
  */
201
209
  declare function encodeReturnDeeplinkForABA(deeplink: string | ReturnDeeplink | undefined): string;
202
210
 
203
- export { type ABAConfig, ABAPayWay, type HashParams, type KHQROptions, type PaymentOption, type PaymentStatus, type PurchaseItem, type PurchaseRequest, type PurchaseResponse, type ReturnDeeplink, type StatusResponse, encodeItemsForABA, encodeReturnDeeplinkForABA, formatPhoneForABA, generateABAHash, generateKHQR, generateTransactionId, getABATimestamp, getQRExpiration };
211
+ export { type ABAConfig, ABAPayWay, type CloseTransactionResponse, type HashParams, type KHQROptions, type PaymentOption, type PaymentStatus, type PurchaseItem, type PurchaseRequest, type PurchaseResponse, type ReturnDeeplink, type StatusResponse, encodeItemsForABA, encodeReturnDeeplinkForABA, formatPhoneForABA, generateABAHash, generateKHQR, generateTransactionId, getABATimestamp, getQRExpiration };
package/dist/index.d.ts CHANGED
@@ -109,7 +109,14 @@ interface PurchaseResponse {
109
109
  readonly error?: string;
110
110
  readonly errorCode?: string;
111
111
  }
112
- type PaymentStatus = "PENDING" | "APPROVED" | "DECLINED" | "REFUNDED" | "ERROR";
112
+ type PaymentStatus = "PENDING" | "APPROVED" | "DECLINED" | "REFUNDED" | "CANCELLED" | "ERROR";
113
+ interface CloseTransactionResponse {
114
+ readonly success: boolean;
115
+ readonly transactionId: string;
116
+ readonly code?: string;
117
+ readonly message?: string;
118
+ readonly error?: string;
119
+ }
113
120
  interface StatusResponse {
114
121
  readonly success: boolean;
115
122
  readonly transactionId: string;
@@ -158,6 +165,7 @@ declare class ABAPayWay {
158
165
  constructor(config: ABAConfig);
159
166
  createPurchase(request: PurchaseRequest): Promise<PurchaseResponse>;
160
167
  checkStatus(transactionId: string): Promise<StatusResponse>;
168
+ closeTransaction(transactionId: string): Promise<CloseTransactionResponse>;
161
169
  /**
162
170
  * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`
163
171
  * header. Returns false for anything it cannot positively verify — a bad
@@ -200,4 +208,4 @@ declare function encodeItemsForABA(items: string | readonly PurchaseItem[] | und
200
208
  */
201
209
  declare function encodeReturnDeeplinkForABA(deeplink: string | ReturnDeeplink | undefined): string;
202
210
 
203
- export { type ABAConfig, ABAPayWay, type HashParams, type KHQROptions, type PaymentOption, type PaymentStatus, type PurchaseItem, type PurchaseRequest, type PurchaseResponse, type ReturnDeeplink, type StatusResponse, encodeItemsForABA, encodeReturnDeeplinkForABA, formatPhoneForABA, generateABAHash, generateKHQR, generateTransactionId, getABATimestamp, getQRExpiration };
211
+ export { type ABAConfig, ABAPayWay, type CloseTransactionResponse, type HashParams, type KHQROptions, type PaymentOption, type PaymentStatus, type PurchaseItem, type PurchaseRequest, type PurchaseResponse, type ReturnDeeplink, type StatusResponse, encodeItemsForABA, encodeReturnDeeplinkForABA, formatPhoneForABA, generateABAHash, generateKHQR, generateTransactionId, getABATimestamp, getQRExpiration };
package/dist/index.js CHANGED
@@ -292,6 +292,49 @@ var ABAPayWay = class {
292
292
  return failure(err instanceof Error ? err.message : "Unknown error");
293
293
  }
294
294
  }
295
+ async closeTransaction(transactionId) {
296
+ const failure = (error, code, message) => ({
297
+ success: false,
298
+ transactionId: transactionId ?? "",
299
+ code,
300
+ message: message ?? error,
301
+ error
302
+ });
303
+ if (!transactionId || !transactionId.trim()) {
304
+ return failure("transactionId is required");
305
+ }
306
+ const reqTime = getABATimestamp();
307
+ const hash = await hmacSha512Base64(
308
+ `${reqTime}${this.config.merchantId}${transactionId}`,
309
+ this.config.apiKey
310
+ );
311
+ const body = JSON.stringify({
312
+ req_time: reqTime,
313
+ merchant_id: this.config.merchantId,
314
+ tran_id: transactionId,
315
+ hash
316
+ });
317
+ const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/close-transaction`;
318
+ try {
319
+ const response = await fetch(url, {
320
+ method: "POST",
321
+ headers: { "Content-Type": "application/json" },
322
+ body
323
+ });
324
+ const parsed = await parseAbaJson(response);
325
+ if (parsed.error) return failure(parsed.error);
326
+ const status = readAbaStatus(parsed.data);
327
+ if (!status.ok) return failure(status.message, status.code || void 0, status.message);
328
+ return {
329
+ success: true,
330
+ transactionId: parsed.data?.status?.tran_id ?? transactionId,
331
+ code: status.code,
332
+ message: status.message
333
+ };
334
+ } catch (err) {
335
+ return failure(err instanceof Error ? err.message : "Unknown error");
336
+ }
337
+ }
295
338
  /**
296
339
  * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`
297
340
  * header. Returns false for anything it cannot positively verify — a bad
@@ -356,6 +399,7 @@ function mapPaymentStatus(raw) {
356
399
  if (normalized === "APPROVED") return "APPROVED";
357
400
  if (normalized === "DECLINED") return "DECLINED";
358
401
  if (normalized === "REFUNDED") return "REFUNDED";
402
+ if (normalized === "CANCELLED" || normalized === "CANCELED") return "CANCELLED";
359
403
  if (normalized === "PENDING") return "PENDING";
360
404
  return "ERROR";
361
405
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hash.ts","../src/response.ts","../src/utils.ts","../src/client.ts","../src/khqr.ts"],"sourcesContent":["import type { HashParams } from \"./types\";\n\n/**\n * Base64 of an HMAC-SHA512 digest — the one primitive ABA uses everywhere,\n * both for request signatures and for pushback verification. Kept in one\n * place so the two callers cannot drift apart.\n */\nexport async function hmacSha512Base64(message: string, secret: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-512\" },\n false,\n [\"sign\"]\n );\n const signature = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(message));\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\nexport async function generateABAHash(\n params: HashParams,\n publicKey: string\n): Promise<string> {\n // ABA requires parameters concatenated in this EXACT order\n const hashString = [\n params.req_time,\n params.merchant_id,\n params.tran_id,\n params.amount ?? \"\",\n params.items ?? \"\",\n params.shipping ?? \"\",\n params.ctid ?? \"\",\n params.pwt ?? \"\",\n params.firstname ?? \"\",\n params.lastname ?? \"\",\n params.email ?? \"\",\n params.phone ?? \"\",\n params.type ?? \"\",\n params.payment_option ?? \"\",\n params.return_url ?? \"\",\n params.cancel_url ?? \"\",\n params.continue_success_url ?? \"\",\n params.return_deeplink ?? \"\",\n params.currency ?? \"\",\n params.custom_fields ?? \"\",\n params.return_params ?? \"\",\n ].join(\"\");\n\n return hmacSha512Base64(hashString, publicKey);\n}\n","/**\n * ABA wraps every reply in a status envelope, and it ships two shapes.\n *\n * Legacy (`check-transaction`, older purchase):\n * { \"status\": 6, \"description\": \"tran_id not found\" }\n *\n * v3 (`check-transaction-2`, current purchase):\n * { \"status\": { \"code\": \"00\", \"message\": \"Success!\", ... }, \"description\": \"success\" }\n *\n * Note the success code is the *string* `\"00\"` in v3 and the *number* `0` in\n * the legacy shape, while failures are plain numbers (1, 5, 6, 21) in both.\n * A bare `data.status !== 0` therefore reads every v3 success as a failure,\n * which is what made a working sandbox key look broken. Normalise both here\n * so the client only ever branches on `ok`.\n */\n\nexport interface AbaStatus {\n /** Status code as a string, e.g. \"00\" for success, \"21\" for an expired key. */\n readonly code: string;\n readonly ok: boolean;\n readonly message: string;\n}\n\nexport function readAbaStatus(data: unknown): AbaStatus {\n const body = (data ?? {}) as Record<string, unknown>;\n const raw = body.status;\n\n let code: string;\n let message: string;\n\n if (raw !== null && typeof raw === \"object\") {\n const nested = raw as Record<string, unknown>;\n code = nested.code == null ? \"\" : String(nested.code);\n message = firstString(nested.message, body.description);\n } else {\n code = raw == null ? \"\" : String(raw);\n message = firstString(body.description, body.message);\n }\n\n return { code, ok: isSuccessCode(code), message: message || \"Unknown error\" };\n}\n\n/**\n * Success is 0, however ABA spells it: `0`, `\"0\"`, or v3's `\"00\"`. Guard the\n * empty string explicitly — `Number(\"\")` is 0, so a missing status would\n * otherwise read as success.\n */\nfunction isSuccessCode(code: string): boolean {\n if (code === \"\") return false;\n return Number(code) === 0;\n}\n\nfunction firstString(...values: unknown[]): string {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return \"\";\n}\n","import type { PurchaseItem, ReturnDeeplink } from \"./types\";\n\nexport function generateTransactionId(): string {\n const timestamp = Date.now().toString(36).toUpperCase();\n const random = Math.random().toString(36).substring(2, 6).toUpperCase();\n return `EA${timestamp}${random}`.substring(0, 20);\n}\n\n// ABA expects req_time in UTC (YYYYMMDDHHmmss). Using the server's local clock\n// makes the request look expired from any machine that is not set to UTC.\nexport function getABATimestamp(): string {\n const now = new Date();\n const pad = (n: number) => n.toString().padStart(2, \"0\");\n return (\n now.getUTCFullYear().toString() +\n pad(now.getUTCMonth() + 1) +\n pad(now.getUTCDate()) +\n pad(now.getUTCHours()) +\n pad(now.getUTCMinutes()) +\n pad(now.getUTCSeconds())\n );\n}\n\nexport function formatPhoneForABA(phone: string): string {\n if (!phone) return \"\";\n const cleaned = phone.replace(/[\\s-]/g, \"\");\n if (cleaned.startsWith(\"+855\")) return \"0\" + cleaned.slice(4);\n if (cleaned.startsWith(\"855\")) return \"0\" + cleaned.slice(3);\n return cleaned;\n}\n\nexport function getQRExpiration(minutes = 3): Date {\n return new Date(Date.now() + minutes * 60 * 1000);\n}\n\n/**\n * Base64 for arbitrary text. `btoa` alone throws on anything outside Latin-1,\n * and item names here can be Khmer or Chinese, so encode to UTF-8 bytes first.\n */\nexport function toBase64Utf8(text: string): string {\n const bytes = new TextEncoder().encode(text);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * ABA wants `items` as base64-encoded JSON. An array is encoded; a string is\n * passed through unchanged so existing callers keep working.\n */\nexport function encodeItemsForABA(\n items: string | readonly PurchaseItem[] | undefined\n): string {\n if (items == null) return \"\";\n if (typeof items === \"string\") return items;\n return toBase64Utf8(JSON.stringify(items));\n}\n\n/**\n * ABA wants `return_deeplink` as base64-encoded JSON naming the schemes that\n * bring the payer back from ABA Mobile to your app:\n *\n * { \"ios_scheme\": \"myapp://order/42\", \"android_scheme\": \"myapp://order/42\" }\n *\n * Without it the payer finishes in ABA Mobile and is stranded there. Encode an\n * object; pass a string through unchanged, so callers who already base64'd it\n * themselves keep working.\n */\nexport function encodeReturnDeeplinkForABA(\n deeplink: string | ReturnDeeplink | undefined\n): string {\n if (deeplink == null) return \"\";\n if (typeof deeplink === \"string\") return deeplink;\n return toBase64Utf8(JSON.stringify(deeplink));\n}\n","import type {\n ABAConfig,\n PurchaseRequest,\n PurchaseResponse,\n StatusResponse,\n PaymentStatus,\n} from \"./types\";\nimport { generateABAHash, hmacSha512Base64 } from \"./hash\";\nimport { readAbaStatus } from \"./response\";\nimport {\n getABATimestamp,\n formatPhoneForABA,\n encodeItemsForABA,\n encodeReturnDeeplinkForABA,\n} from \"./utils\";\n\n/**\n * ABA does not always answer with JSON. Bad credentials get an HTML error page,\n * and a gateway hiccup gets plain text. Parsing blindly turns those into\n * \"Unexpected token '<'\", which tells the operator nothing about the real\n * problem, so read the body as text and report what actually came back.\n */\nasync function parseAbaJson(response: Response): Promise<{ data?: any; error?: string }> {\n const text = await response.text();\n try {\n return { data: JSON.parse(text) };\n } catch {\n const snippet = text.trim().slice(0, 200);\n // A wrong merchant_id makes ABA render the checkout page instead of\n // answering the API, so HTML here almost always means bad credentials.\n if (response.ok) {\n return {\n error:\n `ABA PayWay returned a non-JSON response (HTTP ${response.status}). ` +\n `This usually means the merchant ID or API key is wrong. Response starts: ${snippet}`,\n };\n }\n return { error: `HTTP ${response.status}: ${snippet}` };\n }\n}\n\n/**\n * ABA hands merchants the full purchase URL (\"API Url\" on the credential\n * sheet), but the SDK builds endpoint paths itself. Accept either and keep\n * only the origin so pasting the sheet value verbatim works.\n */\nfunction normalizeBaseUrl(baseUrl: string): string {\n const trimmed = baseUrl.trim().replace(/\\/+$/, \"\");\n const marker = \"/api/payment-gateway\";\n const at = trimmed.indexOf(marker);\n return at === -1 ? trimmed : trimmed.slice(0, at);\n}\n\nexport class ABAPayWay {\n readonly config: Readonly<ABAConfig>;\n\n constructor(config: ABAConfig) {\n if (!config.merchantId) throw new Error(\"merchantId is required\");\n if (!config.apiKey) throw new Error(\"apiKey is required\");\n if (!config.baseUrl) throw new Error(\"baseUrl is required\");\n this.config = Object.freeze({ ...config, baseUrl: normalizeBaseUrl(config.baseUrl) });\n }\n\n async createPurchase(request: PurchaseRequest): Promise<PurchaseResponse> {\n const reqTime = getABATimestamp();\n const amount = request.amount.toFixed(2);\n const phone = request.phone ? formatPhoneForABA(request.phone) : \"\";\n // Encode once. The hash and the body must carry the identical string or\n // ABA rebuilds a different signature and rejects the request.\n const items = encodeItemsForABA(request.items);\n // Same rule as `items`: encode once, then reuse for both the hash and the\n // body. Encoding twice risks two different strings and a \"Wrong Hash.\"\n const returnDeeplink = encodeReturnDeeplinkForABA(request.returnDeeplink);\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n shipping: \"\",\n ctid: \"\",\n pwt: \"\",\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n type: \"\",\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n hash,\n });\n\n // Body-only fields. ABA does not hash `payment_gate` or `view_type`, so\n // they are appended after `hash` is built — adding them to hashParams\n // would produce a signature ABA cannot rebuild. Both are omitted entirely\n // when unset, matching the rule that ABA rejects fields it did not expect\n // (an empty `shipping` is what makes it answer \"Wrong shipping price\").\n if (request.paymentGate !== undefined) {\n body.set(\"payment_gate\", String(request.paymentGate));\n }\n if (request.viewType) body.set(\"view_type\", request.viewType);\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/purchase`;\n\n const failure = (error: string, errorCode?: string): PurchaseResponse => ({\n success: false,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n // The Checkout service answers 302 to the hosted payment page. Let it\n // be followed and the body is that page's HTML, which parses as\n // neither JSON nor an error worth reporting — so stop at the redirect\n // and read the address out of it instead.\n redirect: \"manual\",\n });\n\n // A 302 is success for the Checkout service flow: the payer finishes on\n // the page ABA points at. There is no JSON body to read, so hand back\n // the URL and let the caller poll checkStatus for the outcome.\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get(\"location\");\n if (!location) {\n return failure(\n `ABA PayWay redirected (HTTP ${response.status}) without a Location header.`,\n );\n }\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: new URL(location, this.config.baseUrl).toString(),\n };\n }\n\n // A rejected request comes back as HTTP 403 with the reason in a JSON\n // status envelope (\"Wrong Hash.\", \"End of API lifetime\"). Parse it\n // rather than dumping the raw body, so callers get a real error code.\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n const data = parsed.data;\n\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: data.checkout_url,\n abapayDeeplink: data.abapay_deeplink,\n appStoreUrl: data.app_store,\n playStoreUrl: data.play_store,\n // v3 answers in camelCase; the older API used snake_case.\n qrString: data.qrString ?? data.qr_string,\n qrImage: data.qrImage ?? data.qr_image,\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n async checkStatus(transactionId: string): Promise<StatusResponse> {\n const reqTime = getABATimestamp();\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n hash,\n });\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/check-transaction-2`;\n\n const failure = (error: string, errorCode?: string): StatusResponse => ({\n success: false,\n transactionId,\n status: \"ERROR\",\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n // v3 nests the transaction detail under `data`; the legacy shape put\n // these fields at the top level.\n const detail = parsed.data.data ?? parsed.data;\n\n const amount = toNumber(detail.total_amount ?? detail.amount);\n // `payment_currency` is an empty string until the payment settles.\n const currency = firstNonEmpty(detail.payment_currency, detail.currency);\n\n return {\n success: true,\n transactionId,\n status: mapPaymentStatus(detail.payment_status ?? detail.description),\n amount,\n currency,\n paymentTime: firstNonEmpty(detail.transaction_date, detail.payment_datetime),\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n /**\n * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`\n * header. Returns false for anything it cannot positively verify — a bad\n * signature, a malformed body, a missing key — so a caller can branch on\n * the result without a try/catch.\n *\n * `payload` may be the raw request body or the already-parsed object.\n * Unlike most gateways, ABA's scheme rebuilds the signature from the parsed\n * values rather than the raw bytes, so re-serialising the body on the way\n * in cannot invalidate it. Frameworks that hand you a parsed body are fine.\n *\n * `secret` defaults to `webhookSecret`, then to `apiKey` — ABA does not\n * issue a separate pushback secret, it signs with the merchant API key.\n */\n async verifyWebhook(\n payload: string | Record<string, unknown>,\n signature: string,\n secret?: string\n ): Promise<boolean> {\n try {\n const key = secret ?? this.config.webhookSecret ?? this.config.apiKey;\n if (!key || !signature) return false;\n\n const body: unknown = typeof payload === \"string\" ? JSON.parse(payload) : payload;\n // A JSON array or scalar has no keys to sort; only an object can be a\n // pushback body, and Array.isArray must be checked because arrays are\n // objects too.\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) return false;\n\n const expected = await hmacSha512Base64(\n buildPushbackHashBase(body as Record<string, unknown>),\n key\n );\n\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) return false;\n\n let mismatch = 0;\n for (let i = 0; i < expected.length; i++) {\n mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n return mismatch === 0;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * ABA signs pushback differently from API requests. A request hash uses a\n * fixed, documented field order (see `hash.ts`); a pushback hash instead\n * sorts the body's keys ascending and concatenates their **values** — no\n * keys, no separator — which is PHP's `ksort()` then `$b4hash .= $value`.\n *\n * Sorting the whole body rather than a fixed list means a field ABA adds\n * later is included automatically, so never narrow this to the known five\n * (`tran_id`, `apv`, `status`, `return_params`, `merchant_ref`).\n */\nfunction buildPushbackHashBase(body: Record<string, unknown>): string {\n // Default sort is by UTF-16 code unit, which matches PHP's byte-wise\n // string comparison for ABA's ASCII field names.\n return Object.keys(body)\n .sort()\n .map((key) => stringifyPushbackValue(body[key]))\n .join(\"\");\n}\n\n/**\n * ABA's reference implementation is PHP string concatenation, so values are\n * coerced the way PHP would: a missing value contributes nothing, and only a\n * nested array or object is JSON-encoded first.\n *\n * Every documented pushback field is a string — `return_params` arrives\n * already JSON-encoded as a string — so the object branch is defensive. Note\n * that PHP's `json_encode` escapes `/` as `\\/` while `JSON.stringify` does\n * not, so a nested object ABA adds later could disagree here.\n */\nfunction stringifyPushbackValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n // PHP casts true to \"1\" and false to the empty string.\n if (typeof value === \"boolean\") return value ? \"1\" : \"\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\nfunction toNumber(value: unknown): number | undefined {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = parseFloat(value);\n return Number.isNaN(parsed) ? undefined : parsed;\n }\n return undefined;\n}\n\nfunction firstNonEmpty(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return undefined;\n}\n\nfunction mapPaymentStatus(raw: string | undefined): PaymentStatus {\n const normalized = (raw ?? \"\").toUpperCase();\n if (normalized === \"APPROVED\") return \"APPROVED\";\n if (normalized === \"DECLINED\") return \"DECLINED\";\n if (normalized === \"REFUNDED\") return \"REFUNDED\";\n if (normalized === \"PENDING\") return \"PENDING\";\n return \"ERROR\";\n}\n","import type { KHQROptions } from \"./types\";\n\nconst DEFAULT_HEADER_COLOR = \"#bc271a\";\nconst QR_API_BASE = \"https://quickchart.io/qr\";\nconst CARD_SHELL_PATH = \"M189.868 10.8675H27.8677C18.4788 10.8675 10.8677 18.4787 10.8677 27.8675V287.867C10.8677 297.256 18.4788 304.867 27.8677 304.867H189.868C199.257 304.867 206.868 297.256 206.868 287.867V27.8675C206.868 18.4787 199.257 10.8675 189.868 10.8675Z\";\nconst KHQR_HEADER_BACKGROUND_PATH = \"M178.91 0C188.299 0.00000824649 195.91 7.61117 195.91 17V36.3516H196V54L178.582 37H0V17C0.00000103088 7.61116 7.61116 0 17 0H178.91Z\";\nconst KHQR_HEADER_MARK_PATHS = [\n \"M104.488 17.1027V20.5948H100.95C100.596 20.5948 100.331 20.3329 100.331 19.9836V17.1027C100.331 16.7535 100.596 16.4916 100.95 16.4916H103.781C104.223 16.4043 104.488 16.7535 104.488 17.1027Z\",\n \"M120.944 18.5H119.175C119.175 16.4047 117.494 14.746 115.371 14.746C113.69 14.746 112.274 15.7936 111.743 17.365C111.655 17.7143 111.566 18.1507 111.566 18.5V23.9999H111.478C110.505 23.9999 109.797 23.2142 109.797 22.3412V18.5C109.797 17.0159 110.416 15.5317 111.566 14.4841C112.628 13.5238 113.955 13 115.371 13C118.467 13 120.944 15.4444 120.944 18.5Z\",\n \"M120.945 24H118.467L117.848 23.3889L116.521 22.0794L114.663 20.2461H117.14L120.945 24Z\",\n \"M105.107 22.2539H99.7994C99.18 22.2539 98.6492 21.7301 98.6492 21.119V15.8809C98.6492 15.2698 99.18 14.746 99.7994 14.746H105.107C105.727 14.746 106.257 15.2698 106.257 15.8809V21.119L108.027 22.865V14.6587C108.027 13.6984 107.231 13 106.346 13H98.6492C97.6756 13 96.9683 13.7857 96.9683 14.6587V22.2539C96.9683 23.2142 97.7642 23.9126 98.6492 23.9126H106.877L105.107 22.2539Z\",\n \"M83.6093 23.9999H81.1318L76.0005 18.8492V23.9999H73.9658V13H76.0005V17.8888L80.9553 13H83.3436L78.0356 18.2381L83.6093 23.9999Z\",\n \"M92.898 13H94.8446V23.9999H92.898V19.1984H87.2358V23.9999H85.2012V13H87.2358V17.6269H92.898V13Z\",\n].map((path) => `<path d=\"${path}\" fill=\"#ffffff\"/>`).join(\"\");\n\nfunction formatAmount(amount: number, currency: string): string {\n if (currency === \"KHR\") {\n return amount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 }) + \" KHR\";\n }\n return \"$\" + amount.toFixed(2);\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nexport async function generateKHQR(options: KHQROptions): Promise<string> {\n const {\n emvData,\n amount,\n currency,\n merchantName,\n headerColor = DEFAULT_HEADER_COLOR,\n } = options;\n\n let qrSvgContent = \"\";\n try {\n const qrUrl = `${QR_API_BASE}?text=${encodeURIComponent(emvData)}&size=280&margin=1&format=svg`;\n const qrResponse = await fetch(qrUrl);\n if (qrResponse.ok) qrSvgContent = await qrResponse.text();\n } catch { /* fallback: empty QR area */ }\n\n // Keep the fetched QR's own <svg> wrapper intact (only strip a leading XML\n // declaration) — quickchart draws the QR modules in a small unit grid (e.g.\n // viewBox=\"0 0 43 43\") and relies on its own width/height=\"280\" to scale\n // that up. Stripping the wrapper loses that scale, rendering the QR at\n // native module size (a few percent of the intended 280px).\n const innerQr = qrSvgContent.replace(/<\\?xml[^>]*\\?>/g, \"\");\n\n const formattedAmount = formatAmount(amount, currency);\n const safeMerchantName = escapeXml(merchantName);\n\n // The QR itself is clipped and translated in two nested <g>s rather than\n // one: putting both `clip-path` and `transform` on the same element shifts\n // the clip rect's coordinates by that same transform, clipping away most\n // of the actual QR pattern.\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 218 316\" width=\"218\" height=\"316\" role=\"img\" aria-label=\"PayWay KHQR payment card\">\n <defs>\n <filter id=\"card-shadow\" x=\"0\" y=\"0\" width=\"217.735\" height=\"315.735\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"5.43375\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect1_dropShadow\" result=\"shape\"/>\n </filter>\n <clipPath id=\"card-clip\"><path d=\"${CARD_SHELL_PATH}\"/></clipPath>\n <clipPath id=\"qr-clip\"><rect id=\"qr-area\" x=\"37\" y=\"145\" width=\"144\" height=\"144\"/></clipPath>\n </defs>\n <rect width=\"218\" height=\"316\" fill=\"#ffffff\"/>\n <g filter=\"url(#card-shadow)\">\n <path d=\"${CARD_SHELL_PATH}\" fill=\"#ffffff\"/>\n </g>\n <g clip-path=\"url(#card-clip)\">\n <g id=\"khqr-header-logo\" transform=\"translate(11,11)\" aria-label=\"KHQR logo\">\n <path d=\"${KHQR_HEADER_BACKGROUND_PATH}\" fill=\"${headerColor}\"/>\n ${KHQR_HEADER_MARK_PATHS}\n </g>\n <text x=\"51\" y=\"81\" fill=\"#111111\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"10\">${safeMerchantName}</text>\n <text x=\"51\" y=\"108\" fill=\"#000000\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"20\" font-weight=\"500\">${formattedAmount}</text>\n <line x1=\"11\" y1=\"124\" x2=\"207\" y2=\"124\" stroke=\"#8a8a8a\" stroke-width=\"1\" stroke-dasharray=\"4 5\"/>\n <rect x=\"37\" y=\"145\" width=\"144\" height=\"144\" fill=\"#ffffff\"/>\n <g clip-path=\"url(#qr-clip)\">\n <g transform=\"translate(37,145) scale(0.5142857143)\">\n ${innerQr || '<svg width=\"280\" height=\"280\"><rect width=\"280\" height=\"280\" fill=\"#f5f5f5\"/><text x=\"140\" y=\"145\" text-anchor=\"middle\" fill=\"#777777\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"16\">QR Code</text></svg>'}\n </g>\n </g>\n <g aria-label=\"KHQR brand mark\">\n <circle cx=\"109\" cy=\"217\" r=\"17\" fill=\"#ffffff\"/>\n <circle cx=\"109\" cy=\"217\" r=\"13\" fill=\"${headerColor}\"/>\n <path d=\"M102 213h3v-3h8v3h3v8h-3v3h-8v-3h-3z\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/>\n <path d=\"M109 213v8M105 217h8\" stroke=\"#ffffff\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </g>\n </g>\n</svg>`;\n\n return \"data:image/svg+xml;base64,\" + btoa(svg);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/hash.ts","../src/response.ts","../src/utils.ts","../src/client.ts","../src/khqr.ts"],"sourcesContent":["import type { HashParams } from \"./types\";\n\n/**\n * Base64 of an HMAC-SHA512 digest — the one primitive ABA uses everywhere,\n * both for request signatures and for pushback verification. Kept in one\n * place so the two callers cannot drift apart.\n */\nexport async function hmacSha512Base64(message: string, secret: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-512\" },\n false,\n [\"sign\"]\n );\n const signature = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(message));\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\nexport async function generateABAHash(\n params: HashParams,\n publicKey: string\n): Promise<string> {\n // ABA requires parameters concatenated in this EXACT order\n const hashString = [\n params.req_time,\n params.merchant_id,\n params.tran_id,\n params.amount ?? \"\",\n params.items ?? \"\",\n params.shipping ?? \"\",\n params.ctid ?? \"\",\n params.pwt ?? \"\",\n params.firstname ?? \"\",\n params.lastname ?? \"\",\n params.email ?? \"\",\n params.phone ?? \"\",\n params.type ?? \"\",\n params.payment_option ?? \"\",\n params.return_url ?? \"\",\n params.cancel_url ?? \"\",\n params.continue_success_url ?? \"\",\n params.return_deeplink ?? \"\",\n params.currency ?? \"\",\n params.custom_fields ?? \"\",\n params.return_params ?? \"\",\n ].join(\"\");\n\n return hmacSha512Base64(hashString, publicKey);\n}\n","/**\n * ABA wraps every reply in a status envelope, and it ships two shapes.\n *\n * Legacy (`check-transaction`, older purchase):\n * { \"status\": 6, \"description\": \"tran_id not found\" }\n *\n * v3 (`check-transaction-2`, current purchase):\n * { \"status\": { \"code\": \"00\", \"message\": \"Success!\", ... }, \"description\": \"success\" }\n *\n * Note the success code is the *string* `\"00\"` in v3 and the *number* `0` in\n * the legacy shape, while failures are plain numbers (1, 5, 6, 21) in both.\n * A bare `data.status !== 0` therefore reads every v3 success as a failure,\n * which is what made a working sandbox key look broken. Normalise both here\n * so the client only ever branches on `ok`.\n */\n\nexport interface AbaStatus {\n /** Status code as a string, e.g. \"00\" for success, \"21\" for an expired key. */\n readonly code: string;\n readonly ok: boolean;\n readonly message: string;\n}\n\nexport function readAbaStatus(data: unknown): AbaStatus {\n const body = (data ?? {}) as Record<string, unknown>;\n const raw = body.status;\n\n let code: string;\n let message: string;\n\n if (raw !== null && typeof raw === \"object\") {\n const nested = raw as Record<string, unknown>;\n code = nested.code == null ? \"\" : String(nested.code);\n message = firstString(nested.message, body.description);\n } else {\n code = raw == null ? \"\" : String(raw);\n message = firstString(body.description, body.message);\n }\n\n return { code, ok: isSuccessCode(code), message: message || \"Unknown error\" };\n}\n\n/**\n * Success is 0, however ABA spells it: `0`, `\"0\"`, or v3's `\"00\"`. Guard the\n * empty string explicitly — `Number(\"\")` is 0, so a missing status would\n * otherwise read as success.\n */\nfunction isSuccessCode(code: string): boolean {\n if (code === \"\") return false;\n return Number(code) === 0;\n}\n\nfunction firstString(...values: unknown[]): string {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return \"\";\n}\n","import type { PurchaseItem, ReturnDeeplink } from \"./types\";\n\nexport function generateTransactionId(): string {\n const timestamp = Date.now().toString(36).toUpperCase();\n const random = Math.random().toString(36).substring(2, 6).toUpperCase();\n return `EA${timestamp}${random}`.substring(0, 20);\n}\n\n// ABA expects req_time in UTC (YYYYMMDDHHmmss). Using the server's local clock\n// makes the request look expired from any machine that is not set to UTC.\nexport function getABATimestamp(): string {\n const now = new Date();\n const pad = (n: number) => n.toString().padStart(2, \"0\");\n return (\n now.getUTCFullYear().toString() +\n pad(now.getUTCMonth() + 1) +\n pad(now.getUTCDate()) +\n pad(now.getUTCHours()) +\n pad(now.getUTCMinutes()) +\n pad(now.getUTCSeconds())\n );\n}\n\nexport function formatPhoneForABA(phone: string): string {\n if (!phone) return \"\";\n const cleaned = phone.replace(/[\\s-]/g, \"\");\n if (cleaned.startsWith(\"+855\")) return \"0\" + cleaned.slice(4);\n if (cleaned.startsWith(\"855\")) return \"0\" + cleaned.slice(3);\n return cleaned;\n}\n\nexport function getQRExpiration(minutes = 3): Date {\n return new Date(Date.now() + minutes * 60 * 1000);\n}\n\n/**\n * Base64 for arbitrary text. `btoa` alone throws on anything outside Latin-1,\n * and item names here can be Khmer or Chinese, so encode to UTF-8 bytes first.\n */\nexport function toBase64Utf8(text: string): string {\n const bytes = new TextEncoder().encode(text);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * ABA wants `items` as base64-encoded JSON. An array is encoded; a string is\n * passed through unchanged so existing callers keep working.\n */\nexport function encodeItemsForABA(\n items: string | readonly PurchaseItem[] | undefined\n): string {\n if (items == null) return \"\";\n if (typeof items === \"string\") return items;\n return toBase64Utf8(JSON.stringify(items));\n}\n\n/**\n * ABA wants `return_deeplink` as base64-encoded JSON naming the schemes that\n * bring the payer back from ABA Mobile to your app:\n *\n * { \"ios_scheme\": \"myapp://order/42\", \"android_scheme\": \"myapp://order/42\" }\n *\n * Without it the payer finishes in ABA Mobile and is stranded there. Encode an\n * object; pass a string through unchanged, so callers who already base64'd it\n * themselves keep working.\n */\nexport function encodeReturnDeeplinkForABA(\n deeplink: string | ReturnDeeplink | undefined\n): string {\n if (deeplink == null) return \"\";\n if (typeof deeplink === \"string\") return deeplink;\n return toBase64Utf8(JSON.stringify(deeplink));\n}\n","import type {\n ABAConfig,\n PurchaseRequest,\n PurchaseResponse,\n StatusResponse,\n CloseTransactionResponse,\n PaymentStatus,\n} from \"./types\";\nimport { generateABAHash, hmacSha512Base64 } from \"./hash\";\nimport { readAbaStatus } from \"./response\";\nimport {\n getABATimestamp,\n formatPhoneForABA,\n encodeItemsForABA,\n encodeReturnDeeplinkForABA,\n} from \"./utils\";\n\n/**\n * ABA does not always answer with JSON. Bad credentials get an HTML error page,\n * and a gateway hiccup gets plain text. Parsing blindly turns those into\n * \"Unexpected token '<'\", which tells the operator nothing about the real\n * problem, so read the body as text and report what actually came back.\n */\nasync function parseAbaJson(response: Response): Promise<{ data?: any; error?: string }> {\n const text = await response.text();\n try {\n return { data: JSON.parse(text) };\n } catch {\n const snippet = text.trim().slice(0, 200);\n // A wrong merchant_id makes ABA render the checkout page instead of\n // answering the API, so HTML here almost always means bad credentials.\n if (response.ok) {\n return {\n error:\n `ABA PayWay returned a non-JSON response (HTTP ${response.status}). ` +\n `This usually means the merchant ID or API key is wrong. Response starts: ${snippet}`,\n };\n }\n return { error: `HTTP ${response.status}: ${snippet}` };\n }\n}\n\n/**\n * ABA hands merchants the full purchase URL (\"API Url\" on the credential\n * sheet), but the SDK builds endpoint paths itself. Accept either and keep\n * only the origin so pasting the sheet value verbatim works.\n */\nfunction normalizeBaseUrl(baseUrl: string): string {\n const trimmed = baseUrl.trim().replace(/\\/+$/, \"\");\n const marker = \"/api/payment-gateway\";\n const at = trimmed.indexOf(marker);\n return at === -1 ? trimmed : trimmed.slice(0, at);\n}\n\nexport class ABAPayWay {\n readonly config: Readonly<ABAConfig>;\n\n constructor(config: ABAConfig) {\n if (!config.merchantId) throw new Error(\"merchantId is required\");\n if (!config.apiKey) throw new Error(\"apiKey is required\");\n if (!config.baseUrl) throw new Error(\"baseUrl is required\");\n this.config = Object.freeze({ ...config, baseUrl: normalizeBaseUrl(config.baseUrl) });\n }\n\n async createPurchase(request: PurchaseRequest): Promise<PurchaseResponse> {\n const reqTime = getABATimestamp();\n const amount = request.amount.toFixed(2);\n const phone = request.phone ? formatPhoneForABA(request.phone) : \"\";\n // Encode once. The hash and the body must carry the identical string or\n // ABA rebuilds a different signature and rejects the request.\n const items = encodeItemsForABA(request.items);\n // Same rule as `items`: encode once, then reuse for both the hash and the\n // body. Encoding twice risks two different strings and a \"Wrong Hash.\"\n const returnDeeplink = encodeReturnDeeplinkForABA(request.returnDeeplink);\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n shipping: \"\",\n ctid: \"\",\n pwt: \"\",\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n type: \"\",\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: request.transactionId,\n amount,\n items,\n firstname: request.firstName ?? \"\",\n lastname: request.lastName ?? \"\",\n email: request.email ?? \"\",\n phone,\n payment_option: request.paymentOption ?? \"\",\n return_url: request.returnUrl ?? \"\",\n cancel_url: request.cancelUrl ?? \"\",\n continue_success_url: request.continueSuccessUrl ?? \"\",\n return_deeplink: returnDeeplink,\n currency: request.currency,\n custom_fields: request.customFields ?? \"\",\n return_params: request.returnParams ?? \"\",\n hash,\n });\n\n // Body-only fields. ABA does not hash `payment_gate` or `view_type`, so\n // they are appended after `hash` is built — adding them to hashParams\n // would produce a signature ABA cannot rebuild. Both are omitted entirely\n // when unset, matching the rule that ABA rejects fields it did not expect\n // (an empty `shipping` is what makes it answer \"Wrong shipping price\").\n if (request.paymentGate !== undefined) {\n body.set(\"payment_gate\", String(request.paymentGate));\n }\n if (request.viewType) body.set(\"view_type\", request.viewType);\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/purchase`;\n\n const failure = (error: string, errorCode?: string): PurchaseResponse => ({\n success: false,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n // The Checkout service answers 302 to the hosted payment page. Let it\n // be followed and the body is that page's HTML, which parses as\n // neither JSON nor an error worth reporting — so stop at the redirect\n // and read the address out of it instead.\n redirect: \"manual\",\n });\n\n // A 302 is success for the Checkout service flow: the payer finishes on\n // the page ABA points at. There is no JSON body to read, so hand back\n // the URL and let the caller poll checkStatus for the outcome.\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get(\"location\");\n if (!location) {\n return failure(\n `ABA PayWay redirected (HTTP ${response.status}) without a Location header.`,\n );\n }\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: new URL(location, this.config.baseUrl).toString(),\n };\n }\n\n // A rejected request comes back as HTTP 403 with the reason in a JSON\n // status envelope (\"Wrong Hash.\", \"End of API lifetime\"). Parse it\n // rather than dumping the raw body, so callers get a real error code.\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n const data = parsed.data;\n\n return {\n success: true,\n transactionId: request.transactionId,\n amount: request.amount,\n currency: request.currency,\n checkoutUrl: data.checkout_url,\n abapayDeeplink: data.abapay_deeplink,\n appStoreUrl: data.app_store,\n playStoreUrl: data.play_store,\n // v3 answers in camelCase; the older API used snake_case.\n qrString: data.qrString ?? data.qr_string,\n qrImage: data.qrImage ?? data.qr_image,\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n async checkStatus(transactionId: string): Promise<StatusResponse> {\n const reqTime = getABATimestamp();\n\n const hashParams = {\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n };\n\n const hash = await generateABAHash(hashParams, this.config.apiKey);\n\n const body = new URLSearchParams({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n hash,\n });\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/check-transaction-2`;\n\n const failure = (error: string, errorCode?: string): StatusResponse => ({\n success: false,\n transactionId,\n status: \"ERROR\",\n error,\n errorCode,\n });\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined);\n\n // v3 nests the transaction detail under `data`; the legacy shape put\n // these fields at the top level.\n const detail = parsed.data.data ?? parsed.data;\n\n const amount = toNumber(detail.total_amount ?? detail.amount);\n // `payment_currency` is an empty string until the payment settles.\n const currency = firstNonEmpty(detail.payment_currency, detail.currency);\n\n return {\n success: true,\n transactionId,\n status: mapPaymentStatus(detail.payment_status ?? detail.description),\n amount,\n currency,\n paymentTime: firstNonEmpty(detail.transaction_date, detail.payment_datetime),\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n async closeTransaction(transactionId: string): Promise<CloseTransactionResponse> {\n const failure = (\n error: string,\n code?: string,\n message?: string\n ): CloseTransactionResponse => ({\n success: false,\n transactionId: transactionId ?? \"\",\n code,\n message: message ?? error,\n error,\n });\n\n if (!transactionId || !transactionId.trim()) {\n return failure(\"transactionId is required\");\n }\n\n const reqTime = getABATimestamp();\n const hash = await hmacSha512Base64(\n `${reqTime}${this.config.merchantId}${transactionId}`,\n this.config.apiKey\n );\n\n const body = JSON.stringify({\n req_time: reqTime,\n merchant_id: this.config.merchantId,\n tran_id: transactionId,\n hash,\n });\n\n const url = `${this.config.baseUrl}/api/payment-gateway/v1/payments/close-transaction`;\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n const parsed = await parseAbaJson(response);\n if (parsed.error) return failure(parsed.error);\n\n const status = readAbaStatus(parsed.data);\n if (!status.ok) return failure(status.message, status.code || undefined, status.message);\n\n return {\n success: true,\n transactionId: parsed.data?.status?.tran_id ?? transactionId,\n code: status.code,\n message: status.message,\n };\n } catch (err) {\n return failure(err instanceof Error ? err.message : \"Unknown error\");\n }\n }\n\n /**\n * Verify an ABA pushback (callback) against its `X-PayWay-HMAC-SHA512`\n * header. Returns false for anything it cannot positively verify — a bad\n * signature, a malformed body, a missing key — so a caller can branch on\n * the result without a try/catch.\n *\n * `payload` may be the raw request body or the already-parsed object.\n * Unlike most gateways, ABA's scheme rebuilds the signature from the parsed\n * values rather than the raw bytes, so re-serialising the body on the way\n * in cannot invalidate it. Frameworks that hand you a parsed body are fine.\n *\n * `secret` defaults to `webhookSecret`, then to `apiKey` — ABA does not\n * issue a separate pushback secret, it signs with the merchant API key.\n */\n async verifyWebhook(\n payload: string | Record<string, unknown>,\n signature: string,\n secret?: string\n ): Promise<boolean> {\n try {\n const key = secret ?? this.config.webhookSecret ?? this.config.apiKey;\n if (!key || !signature) return false;\n\n const body: unknown = typeof payload === \"string\" ? JSON.parse(payload) : payload;\n // A JSON array or scalar has no keys to sort; only an object can be a\n // pushback body, and Array.isArray must be checked because arrays are\n // objects too.\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) return false;\n\n const expected = await hmacSha512Base64(\n buildPushbackHashBase(body as Record<string, unknown>),\n key\n );\n\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) return false;\n\n let mismatch = 0;\n for (let i = 0; i < expected.length; i++) {\n mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n return mismatch === 0;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * ABA signs pushback differently from API requests. A request hash uses a\n * fixed, documented field order (see `hash.ts`); a pushback hash instead\n * sorts the body's keys ascending and concatenates their **values** — no\n * keys, no separator — which is PHP's `ksort()` then `$b4hash .= $value`.\n *\n * Sorting the whole body rather than a fixed list means a field ABA adds\n * later is included automatically, so never narrow this to the known five\n * (`tran_id`, `apv`, `status`, `return_params`, `merchant_ref`).\n */\nfunction buildPushbackHashBase(body: Record<string, unknown>): string {\n // Default sort is by UTF-16 code unit, which matches PHP's byte-wise\n // string comparison for ABA's ASCII field names.\n return Object.keys(body)\n .sort()\n .map((key) => stringifyPushbackValue(body[key]))\n .join(\"\");\n}\n\n/**\n * ABA's reference implementation is PHP string concatenation, so values are\n * coerced the way PHP would: a missing value contributes nothing, and only a\n * nested array or object is JSON-encoded first.\n *\n * Every documented pushback field is a string — `return_params` arrives\n * already JSON-encoded as a string — so the object branch is defensive. Note\n * that PHP's `json_encode` escapes `/` as `\\/` while `JSON.stringify` does\n * not, so a nested object ABA adds later could disagree here.\n */\nfunction stringifyPushbackValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n // PHP casts true to \"1\" and false to the empty string.\n if (typeof value === \"boolean\") return value ? \"1\" : \"\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\nfunction toNumber(value: unknown): number | undefined {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = parseFloat(value);\n return Number.isNaN(parsed) ? undefined : parsed;\n }\n return undefined;\n}\n\nfunction firstNonEmpty(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value !== \"\") return value;\n }\n return undefined;\n}\n\nfunction mapPaymentStatus(raw: string | undefined): PaymentStatus {\n const normalized = (raw ?? \"\").toUpperCase();\n if (normalized === \"APPROVED\") return \"APPROVED\";\n if (normalized === \"DECLINED\") return \"DECLINED\";\n if (normalized === \"REFUNDED\") return \"REFUNDED\";\n if (normalized === \"CANCELLED\" || normalized === \"CANCELED\") return \"CANCELLED\";\n if (normalized === \"PENDING\") return \"PENDING\";\n return \"ERROR\";\n}\n","import type { KHQROptions } from \"./types\";\n\nconst DEFAULT_HEADER_COLOR = \"#bc271a\";\nconst QR_API_BASE = \"https://quickchart.io/qr\";\nconst CARD_SHELL_PATH = \"M189.868 10.8675H27.8677C18.4788 10.8675 10.8677 18.4787 10.8677 27.8675V287.867C10.8677 297.256 18.4788 304.867 27.8677 304.867H189.868C199.257 304.867 206.868 297.256 206.868 287.867V27.8675C206.868 18.4787 199.257 10.8675 189.868 10.8675Z\";\nconst KHQR_HEADER_BACKGROUND_PATH = \"M178.91 0C188.299 0.00000824649 195.91 7.61117 195.91 17V36.3516H196V54L178.582 37H0V17C0.00000103088 7.61116 7.61116 0 17 0H178.91Z\";\nconst KHQR_HEADER_MARK_PATHS = [\n \"M104.488 17.1027V20.5948H100.95C100.596 20.5948 100.331 20.3329 100.331 19.9836V17.1027C100.331 16.7535 100.596 16.4916 100.95 16.4916H103.781C104.223 16.4043 104.488 16.7535 104.488 17.1027Z\",\n \"M120.944 18.5H119.175C119.175 16.4047 117.494 14.746 115.371 14.746C113.69 14.746 112.274 15.7936 111.743 17.365C111.655 17.7143 111.566 18.1507 111.566 18.5V23.9999H111.478C110.505 23.9999 109.797 23.2142 109.797 22.3412V18.5C109.797 17.0159 110.416 15.5317 111.566 14.4841C112.628 13.5238 113.955 13 115.371 13C118.467 13 120.944 15.4444 120.944 18.5Z\",\n \"M120.945 24H118.467L117.848 23.3889L116.521 22.0794L114.663 20.2461H117.14L120.945 24Z\",\n \"M105.107 22.2539H99.7994C99.18 22.2539 98.6492 21.7301 98.6492 21.119V15.8809C98.6492 15.2698 99.18 14.746 99.7994 14.746H105.107C105.727 14.746 106.257 15.2698 106.257 15.8809V21.119L108.027 22.865V14.6587C108.027 13.6984 107.231 13 106.346 13H98.6492C97.6756 13 96.9683 13.7857 96.9683 14.6587V22.2539C96.9683 23.2142 97.7642 23.9126 98.6492 23.9126H106.877L105.107 22.2539Z\",\n \"M83.6093 23.9999H81.1318L76.0005 18.8492V23.9999H73.9658V13H76.0005V17.8888L80.9553 13H83.3436L78.0356 18.2381L83.6093 23.9999Z\",\n \"M92.898 13H94.8446V23.9999H92.898V19.1984H87.2358V23.9999H85.2012V13H87.2358V17.6269H92.898V13Z\",\n].map((path) => `<path d=\"${path}\" fill=\"#ffffff\"/>`).join(\"\");\n\nfunction formatAmount(amount: number, currency: string): string {\n if (currency === \"KHR\") {\n return amount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 }) + \" KHR\";\n }\n return \"$\" + amount.toFixed(2);\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nexport async function generateKHQR(options: KHQROptions): Promise<string> {\n const {\n emvData,\n amount,\n currency,\n merchantName,\n headerColor = DEFAULT_HEADER_COLOR,\n } = options;\n\n let qrSvgContent = \"\";\n try {\n const qrUrl = `${QR_API_BASE}?text=${encodeURIComponent(emvData)}&size=280&margin=1&format=svg`;\n const qrResponse = await fetch(qrUrl);\n if (qrResponse.ok) qrSvgContent = await qrResponse.text();\n } catch { /* fallback: empty QR area */ }\n\n // Keep the fetched QR's own <svg> wrapper intact (only strip a leading XML\n // declaration) — quickchart draws the QR modules in a small unit grid (e.g.\n // viewBox=\"0 0 43 43\") and relies on its own width/height=\"280\" to scale\n // that up. Stripping the wrapper loses that scale, rendering the QR at\n // native module size (a few percent of the intended 280px).\n const innerQr = qrSvgContent.replace(/<\\?xml[^>]*\\?>/g, \"\");\n\n const formattedAmount = formatAmount(amount, currency);\n const safeMerchantName = escapeXml(merchantName);\n\n // The QR itself is clipped and translated in two nested <g>s rather than\n // one: putting both `clip-path` and `transform` on the same element shifts\n // the clip rect's coordinates by that same transform, clipping away most\n // of the actual QR pattern.\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 218 316\" width=\"218\" height=\"316\" role=\"img\" aria-label=\"PayWay KHQR payment card\">\n <defs>\n <filter id=\"card-shadow\" x=\"0\" y=\"0\" width=\"217.735\" height=\"315.735\" filterUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\n <feFlood flood-opacity=\"0\" result=\"BackgroundImageFix\"/>\n <feColorMatrix in=\"SourceAlpha\" type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0\" result=\"hardAlpha\"/>\n <feOffset/>\n <feGaussianBlur stdDeviation=\"5.43375\"/>\n <feComposite in2=\"hardAlpha\" operator=\"out\"/>\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0\"/>\n <feBlend mode=\"normal\" in2=\"BackgroundImageFix\" result=\"effect1_dropShadow\"/>\n <feBlend mode=\"normal\" in=\"SourceGraphic\" in2=\"effect1_dropShadow\" result=\"shape\"/>\n </filter>\n <clipPath id=\"card-clip\"><path d=\"${CARD_SHELL_PATH}\"/></clipPath>\n <clipPath id=\"qr-clip\"><rect id=\"qr-area\" x=\"37\" y=\"145\" width=\"144\" height=\"144\"/></clipPath>\n </defs>\n <rect width=\"218\" height=\"316\" fill=\"#ffffff\"/>\n <g filter=\"url(#card-shadow)\">\n <path d=\"${CARD_SHELL_PATH}\" fill=\"#ffffff\"/>\n </g>\n <g clip-path=\"url(#card-clip)\">\n <g id=\"khqr-header-logo\" transform=\"translate(11,11)\" aria-label=\"KHQR logo\">\n <path d=\"${KHQR_HEADER_BACKGROUND_PATH}\" fill=\"${headerColor}\"/>\n ${KHQR_HEADER_MARK_PATHS}\n </g>\n <text x=\"51\" y=\"81\" fill=\"#111111\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"10\">${safeMerchantName}</text>\n <text x=\"51\" y=\"108\" fill=\"#000000\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"20\" font-weight=\"500\">${formattedAmount}</text>\n <line x1=\"11\" y1=\"124\" x2=\"207\" y2=\"124\" stroke=\"#8a8a8a\" stroke-width=\"1\" stroke-dasharray=\"4 5\"/>\n <rect x=\"37\" y=\"145\" width=\"144\" height=\"144\" fill=\"#ffffff\"/>\n <g clip-path=\"url(#qr-clip)\">\n <g transform=\"translate(37,145) scale(0.5142857143)\">\n ${innerQr || '<svg width=\"280\" height=\"280\"><rect width=\"280\" height=\"280\" fill=\"#f5f5f5\"/><text x=\"140\" y=\"145\" text-anchor=\"middle\" fill=\"#777777\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"16\">QR Code</text></svg>'}\n </g>\n </g>\n <g aria-label=\"KHQR brand mark\">\n <circle cx=\"109\" cy=\"217\" r=\"17\" fill=\"#ffffff\"/>\n <circle cx=\"109\" cy=\"217\" r=\"13\" fill=\"${headerColor}\"/>\n <path d=\"M102 213h3v-3h8v3h3v8h-3v3h-8v-3h-3z\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/>\n <path d=\"M109 213v8M105 217h8\" stroke=\"#ffffff\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </g>\n </g>\n</svg>`;\n\n return \"data:image/svg+xml;base64,\" + btoa(svg);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aba-payway-sdk-unofficial",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Unofficial ABA PayWay payment gateway SDK for Cambodia — KHQR generation, purchase creation, status checking, and webhook verification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",