@laranex/myanmar-payments 4.0.0-dev.2 → 4.0.0-dev.4

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +1 -1
  3. package/dist/cjs/aya-pay/aya-pay.d.ts +5 -2
  4. package/dist/cjs/aya-pay/aya-pay.js +7 -2
  5. package/dist/cjs/core/amount.d.ts +4 -3
  6. package/dist/cjs/core/amount.js +13 -4
  7. package/dist/cjs/core/callback.d.ts +14 -8
  8. package/dist/cjs/core/callback.js +19 -11
  9. package/dist/cjs/core/crypto.d.ts +6 -1
  10. package/dist/cjs/core/crypto.js +17 -3
  11. package/dist/cjs/core/env.d.ts +7 -0
  12. package/dist/cjs/core/env.js +14 -2
  13. package/dist/cjs/core/json.d.ts +2 -2
  14. package/dist/cjs/core/json.js +3 -3
  15. package/dist/cjs/core/status.d.ts +3 -3
  16. package/dist/cjs/core/status.js +2 -2
  17. package/dist/cjs/core/values.d.ts +2 -0
  18. package/dist/cjs/core/values.js +5 -0
  19. package/dist/cjs/cyber-source/cyber-source.d.ts +7 -3
  20. package/dist/cjs/cyber-source/cyber-source.js +27 -13
  21. package/dist/cjs/kbz-pay/kbz-pay.d.ts +6 -2
  22. package/dist/cjs/kbz-pay/kbz-pay.js +2 -2
  23. package/dist/cjs/kbz-pay/signer.d.ts +4 -1
  24. package/dist/cjs/kbz-pay/signer.js +7 -2
  25. package/dist/cjs/wave-money/wave-money.d.ts +6 -3
  26. package/dist/cjs/wave-money/wave-money.js +6 -4
  27. package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +5 -2
  28. package/dist/cjs/yoma-mmqr/yoma-mmqr.js +8 -5
  29. package/dist/esm/aya-pay/aya-pay.d.ts +5 -2
  30. package/dist/esm/aya-pay/aya-pay.js +9 -4
  31. package/dist/esm/core/amount.d.ts +4 -3
  32. package/dist/esm/core/amount.js +13 -4
  33. package/dist/esm/core/callback.d.ts +14 -8
  34. package/dist/esm/core/callback.js +19 -11
  35. package/dist/esm/core/crypto.d.ts +6 -1
  36. package/dist/esm/core/crypto.js +17 -3
  37. package/dist/esm/core/env.d.ts +7 -0
  38. package/dist/esm/core/env.js +13 -2
  39. package/dist/esm/core/json.d.ts +2 -2
  40. package/dist/esm/core/json.js +3 -3
  41. package/dist/esm/core/status.d.ts +3 -3
  42. package/dist/esm/core/status.js +2 -2
  43. package/dist/esm/core/values.d.ts +2 -0
  44. package/dist/esm/core/values.js +4 -0
  45. package/dist/esm/cyber-source/cyber-source.d.ts +7 -3
  46. package/dist/esm/cyber-source/cyber-source.js +29 -15
  47. package/dist/esm/kbz-pay/kbz-pay.d.ts +6 -2
  48. package/dist/esm/kbz-pay/kbz-pay.js +3 -3
  49. package/dist/esm/kbz-pay/signer.d.ts +4 -1
  50. package/dist/esm/kbz-pay/signer.js +8 -3
  51. package/dist/esm/wave-money/wave-money.d.ts +6 -3
  52. package/dist/esm/wave-money/wave-money.js +8 -6
  53. package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +5 -2
  54. package/dist/esm/yoma-mmqr/yoma-mmqr.js +10 -7
  55. package/package.json +1 -1
  56. package/skills/node-myanmar-payments/SKILL.md +22 -20
@@ -1,7 +1,7 @@
1
1
  import {} from '../core/amount.js';
2
2
  import { Acknowledgement, CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
3
3
  import { queryEscape, randomHex } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
4
+ import { defaultEnv, envFirst, envSandbox, parseSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
5
  import { ApiError, SignatureVerificationError } from '../core/errors.js';
6
6
  import { httpClientFrom, Transport, } from '../core/http.js';
7
7
  import { toPlainObject } from '../core/json.js';
@@ -30,7 +30,7 @@ export class KbzPayConfig {
30
30
  this.appId = requireSetting('kbz_pay', 'app_id', options.appId);
31
31
  this.appKey = requireSetting('kbz_pay', 'app_key', options.appKey);
32
32
  this.merchantCode = requireSetting('kbz_pay', 'merchant_code', options.merchantCode);
33
- this.sandbox = options.sandbox ?? true;
33
+ this.sandbox = parseSandbox(options.sandbox);
34
34
  this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ??
35
35
  (this.sandbox ? KbzPayConfig.SANDBOX_API_URL : KbzPayConfig.PRODUCTION_API_URL));
36
36
  this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ??
@@ -57,7 +57,7 @@ const STATUSES = {
57
57
  WAIT_PAY: 'pending',
58
58
  PAYING: 'pending',
59
59
  PAY_FAILED: 'failed',
60
- ORDER_CLOSED: 'cancelled',
60
+ ORDER_CLOSED: 'canceled',
61
61
  ORDER_EXPIRED: 'expired',
62
62
  };
63
63
  /**
@@ -10,6 +10,9 @@ export declare class KbzPaySigner {
10
10
  signString(fields: Readonly<Record<string, unknown>>): string;
11
11
  /** The uppercase SHA-256 signature of `fields`. */
12
12
  sign(fields: Readonly<Record<string, unknown>>): string;
13
- /** Whether `fields.sign` matches, compared in constant time. */
13
+ /**
14
+ * Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
15
+ * fields holding an object or array fail rather than being partly trusted.
16
+ */
14
17
  verify(fields: Readonly<Record<string, unknown>>): boolean;
15
18
  }
@@ -1,5 +1,5 @@
1
1
  import { safeEqual, sha256Hex } from '../core/crypto.js';
2
- import { scalarString } from '../core/values.js';
2
+ import { isNested, scalarString } from '../core/values.js';
3
3
  /**
4
4
  * KBZ Pay's signature: every non-empty scalar field except `sign` and `sign_type`, sorted by key,
5
5
  * joined as raw `key=value` pairs (not URL-encoded), with `&key=<app key>` appended, hashed with
@@ -24,9 +24,14 @@ export class KbzPaySigner {
24
24
  sign(fields) {
25
25
  return sha256Hex(`${this.signString(fields)}&key=${this.appKey}`).toUpperCase();
26
26
  }
27
- /** Whether `fields.sign` matches, compared in constant time. */
27
+ /**
28
+ * Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
29
+ * fields holding an object or array fail rather than being partly trusted.
30
+ */
28
31
  verify(fields) {
29
32
  const sign = fields.sign;
30
- return typeof sign === 'string' && safeEqual(this.sign(fields), sign.toUpperCase());
33
+ return (typeof sign === 'string' &&
34
+ !Object.values(fields).some(isNested) &&
35
+ safeEqual(this.sign(fields), sign.toUpperCase()));
31
36
  }
32
37
  }
@@ -13,8 +13,11 @@ export interface WaveMoneyConfigOptions {
13
13
  merchantName: string;
14
14
  /** How long the customer has to pay, in seconds (default 300). */
15
15
  timeToLiveSeconds?: number | undefined;
16
- /** Use the test environment (default `true`). */
17
- sandbox?: boolean | undefined;
16
+ /**
17
+ * Use the test environment (default `true`).
18
+ * Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
19
+ */
20
+ sandbox?: boolean | string | undefined;
18
21
  /** Overrides the API base URL. */
19
22
  baseUrl?: string | undefined;
20
23
  /** Overrides the host the customer is redirected to. Wave serves it without the API port. */
@@ -104,7 +107,7 @@ export declare class WaveMoney {
104
107
  /**
105
108
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
106
109
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
107
- * optional.
110
+ * optional. A hashed field holding an object or array fails verification.
108
111
  */
109
112
  handleCallback(request: CallbackRequest): PaymentCallback;
110
113
  private hash;
@@ -1,14 +1,14 @@
1
1
  import { Amount } from '../core/amount.js';
2
2
  import { CallbackRequest, losslessBody, PaymentCallback } from '../core/callback.js';
3
3
  import { hmacSha256Hex, queryEscape, randomHex, safeEqual } from '../core/crypto.js';
4
- import { defaultEnv, envFirst, envInt, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
4
+ import { defaultEnv, envFirst, envInt, envSandbox, parseSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
5
  import { ApiError, SignatureVerificationError } from '../core/errors.js';
6
6
  import { httpClientFrom, Transport, } from '../core/http.js';
7
7
  import { isLosslessObject, toPlainObject } from '../core/json.js';
8
8
  import { RedirectPayment } from '../core/results.js';
9
9
  import { resolveStatus } from '../core/status.js';
10
10
  import { toAmount, Validator } from '../core/validate.js';
11
- import { get, optional, scalarString, trimmed } from '../core/values.js';
11
+ import { get, isNested, optional, scalarString, trimmed } from '../core/values.js';
12
12
  /**
13
13
  * Wave Money credentials and endpoints. A missing credential throws a `ConfigurationError`.
14
14
  */
@@ -37,7 +37,7 @@ export class WaveMoneyConfig {
37
37
  ttl !== undefined && Number.isInteger(ttl) && ttl > 0
38
38
  ? ttl
39
39
  : WaveMoneyConfig.DEFAULT_TIME_TO_LIVE_SECONDS;
40
- this.sandbox = options.sandbox ?? true;
40
+ this.sandbox = parseSandbox(options.sandbox);
41
41
  this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
42
42
  (this.sandbox ? WaveMoneyConfig.SANDBOX_URL : WaveMoneyConfig.PRODUCTION_URL));
43
43
  this.authenticateUrl = trimUrl(optionalSetting(options.authenticateUrl) ??
@@ -67,7 +67,7 @@ const STATUSES = {
67
67
  INSUFFICIENT_BALANCE: 'pending',
68
68
  ACCOUNT_LOCKED: 'failed',
69
69
  BILL_COLLECTION_FAILED: 'failed',
70
- PAYMENT_REQUEST_CANCELLED: 'cancelled',
70
+ PAYMENT_REQUEST_CANCELLED: 'canceled',
71
71
  TRANSACTION_TIMED_OUT: 'expired',
72
72
  SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
73
73
  };
@@ -205,13 +205,15 @@ export class WaveMoney {
205
205
  /**
206
206
  * Verifies Wave's callback. Only `PAYMENT_CONFIRMED` means the customer paid. `orderId` falls
207
207
  * back to `merchantReferenceId` when it is missing, null or empty, because Wave marks `orderId` as
208
- * optional.
208
+ * optional. A hashed field holding an object or array fails verification.
209
209
  */
210
210
  handleCallback(request) {
211
211
  const payload = losslessBody(request);
212
212
  const expected = this.hash(CALLBACK_FIELDS.map((field) => scalarString(payload[field]) ?? 'null'));
213
213
  const hashValue = payload.hashValue;
214
- if (typeof hashValue !== 'string' || !safeEqual(expected, hashValue.toLowerCase())) {
214
+ if (typeof hashValue !== 'string' ||
215
+ CALLBACK_FIELDS.some((field) => isNested(payload[field])) ||
216
+ !safeEqual(expected, hashValue.toLowerCase())) {
215
217
  throw new SignatureVerificationError('Wave Money callback hash verification failed.', toPlainObject(payload));
216
218
  }
217
219
  const gatewayStatus = trimmed(payload, 'status');
@@ -16,8 +16,11 @@ export interface YomaMmqrConfigOptions {
16
16
  webhookHashKey: string;
17
17
  /** The secret you shared with Yoma; when set, callbacks must carry it in `X-Webhook-Secret`. */
18
18
  webhookSecret?: string | undefined;
19
- /** Use the UAT payment hub (default `true`). */
20
- sandbox?: boolean | undefined;
19
+ /**
20
+ * Use the UAT payment hub (default `true`).
21
+ * Text is read like a `*_SANDBOX` variable: `false`, `0`, `f`, `no` or `off` select production.
22
+ */
23
+ sandbox?: boolean | string | undefined;
21
24
  /** Overrides the API base URL. */
22
25
  baseUrl?: string | undefined;
23
26
  /** The `{version}` segment of the API paths (default `v1rc`). */
@@ -2,14 +2,14 @@ import {} from '../core/amount.js';
2
2
  import { MemoryTokenCache } from '../core/cache.js';
3
3
  import { CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
4
4
  import { hmacSha256Hex, safeEqual, sha256Hex } from '../core/crypto.js';
5
- import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
+ import { defaultEnv, envFirst, envSandbox, parseSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
6
6
  import { ApiError, SignatureVerificationError } from '../core/errors.js';
7
7
  import { httpClientFrom, Transport, } from '../core/http.js';
8
8
  import { toPlainObject } from '../core/json.js';
9
9
  import { QrPayment } from '../core/results.js';
10
10
  import { resolveStatus } from '../core/status.js';
11
11
  import { toAmount, Validator } from '../core/validate.js';
12
- import { get, optional, trimmed } from '../core/values.js';
12
+ import { get, isNested, optional, trimmed } from '../core/values.js';
13
13
  /**
14
14
  * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
15
15
  */
@@ -33,7 +33,7 @@ export class YomaMmqrConfig {
33
33
  this.clientSecret = requireSetting('yoma_mmqr', 'client_secret', options.clientSecret);
34
34
  this.webhookHashKey = requireSetting('yoma_mmqr', 'webhook_hashkey', options.webhookHashKey);
35
35
  this.webhookSecret = optionalSetting(options.webhookSecret);
36
- this.sandbox = options.sandbox ?? true;
36
+ this.sandbox = parseSandbox(options.sandbox);
37
37
  this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
38
38
  (this.sandbox ? YomaMmqrConfig.SANDBOX_URL : YomaMmqrConfig.PRODUCTION_URL));
39
39
  this.apiVersion = optionalSetting(options.apiVersion) ?? YomaMmqrConfig.DEFAULT_API_VERSION;
@@ -158,7 +158,9 @@ export class YomaMmqr {
158
158
  const orderNumber = get(payload, 'orderNumber');
159
159
  const status = trimmed(payload, 'status');
160
160
  const expected = hmacSha256Hex(orderNumber + this.config.webhookHashKey, `orderNumber=${orderNumber}&status=${status}`);
161
- if (orderNumber === '' || !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
161
+ if (orderNumber === '' ||
162
+ isNested(payload.status) ||
163
+ !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
162
164
  throw new SignatureVerificationError('Yoma MMQR callback hash verification failed.', toPlainObject(payload));
163
165
  }
164
166
  return new PaymentCallback({
@@ -207,13 +209,14 @@ export class YomaMmqr {
207
209
  if (!response.successful() || token === '') {
208
210
  throw apiError('token', response.status, body, get(body, 'error'));
209
211
  }
210
- const expiresIn = Number.parseInt(get(body, 'expires_in'), 10);
211
- const ttl = Math.max(60, (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) - 60);
212
+ // The leading digits of `expires_in`; missing, non-numeric or 0 means an hour.
213
+ const expiresIn = Number(/^\d+/.exec(get(body, 'expires_in'))?.[0] ?? 0);
214
+ const ttl = Math.max(60, (expiresIn > 0 ? expiresIn : 3600) - 60);
212
215
  await this.cache.set(this.tokenCacheKey(), token, ttl);
213
216
  return token;
214
217
  }
215
218
  tokenCacheKey() {
216
- return `node-myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
219
+ return `myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
217
220
  }
218
221
  }
219
222
  function apiError(endpoint, status, body, errorCode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laranex/myanmar-payments",
3
- "version": "4.0.0-dev.2",
3
+ "version": "4.0.0-dev.4",
4
4
  "description": "Node.js SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed TypeScript API, ESM and CommonJS, no runtime dependencies.",
5
5
  "keywords": [
6
6
  "laranex",
@@ -11,7 +11,7 @@ metadata:
11
11
 
12
12
  ## When to use
13
13
 
14
- Use this skill when a Node.js or TypeScript app takes payments through KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR or CyberSource. Start payments and verify callbacks with the package's typed API; never build gateway signatures by hand.
14
+ Use this skill when a Node.js or TypeScript app takes payments through KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR or CyberSource. Start payments and verify callbacks with the package's typed API; never build gateway signatures by hand. In NestJS, use `@laranex/nestjs-myanmar-payments` instead; it wraps this package.
15
15
 
16
16
  ## Install
17
17
 
@@ -19,11 +19,11 @@ Use this skill when a Node.js or TypeScript app takes payments through KBZ Pay,
19
19
  npm install @laranex/myanmar-payments
20
20
  ```
21
21
 
22
- Requires Node.js 20+ and has no runtime dependencies (global `fetch`, `node:crypto`). It ships ESM and CommonJS with types. Import shared types from the root and each gateway from the root or its subpath: `@laranex/myanmar-payments/kbz-pay`, `/wave-money`, `/aya-pay`, `/yoma-mmqr`, `/cyber-source`.
22
+ Requires Node.js 20+ and has no runtime dependencies (global `fetch`, `node:crypto`); it ships ESM and CommonJS with types. Import shared types from the root and each gateway from the root or its subpath: `@laranex/myanmar-payments/kbz-pay`, `/wave-money`, `/aya-pay`, `/yoma-mmqr` and `/cyber-source`.
23
23
 
24
24
  ## Configure
25
25
 
26
- Every gateway has a config class with `fromEnv(process.env)`, reading `KBZ_PAY_*`, `WAVE_MONEY_*`, `AYA_PAY_*` (or `AYA_PGW_*`), `YOMA_MMQR_*` and `CYBER_SOURCE_*` (the same variables as the PHP, Laravel and Go packages). `sandbox` defaults to `true`; set `*_SANDBOX=false` (or `sandbox: false`) in production.
26
+ `MyanmarPayments.fromEnv(process.env)` reads `KBZ_PAY_*`, `WAVE_MONEY_*`, `AYA_PAY_*` (or `AYA_PGW_*`), `YOMA_MMQR_*` and `CYBER_SOURCE_*` (the same variables as the PHP, Go and Python SDKs). `sandbox` defaults to `true`; set `*_SANDBOX=false` (or `sandbox: false`) in production.
27
27
 
28
28
  ```ts
29
29
  import { MyanmarPayments } from '@laranex/myanmar-payments';
@@ -32,18 +32,21 @@ const payments = MyanmarPayments.fromEnv(process.env); // create once, share acr
32
32
  const kbz = payments.kbzPay(); // also waveMoney(), ayaPay(), yomaMmqr(), cyberSource()
33
33
  ```
34
34
 
35
- - Or build one gateway: `new KbzPay({ appId, appKey, merchantCode })`, `KbzPay.fromEnv()`, `new KbzPay(KbzPayConfig.fromEnv(process.env))`.
36
- - Options (second argument): `{ fetch, httpClient, timeoutMs }`; Yoma and the facade also take `tokenCache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
35
+ - Or build one gateway: `new KbzPay(new KbzPayConfig({ appId, appKey, merchantCode }))`, `KbzPay.fromEnv()` or `new KbzPay(KbzPayConfig.fromEnv(process.env))`.
36
+ - Or pass the settings directly: gateways and `new MyanmarPayments({ kbzPay: { appId, appKey, merchantCode } })` take config objects or plain option objects; a string `sandbox` such as `'false'` is read like the variable.
37
+ - Options (second argument): `{ httpClient, fetch, timeoutMs }` (default 30 seconds). Yoma and the facade also take `tokenCache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
37
38
  - A missing credential throws `ConfigurationError` (`gateway`, `key`).
38
39
 
39
40
  ## Use
40
41
 
41
42
  ### Amounts
42
43
 
43
- Amounts are `Amount.kyat(1000)`, `Amount.parse('1000.50')`, or a whole-number `number`/`bigint`; floats like `10.5` are rejected. Only KBZ Pay (up to 2 decimals) and CyberSource accept decimals; Wave, AYA and Yoma take whole kyat. Invalid data throws `InvalidPaymentDataError` with `errors` per field, before any request is sent.
44
+ Amounts are `Amount.kyat(1000)`, `Amount.parse('1000.50')` or a whole `number`/`bigint`; never a float such as `10.5`. Only KBZ Pay (up to 2 decimals) and CyberSource accept decimals; Wave, AYA and Yoma take whole kyat. Invalid data throws `InvalidPaymentDataError` with `errors` per field, before any request is sent. Compare a gateway's amount by value with `amount.equals(callback.amount)`.
44
45
 
45
46
  ### Start a payment
46
47
 
48
+ Each gateway takes a data object (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`s, `AyaPayPaymentData` with an `AyaPayMethod`, `YomaMmqrPaymentData`, `CyberSourcePaymentData` with a `CyberSourceTransactionType`) and returns a typed result:
49
+
47
50
  ```ts
48
51
  import { Amount } from '@laranex/myanmar-payments';
49
52
 
@@ -56,19 +59,20 @@ res.writeHead(302, { Location: payment.url }).end();
56
59
  ```
57
60
 
58
61
  - `RedirectPayment` (`url`) from `kbz.pwa(data)` and `wave.initiate(data)`. Wave fills `data.merchantReferenceId` when empty; store it.
59
- - `FormPayment` from `aya.initiate(data)` and `cyberSource.initiate(data)` (synchronous, no network call): send `payment.toHtml()` as an auto-submitting page, or render `action`, `fields` and `enctype` yourself.
62
+ - `FormPayment` from `aya.initiate(data)` and `cyberSource.initiate(data)` (no network call): send `payment.toHtml()` as an auto-submitting page, or render `action`, `fields` and `enctype` yourself.
60
63
  - `QrPayment` from `kbz.qr(data)` (encode `qrString`) and `yoma.initiate(data)` (`qrImageDataUri()`, `expiresAt`, `reference`). A Yoma QR lives `YomaMmqr.QR_LIFETIME_SECONDS` (120); renew it with `yoma.renewQr(orderId)`.
61
- - `AppPayment` from `kbz.app(data)`: `res.json(payment)` sends `orderId`, `orderInfo`, `sign`, `signType`.
62
- - Every network method takes `{ signal }` as its last argument.
64
+ - `AppPayment` from `kbz.app(data)`: `res.json(payment)` sends `orderId`, `orderInfo`, `sign` and `signType` to your mobile app.
63
65
 
64
- AYA needs a channel: `await aya.services()` lists `AyaPayService` entries (`key`, `supports(method)`), then `aya.initiate({ orderId: 'ORDER123', amount: 1000, channel: 'kbz_pay', method: AyaPayMethod.Qr })`.
66
+ AYA needs a channel: `await aya.services()` lists `AyaPayService` entries (`key`, `supports(method)`), then `aya.initiate({ orderId: 'ORDER123', amount: 1000, channel: 'kbz_pay', method: AyaPayMethod.Qr })`. Every network method takes `{ signal }` as its last argument.
65
67
 
66
68
  ### Handle the callback
67
69
 
70
+ Build a `CallbackRequest` from the raw request, verify it, then reply:
71
+
68
72
  ```ts
69
73
  import { CallbackRequest, SignatureVerificationError } from '@laranex/myanmar-payments';
70
74
 
71
- const request = await CallbackRequest.fromNodeRequest(req); // or fromWebRequest(request) for Fetch servers
75
+ const request = await CallbackRequest.fromNodeRequest(req); // or fromWebRequest(request)
72
76
  try {
73
77
  const callback = kbz.handleCallback(request);
74
78
  if (callback.isSuccessful()) {
@@ -76,22 +80,20 @@ try {
76
80
  }
77
81
  callback.acknowledgement.send(res); // or return callback.acknowledgement.toResponse()
78
82
  } catch (error) {
79
- if (error instanceof SignatureVerificationError) {
80
- res.statusCode = 400;
81
- res.end('invalid signature');
82
- } else throw error;
83
+ if (!(error instanceof SignatureVerificationError)) throw error;
84
+ res.writeHead(400).end('invalid signature');
83
85
  }
84
86
  ```
85
87
 
86
- - Give the callback route the raw body: in Express use `express.raw({ type: '*/*' })` or mount it before `express.json()`.
88
+ - Give the package the raw body: in Express use `express.raw({ type: '*/*' })` on the callback route; in Fastify keep the body as a string (`parseAs: 'string'`) and build `CallbackRequest.from({ body, headers, query })`.
87
89
  - Check AYA's browser return with `aya.verifyRedirect(request)`.
88
- - For production, store the verified call, acknowledge immediately, then process it once in the background.
90
+ - For production, store the verified callback, acknowledge immediately, then process it once in the background.
89
91
 
90
92
  ### Check status and handle errors
91
93
 
92
94
  - `kbz.status(orderId)`, `aya.status(orderId)` and `yoma.status(reference)` return `PaymentStatusResult` with `status` and `isSuccessful()`. Wave Money and CyberSource have no status API.
93
- - Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `Cancelled`, `Expired`, `Unknown`; `PaymentStatus.isFinal(status)`.
94
- - Gateway failures throw `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause` for network errors and aborts). All errors extend `PaymentError`.
95
+ - Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `Canceled`, `Expired`, `Unknown`; `PaymentStatus.isFinal(status)`.
96
+ - Gateway failures throw `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`; network errors and aborts are the `cause`). All errors extend `PaymentError`.
95
97
 
96
98
  ## Test your app
97
99
 
@@ -104,6 +106,6 @@ try {
104
106
  - Fulfilling orders from return pages or query strings; fulfill only from a verified callback or a status check.
105
107
  - Treating `pending` or `unknown` as paid.
106
108
  - Passing floats or `Number(price)` as amounts; use `Amount.parse()`.
107
- - Parsing the callback body with `express.json()` before the package sees it when you can pass the raw body.
109
+ - Building the `CallbackRequest` from `req.body` parsed by `express.json()` instead of the raw body.
108
110
  - Reusing a Wave `merchantReferenceId`, or calling Yoma `initiate()` twice for one order (use `renewQr()`).
109
111
  - Opening a KBZ Pay PWA link outside a phone with the KBZ Pay app.