@laranex/myanmar-payments 4.0.0-dev.0 → 4.0.0-dev.10

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 (68) hide show
  1. package/CHANGELOG.md +19 -4
  2. package/README.md +3 -3
  3. package/dist/cjs/aya-pay/aya-pay.d.ts +13 -9
  4. package/dist/cjs/aya-pay/aya-pay.js +16 -12
  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 +14 -5
  12. package/dist/cjs/core/env.js +35 -11
  13. package/dist/cjs/core/errors.d.ts +9 -3
  14. package/dist/cjs/core/errors.js +12 -4
  15. package/dist/cjs/core/http.d.ts +10 -11
  16. package/dist/cjs/core/http.js +7 -9
  17. package/dist/cjs/core/index.d.ts +2 -2
  18. package/dist/cjs/core/index.js +3 -2
  19. package/dist/cjs/core/json.d.ts +2 -2
  20. package/dist/cjs/core/json.js +3 -3
  21. package/dist/cjs/core/status.d.ts +3 -3
  22. package/dist/cjs/core/status.js +2 -2
  23. package/dist/cjs/core/values.d.ts +2 -0
  24. package/dist/cjs/core/values.js +5 -0
  25. package/dist/cjs/cyber-source/cyber-source.d.ts +12 -14
  26. package/dist/cjs/cyber-source/cyber-source.js +39 -26
  27. package/dist/cjs/kbz-pay/kbz-pay.d.ts +27 -11
  28. package/dist/cjs/kbz-pay/kbz-pay.js +43 -20
  29. package/dist/cjs/kbz-pay/signer.d.ts +4 -1
  30. package/dist/cjs/kbz-pay/signer.js +7 -2
  31. package/dist/cjs/wave-money/wave-money.d.ts +26 -13
  32. package/dist/cjs/wave-money/wave-money.js +29 -29
  33. package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +17 -13
  34. package/dist/cjs/yoma-mmqr/yoma-mmqr.js +21 -19
  35. package/dist/esm/aya-pay/aya-pay.d.ts +13 -9
  36. package/dist/esm/aya-pay/aya-pay.js +18 -14
  37. package/dist/esm/core/amount.d.ts +4 -3
  38. package/dist/esm/core/amount.js +13 -4
  39. package/dist/esm/core/callback.d.ts +14 -8
  40. package/dist/esm/core/callback.js +19 -11
  41. package/dist/esm/core/crypto.d.ts +6 -1
  42. package/dist/esm/core/crypto.js +17 -3
  43. package/dist/esm/core/env.d.ts +14 -5
  44. package/dist/esm/core/env.js +32 -9
  45. package/dist/esm/core/errors.d.ts +9 -3
  46. package/dist/esm/core/errors.js +12 -4
  47. package/dist/esm/core/http.d.ts +10 -11
  48. package/dist/esm/core/http.js +6 -8
  49. package/dist/esm/core/index.d.ts +2 -2
  50. package/dist/esm/core/index.js +2 -1
  51. package/dist/esm/core/json.d.ts +2 -2
  52. package/dist/esm/core/json.js +3 -3
  53. package/dist/esm/core/status.d.ts +3 -3
  54. package/dist/esm/core/status.js +2 -2
  55. package/dist/esm/core/values.d.ts +2 -0
  56. package/dist/esm/core/values.js +4 -0
  57. package/dist/esm/cyber-source/cyber-source.d.ts +12 -14
  58. package/dist/esm/cyber-source/cyber-source.js +41 -28
  59. package/dist/esm/kbz-pay/kbz-pay.d.ts +27 -11
  60. package/dist/esm/kbz-pay/kbz-pay.js +45 -22
  61. package/dist/esm/kbz-pay/signer.d.ts +4 -1
  62. package/dist/esm/kbz-pay/signer.js +8 -3
  63. package/dist/esm/wave-money/wave-money.d.ts +26 -13
  64. package/dist/esm/wave-money/wave-money.js +31 -31
  65. package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +17 -13
  66. package/dist/esm/yoma-mmqr/yoma-mmqr.js +23 -21
  67. package/package.json +2 -2
  68. package/skills/node-myanmar-payments/SKILL.md +26 -22
@@ -16,35 +16,39 @@ 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;
21
- /** Overrides the API base URL. */
19
+ /** The `{version}` segment of the API paths, e.g. `v1rc`. */
20
+ apiVersion: string;
21
+ /**
22
+ * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
23
+ * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
24
+ * client you pass keeps its own timeout.
25
+ */
26
+ timeoutSeconds?: number | string | null | undefined;
27
+ /** Overrides the API base URL, e.g. to point at UAT. */
22
28
  baseUrl?: string | undefined;
23
- /** The `{version}` segment of the API paths (default `v1rc`). */
24
- apiVersion?: string | undefined;
25
29
  }
26
30
  /**
27
- * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
31
+ * Yoma MMQR credentials and endpoints. A missing setting throws a `ConfigurationError`. The URL
32
+ * defaults to production.
28
33
  */
29
34
  export declare class YomaMmqrConfig {
30
- static readonly SANDBOX_URL = "https://devapi.yomabank.net";
31
35
  static readonly PRODUCTION_URL = "https://paymenthubapi.yomabank.com";
32
- static readonly DEFAULT_API_VERSION = "v1rc";
33
36
  readonly merchantId: string;
34
37
  readonly clientId: string;
35
38
  readonly clientSecret: string;
36
39
  readonly webhookHashKey: string;
37
40
  readonly webhookSecret: string | undefined;
38
- readonly sandbox: boolean;
41
+ /** The `{version}` segment of the API paths. */
42
+ readonly apiVersion: string;
43
+ /** Seconds before the default HTTP client gives up on a request. */
44
+ readonly timeoutSeconds: number;
39
45
  /** The base URL in use. */
40
46
  readonly baseUrl: string;
41
- /** The API version in use. */
42
- readonly apiVersion: string;
43
47
  constructor(options: YomaMmqrConfigOptions);
44
48
  /**
45
49
  * Reads `YOMA_MMQR_MERCHANT_ID`, `YOMA_MMQR_CLIENT_ID`, `YOMA_MMQR_CLIENT_SECRET`,
46
- * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_WEBHOOK_SECRET`, `YOMA_MMQR_SANDBOX`,
47
- * `YOMA_MMQR_BASE_URL` and `YOMA_MMQR_API_VERSION`.
50
+ * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_API_VERSION`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`,
51
+ * `YOMA_MMQR_WEBHOOK_SECRET` and `YOMA_MMQR_BASE_URL`.
48
52
  */
49
53
  static fromEnv(env?: EnvSource): YomaMmqrConfig;
50
54
  }
@@ -2,46 +2,45 @@ 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, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSetting, resolveTimeout, 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
- * Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
14
+ * Yoma MMQR credentials and endpoints. A missing setting throws a `ConfigurationError`. The URL
15
+ * defaults to production.
15
16
  */
16
17
  export class YomaMmqrConfig {
17
- static SANDBOX_URL = 'https://devapi.yomabank.net';
18
18
  static PRODUCTION_URL = 'https://paymenthubapi.yomabank.com';
19
- static DEFAULT_API_VERSION = 'v1rc';
20
19
  merchantId;
21
20
  clientId;
22
21
  clientSecret;
23
22
  webhookHashKey;
24
23
  webhookSecret;
25
- sandbox;
24
+ /** The `{version}` segment of the API paths. */
25
+ apiVersion;
26
+ /** Seconds before the default HTTP client gives up on a request. */
27
+ timeoutSeconds;
26
28
  /** The base URL in use. */
27
29
  baseUrl;
28
- /** The API version in use. */
29
- apiVersion;
30
30
  constructor(options) {
31
31
  this.merchantId = requireSetting('yoma_mmqr', 'merchant_id', options.merchantId);
32
32
  this.clientId = requireSetting('yoma_mmqr', 'client_id', options.clientId);
33
33
  this.clientSecret = requireSetting('yoma_mmqr', 'client_secret', options.clientSecret);
34
34
  this.webhookHashKey = requireSetting('yoma_mmqr', 'webhook_hashkey', options.webhookHashKey);
35
+ this.apiVersion = requireSetting('yoma_mmqr', 'api_version', options.apiVersion);
36
+ this.timeoutSeconds = resolveTimeout('yoma_mmqr', options.timeoutSeconds);
35
37
  this.webhookSecret = optionalSetting(options.webhookSecret);
36
- this.sandbox = options.sandbox ?? true;
37
- this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
38
- (this.sandbox ? YomaMmqrConfig.SANDBOX_URL : YomaMmqrConfig.PRODUCTION_URL));
39
- this.apiVersion = optionalSetting(options.apiVersion) ?? YomaMmqrConfig.DEFAULT_API_VERSION;
38
+ this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? YomaMmqrConfig.PRODUCTION_URL);
40
39
  }
41
40
  /**
42
41
  * Reads `YOMA_MMQR_MERCHANT_ID`, `YOMA_MMQR_CLIENT_ID`, `YOMA_MMQR_CLIENT_SECRET`,
43
- * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_WEBHOOK_SECRET`, `YOMA_MMQR_SANDBOX`,
44
- * `YOMA_MMQR_BASE_URL` and `YOMA_MMQR_API_VERSION`.
42
+ * `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_API_VERSION`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`,
43
+ * `YOMA_MMQR_WEBHOOK_SECRET` and `YOMA_MMQR_BASE_URL`.
45
44
  */
46
45
  static fromEnv(env = defaultEnv()) {
47
46
  return new YomaMmqrConfig({
@@ -49,10 +48,10 @@ export class YomaMmqrConfig {
49
48
  clientId: envFirst(env, 'YOMA_MMQR_CLIENT_ID'),
50
49
  clientSecret: envFirst(env, 'YOMA_MMQR_CLIENT_SECRET'),
51
50
  webhookHashKey: envFirst(env, 'YOMA_MMQR_WEBHOOK_HASHKEY'),
51
+ apiVersion: envFirst(env, 'YOMA_MMQR_API_VERSION'),
52
+ timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
52
53
  webhookSecret: envFirst(env, 'YOMA_MMQR_WEBHOOK_SECRET'),
53
- sandbox: envSandbox(env, 'YOMA_MMQR_SANDBOX'),
54
54
  baseUrl: envFirst(env, 'YOMA_MMQR_BASE_URL'),
55
- apiVersion: envFirst(env, 'YOMA_MMQR_API_VERSION'),
56
55
  });
57
56
  }
58
57
  }
@@ -76,7 +75,7 @@ export class YomaMmqr {
76
75
  pendingToken;
77
76
  constructor(config, options = {}) {
78
77
  this.config = config instanceof YomaMmqrConfig ? config : new YomaMmqrConfig(config);
79
- this.transport = new Transport(httpClientFrom(options));
78
+ this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
80
79
  this.cache = options.tokenCache ?? new MemoryTokenCache();
81
80
  }
82
81
  /** A gateway configured from the `YOMA_MMQR_*` environment variables. */
@@ -158,7 +157,9 @@ export class YomaMmqr {
158
157
  const orderNumber = get(payload, 'orderNumber');
159
158
  const status = trimmed(payload, 'status');
160
159
  const expected = hmacSha256Hex(orderNumber + this.config.webhookHashKey, `orderNumber=${orderNumber}&status=${status}`);
161
- if (orderNumber === '' || !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
160
+ if (orderNumber === '' ||
161
+ isNested(payload.status) ||
162
+ !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
162
163
  throw new SignatureVerificationError('Yoma MMQR callback hash verification failed.', toPlainObject(payload));
163
164
  }
164
165
  return new PaymentCallback({
@@ -207,13 +208,14 @@ export class YomaMmqr {
207
208
  if (!response.successful() || token === '') {
208
209
  throw apiError('token', response.status, body, get(body, 'error'));
209
210
  }
210
- const expiresIn = Number.parseInt(get(body, 'expires_in'), 10);
211
- const ttl = Math.max(60, (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) - 60);
211
+ // The leading digits of `expires_in`; missing, non-numeric or 0 means an hour.
212
+ const expiresIn = Number(/^\d+/.exec(get(body, 'expires_in'))?.[0] ?? 0);
213
+ const ttl = Math.max(60, (expiresIn > 0 ? expiresIn : 3600) - 60);
212
214
  await this.cache.set(this.tokenCacheKey(), token, ttl);
213
215
  return token;
214
216
  }
215
217
  tokenCacheKey() {
216
- return `node-myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
218
+ return `myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
217
219
  }
218
220
  }
219
221
  function apiError(endpoint, status, body, errorCode) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@laranex/myanmar-payments",
3
- "version": "4.0.0-dev.0",
4
- "description": "Node.js and TypeScript SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR and CyberSource.",
3
+ "version": "4.0.0-dev.10",
4
+ "description": "Node.js SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and results, exact amounts, no runtime dependencies.",
5
5
  "keywords": [
6
6
  "laranex",
7
7
  "node-myanmar-payments",
@@ -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_*` plus `MYANMAR_PAYMENTS_HTTP_TIMEOUT` (the same variables as the PHP, Go and Python SDKs). Every gateway defaults to its production URLs; to test against UAT, set the URL overrides (`KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL`, `WAVE_MONEY_BASE_URL`, `WAVE_MONEY_AUTHENTICATE_URL`, `AYA_PAY_BASE_URL`, `YOMA_MMQR_BASE_URL`, `CYBER_SOURCE_BASE_URL`) to the gateway's UAT URLs. The HTTP timeout `timeoutSeconds` / `MYANMAR_PAYMENTS_HTTP_TIMEOUT` (seconds, for every gateway except CyberSource) is optional and defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`); an explicit value must be a whole number greater than 0. `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS` (KBZ's `timeToLiveSeconds`) is optional: seconds each KBZ order stays payable, a multiple of 60 from 60 to 7200 (KBZ's 7200 when unset); a payment's `timeToLiveSeconds` overrides it. Every other setting is required, with no defaults: credentials, Wave's `merchantName` and `timeToLiveSeconds`, and Yoma's `apiVersion` (e.g. `v1rc`).
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).
37
- - A missing credential throws `ConfigurationError` (`gateway`, `key`).
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; whole-number settings also take text such as `'30'`.
37
+ - Options (second argument): `{ httpClient, fetch }`. Without `httpClient`, the gateway uses `fetch` with the config's `timeoutSeconds`; a client you pass keeps its own timeout. Yoma and the facade also take `tokenCache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
38
+ - A missing setting throws `ConfigurationError` (`gateway`, `key`); a time setting that is not a whole number greater than 0 throws it too.
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
 
@@ -55,20 +58,23 @@ const payment = await kbz.pwa({
55
58
  res.writeHead(302, { Location: payment.url }).end();
56
59
  ```
57
60
 
58
- - `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.
61
+ - `RedirectPayment` (`url`) from `kbz.pwa(data)` and `wave.initiate(data)`. Wave fills `data.merchantReferenceId` when empty; store it. Pass `merchantName` or `timeToLiveSeconds` in Wave's payment data to override the configured `WAVE_MONEY_MERCHANT_NAME` or `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS` for that one payment (the settings stay required).
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.
65
+
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.
63
67
 
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 })`.
68
+ CyberSource has no payment defaults: pass `currency` (e.g. `'MMK'`), `transactionType` (e.g. `CyberSourceTransactionType.Sale`) and `locale` (e.g. `'en-us'`) on every `cyberSource.initiate(data)`.
65
69
 
66
70
  ### Handle the callback
67
71
 
72
+ Build a `CallbackRequest` from the raw request, verify it, then reply:
73
+
68
74
  ```ts
69
75
  import { CallbackRequest, SignatureVerificationError } from '@laranex/myanmar-payments';
70
76
 
71
- const request = await CallbackRequest.fromNodeRequest(req); // or fromWebRequest(request) for Fetch servers
77
+ const request = await CallbackRequest.fromNodeRequest(req); // or fromWebRequest(request)
72
78
  try {
73
79
  const callback = kbz.handleCallback(request);
74
80
  if (callback.isSuccessful()) {
@@ -76,22 +82,20 @@ try {
76
82
  }
77
83
  callback.acknowledgement.send(res); // or return callback.acknowledgement.toResponse()
78
84
  } catch (error) {
79
- if (error instanceof SignatureVerificationError) {
80
- res.statusCode = 400;
81
- res.end('invalid signature');
82
- } else throw error;
85
+ if (!(error instanceof SignatureVerificationError)) throw error;
86
+ res.writeHead(400).end('invalid signature');
83
87
  }
84
88
  ```
85
89
 
86
- - Give the callback route the raw body: in Express use `express.raw({ type: '*/*' })` or mount it before `express.json()`.
90
+ - 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
91
  - 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.
92
+ - For production, store the verified callback, acknowledge immediately, then process it once in the background.
89
93
 
90
94
  ### Check status and handle errors
91
95
 
92
96
  - `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`.
97
+ - Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `Canceled`, `Expired`, `Unknown`; `PaymentStatus.isFinal(status)`.
98
+ - Gateway failures throw `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`; network errors and aborts are the `cause`). All errors extend `PaymentError`.
95
99
 
96
100
  ## Test your app
97
101
 
@@ -104,6 +108,6 @@ try {
104
108
  - Fulfilling orders from return pages or query strings; fulfill only from a verified callback or a status check.
105
109
  - Treating `pending` or `unknown` as paid.
106
110
  - 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.
111
+ - Building the `CallbackRequest` from `req.body` parsed by `express.json()` instead of the raw body.
108
112
  - Reusing a Wave `merchantReferenceId`, or calling Yoma `initiate()` twice for one order (use `renewQr()`).
109
113
  - Opening a KBZ Pay PWA link outside a phone with the KBZ Pay app.