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

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/CHANGELOG.md CHANGED
@@ -12,10 +12,15 @@ Initial release. The version number matches the other Laranex Myanmar payments p
12
12
  - Exact `Amount` type (`Amount.kyat()`, `Amount.parse()`) kept as decimal text, so amounts never pass through a float; payment data also takes a whole-number `number` or `bigint` and rejects fractions. Decimals are accepted only where the gateway documents them (KBZ Pay up to 2 places, CyberSource any); every gateway except CyberSource is MMK only.
13
13
  - A config class per gateway with `fromEnv(process.env)`, reading the same `*_SANDBOX`, credential and URL variables as the PHP, Laravel and Go packages; a missing credential throws `ConfigurationError`. `MyanmarPayments` builds every gateway lazily from one config object or the environment.
14
14
  - Result classes: `RedirectPayment`, `FormPayment` (with `toHtml()`), `QrPayment` (with `qrImageDataUri()`) and `AppPayment`; `PaymentCallback` (with a gateway-independent `PaymentStatus` and the `Acknowledgement` each gateway expects, sent with `send(res)` or `toResponse()`) and `PaymentStatusResult`.
15
- - `CallbackRequest.fromNodeRequest()` (node:http, Express, Fastify, Koa), `CallbackRequest.fromWebRequest()` (Fetch API servers such as Next.js, Hono and Bun), `CallbackRequest.from()` and `CallbackRequest.fromJson()`. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text.
15
+ - `CallbackRequest.fromNodeRequest()` (node:http, Express), `CallbackRequest.fromWebRequest()` (Fetch API servers such as Next.js, Hono and Bun), `CallbackRequest.from()` and `CallbackRequest.fromJson()`. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text.
16
16
  - Errors: `PaymentError` and its subclasses `InvalidPaymentDataError` (per-field `errors`), `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause`), `SignatureVerificationError` and `ConfigurationError`.
17
17
  - No runtime dependencies: the global `fetch` (or an injected `fetch` / `HttpClient`, 30 second default timeout, `AbortSignal` per call) and `node:crypto`; an injectable `TokenCache` with an in-memory default for Yoma's access token.
18
+ - CyberSource callbacks trust only the fields listed in `signed_field_names`: `decision` and `req_reference_number` must be signed, and unsigned fields are left out of the result and `raw`.
19
+ - AYA Pay reads a `payload` whose `+` signs became spaces in an unencoded return query string.
18
20
  - Callback, return and cancel URLs only need to be valid absolute http or https URLs; there is no HTTPS-only rule (gateways may still require HTTPS in production).
19
21
  - Wave Money's sandbox is `https://preprodpayments.wavemoney.io:8107`, with checkout at `https://preprodpayments.wavemoney.io/authenticate`.
20
22
  - Agent skill in `skills/node-myanmar-payments` so coding agents use the package correctly; install it with `npx skills add laranex/node-myanmar-payments`.
21
23
  - Requires Node.js 20 or higher.
24
+
25
+ ### Changed since the pre-releases
26
+ - `PaymentStatus.Cancelled` is renamed to `PaymentStatus.Canceled` and its value from `'cancelled'` to `'canceled'` (American English), with no alias. Code or stored statuses from the `v4.0.0-dev` pre-releases need the new name; gateway status literals such as Wave Money's `PAYMENT_REQUEST_CANCELLED` are unchanged.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Node Myanmar Payments
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@laranex/myanmar-payments.svg?style=flat-square)](https://www.npmjs.com/package/@laranex/myanmar-payments)
4
- [![Tests](https://img.shields.io/github/actions/workflow/status/laranex/node-myanmar-payments/tests.yml?label=tests&style=flat-square)](https://github.com/laranex/node-myanmar-payments/actions/workflows/tests.yml)
4
+ [![Tests](https://github.com/laranex/node-myanmar-payments/actions/workflows/tests.yml/badge.svg)](https://github.com/laranex/node-myanmar-payments/actions/workflows/tests.yml)
5
5
  [![License](https://img.shields.io/npm/l/@laranex/myanmar-payments.svg?style=flat-square)](LICENSE.md)
6
6
 
7
7
  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. Built for humans and AI agents.
@@ -246,7 +246,8 @@ class AyaPay {
246
246
  /** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
247
247
  verifiedPayload(input, context) {
248
248
  const fail = () => new errors_js_1.SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, (0, json_js_1.toPlainObject)(input));
249
- const json = (0, crypto_js_1.decodeBase64)((0, values_js_1.get)(input, 'payload'));
249
+ // Base64 never contains spaces: a space is a `+` that an unencoded query string turned into one.
250
+ const json = (0, crypto_js_1.decodeBase64)((0, values_js_1.get)(input, 'payload').replace(/ /g, '+'));
250
251
  const payload = json === undefined ? undefined : (0, json_js_1.parseJsonObject)(json);
251
252
  if (payload === undefined) {
252
253
  throw fail();
@@ -16,8 +16,8 @@ export interface CallbackRequestInit {
16
16
  query?: QueryInput | null | undefined;
17
17
  }
18
18
  /**
19
- * The parts of a Node `http.IncomingMessage` this package reads, so Express, Fastify (`request.raw`)
20
- * and Koa (`ctx.req`) requests work too.
19
+ * The parts of a Node `http.IncomingMessage` this package reads, so Express requests work too (and
20
+ * Koa's `ctx.req` when no body parser consumed it).
21
21
  */
22
22
  export interface NodeRequestLike extends AsyncIterable<unknown> {
23
23
  headers: Readonly<Record<string, string | readonly string[] | undefined>>;
@@ -57,10 +57,11 @@ export declare class CallbackRequest {
57
57
  */
58
58
  static fromJson(payload: unknown, headers?: HeadersInput | null): CallbackRequest;
59
59
  /**
60
- * Reads a Node `http.IncomingMessage` (also Express `req`, Fastify `request.raw`, Koa `ctx.req`).
61
- * Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured it as
62
- * text or bytes. When a body parser already consumed the stream, the parsed `body` is encoded
63
- * again as JSON or a form.
60
+ * Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
61
+ * parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
62
+ * it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
63
+ * encoded again as JSON or a form. Fastify parses the body before your handler runs: use
64
+ * {@link CallbackRequest.from} with the raw body there.
64
65
  */
65
66
  static fromNodeRequest(request: NodeRequestLike): Promise<CallbackRequest>;
66
67
  /**
@@ -37,10 +37,11 @@ class CallbackRequest {
37
37
  });
38
38
  }
39
39
  /**
40
- * Reads a Node `http.IncomingMessage` (also Express `req`, Fastify `request.raw`, Koa `ctx.req`).
41
- * Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured it as
42
- * text or bytes. When a body parser already consumed the stream, the parsed `body` is encoded
43
- * again as JSON or a form.
40
+ * Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
41
+ * parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
42
+ * it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
43
+ * encoded again as JSON or a form. Fastify parses the body before your handler runs: use
44
+ * {@link CallbackRequest.from} with the raw body there.
44
45
  */
45
46
  static async fromNodeRequest(request) {
46
47
  const headers = normalizeHeaders(request.headers);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A gateway-independent payment status. Every gateway's own status values are mapped onto these.
3
3
  */
4
- export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'cancelled' | 'expired' | 'unknown';
4
+ export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'canceled' | 'expired' | 'unknown';
5
5
  /**
6
6
  * The payment statuses, plus helpers.
7
7
  *
@@ -18,13 +18,13 @@ export declare const PaymentStatus: Readonly<{
18
18
  /** The payment was attempted and failed or was rejected. */
19
19
  Failed: "failed";
20
20
  /** The payment or order was canceled or closed before completing. */
21
- Cancelled: "cancelled";
21
+ Canceled: "canceled";
22
22
  /** The payment window ran out before the customer paid. */
23
23
  Expired: "expired";
24
24
  /** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
25
25
  Unknown: "unknown";
26
26
  /** Every status, in the order above. */
27
- values: readonly ["successful", "pending", "failed", "cancelled", "expired", "unknown"];
27
+ values: readonly ["successful", "pending", "failed", "canceled", "expired", "unknown"];
28
28
  /** Whether the status will not change any more: `false` for `pending` and `unknown`. */
29
29
  isFinal(status: PaymentStatus): boolean;
30
30
  }>;
@@ -18,7 +18,7 @@ exports.PaymentStatus = Object.freeze({
18
18
  /** The payment was attempted and failed or was rejected. */
19
19
  Failed: 'failed',
20
20
  /** The payment or order was canceled or closed before completing. */
21
- Cancelled: 'cancelled',
21
+ Canceled: 'canceled',
22
22
  /** The payment window ran out before the customer paid. */
23
23
  Expired: 'expired',
24
24
  /** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
@@ -28,7 +28,7 @@ exports.PaymentStatus = Object.freeze({
28
28
  'successful',
29
29
  'pending',
30
30
  'failed',
31
- 'cancelled',
31
+ 'canceled',
32
32
  'expired',
33
33
  'unknown',
34
34
  ]),
@@ -87,7 +87,8 @@ export declare class CyberSource {
87
87
  initiate(data: CyberSourcePaymentData): FormPayment;
88
88
  /**
89
89
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
90
- * page.
90
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
91
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
91
92
  */
92
93
  handleCallback(request: CallbackRequest): PaymentCallback;
93
94
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
@@ -77,7 +77,7 @@ const STATUSES = {
77
77
  REVIEW: 'pending',
78
78
  DECLINE: 'failed',
79
79
  ERROR: 'failed',
80
- CANCEL: 'cancelled',
80
+ CANCEL: 'canceled',
81
81
  };
82
82
  /**
83
83
  * CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
@@ -149,31 +149,39 @@ class CyberSource {
149
149
  }
150
150
  /**
151
151
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
152
- * page.
152
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
153
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
153
154
  */
154
155
  handleCallback(request) {
155
156
  const payload = (0, callback_js_1.losslessInput)(request);
156
157
  const expected = this.sign(payload);
157
- if (expected === undefined || !(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'signature'))) {
158
+ const names = signedNames(payload);
159
+ if (expected === undefined ||
160
+ !(0, crypto_js_1.safeEqual)(expected, (0, values_js_1.get)(payload, 'signature')) ||
161
+ !names.includes('decision') ||
162
+ !names.includes('req_reference_number')) {
158
163
  throw new errors_js_1.SignatureVerificationError('CyberSource callback signature verification failed.', (0, json_js_1.toPlainObject)(payload));
159
164
  }
160
- const decision = (0, values_js_1.trimmed)(payload, 'decision').toUpperCase();
165
+ // A signature only covers the listed fields, so anything else in the post could have been
166
+ // added by the sender (e.g. a `decision` next to the signed request fields).
167
+ const signed = {};
168
+ for (const name of [...names, 'signature']) {
169
+ (0, json_js_1.setKey)(signed, name, payload[name]);
170
+ }
171
+ const decision = (0, values_js_1.trimmed)(signed, 'decision').toUpperCase();
161
172
  return new callback_js_1.PaymentCallback({
162
- orderId: (0, values_js_1.get)(payload, 'req_reference_number'),
173
+ orderId: (0, values_js_1.get)(signed, 'req_reference_number'),
163
174
  status: (0, status_js_1.resolveStatus)(STATUSES, decision),
164
175
  gatewayStatus: decision,
165
- gatewayReference: (0, values_js_1.optional)(payload, 'transaction_id'),
166
- amount: (0, values_js_1.get)(payload, 'auth_amount') || (0, values_js_1.optional)(payload, 'req_amount'),
167
- raw: (0, json_js_1.toPlainObject)(payload),
176
+ gatewayReference: (0, values_js_1.optional)(signed, 'transaction_id'),
177
+ amount: (0, values_js_1.get)(signed, 'auth_amount') || (0, values_js_1.optional)(signed, 'req_amount'),
178
+ raw: (0, json_js_1.toPlainObject)(signed),
168
179
  });
169
180
  }
170
181
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
171
182
  sign(fields) {
172
183
  const pairs = [];
173
- for (const name of (0, values_js_1.get)(fields, 'signed_field_names').split(',')) {
174
- if (name === '') {
175
- continue;
176
- }
184
+ for (const name of signedNames(fields)) {
177
185
  const value = (0, values_js_1.scalarString)(fields[name]);
178
186
  if (value === undefined) {
179
187
  return undefined;
@@ -186,3 +194,9 @@ class CyberSource {
186
194
  }
187
195
  }
188
196
  exports.CyberSource = CyberSource;
197
+ /** The names listed in `signed_field_names`, in order. */
198
+ function signedNames(fields) {
199
+ return (0, values_js_1.get)(fields, 'signed_field_names')
200
+ .split(',')
201
+ .filter((name) => name !== '');
202
+ }
@@ -60,7 +60,7 @@ const STATUSES = {
60
60
  WAIT_PAY: 'pending',
61
61
  PAYING: 'pending',
62
62
  PAY_FAILED: 'failed',
63
- ORDER_CLOSED: 'cancelled',
63
+ ORDER_CLOSED: 'canceled',
64
64
  ORDER_EXPIRED: 'expired',
65
65
  };
66
66
  /**
@@ -71,7 +71,7 @@ const STATUSES = {
71
71
  INSUFFICIENT_BALANCE: 'pending',
72
72
  ACCOUNT_LOCKED: 'failed',
73
73
  BILL_COLLECTION_FAILED: 'failed',
74
- PAYMENT_REQUEST_CANCELLED: 'cancelled',
74
+ PAYMENT_REQUEST_CANCELLED: 'canceled',
75
75
  TRANSACTION_TIMED_OUT: 'expired',
76
76
  SCHEDULER_TRANSACTION_TIMED_OUT: 'expired',
77
77
  };
@@ -242,7 +242,8 @@ export class AyaPay {
242
242
  /** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
243
243
  verifiedPayload(input, context) {
244
244
  const fail = () => new SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, toPlainObject(input));
245
- const json = decodeBase64(get(input, 'payload'));
245
+ // Base64 never contains spaces: a space is a `+` that an unencoded query string turned into one.
246
+ const json = decodeBase64(get(input, 'payload').replace(/ /g, '+'));
246
247
  const payload = json === undefined ? undefined : parseJsonObject(json);
247
248
  if (payload === undefined) {
248
249
  throw fail();
@@ -16,8 +16,8 @@ export interface CallbackRequestInit {
16
16
  query?: QueryInput | null | undefined;
17
17
  }
18
18
  /**
19
- * The parts of a Node `http.IncomingMessage` this package reads, so Express, Fastify (`request.raw`)
20
- * and Koa (`ctx.req`) requests work too.
19
+ * The parts of a Node `http.IncomingMessage` this package reads, so Express requests work too (and
20
+ * Koa's `ctx.req` when no body parser consumed it).
21
21
  */
22
22
  export interface NodeRequestLike extends AsyncIterable<unknown> {
23
23
  headers: Readonly<Record<string, string | readonly string[] | undefined>>;
@@ -57,10 +57,11 @@ export declare class CallbackRequest {
57
57
  */
58
58
  static fromJson(payload: unknown, headers?: HeadersInput | null): CallbackRequest;
59
59
  /**
60
- * Reads a Node `http.IncomingMessage` (also Express `req`, Fastify `request.raw`, Koa `ctx.req`).
61
- * Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured it as
62
- * text or bytes. When a body parser already consumed the stream, the parsed `body` is encoded
63
- * again as JSON or a form.
60
+ * Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
61
+ * parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
62
+ * it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
63
+ * encoded again as JSON or a form. Fastify parses the body before your handler runs: use
64
+ * {@link CallbackRequest.from} with the raw body there.
64
65
  */
65
66
  static fromNodeRequest(request: NodeRequestLike): Promise<CallbackRequest>;
66
67
  /**
@@ -31,10 +31,11 @@ export class CallbackRequest {
31
31
  });
32
32
  }
33
33
  /**
34
- * Reads a Node `http.IncomingMessage` (also Express `req`, Fastify `request.raw`, Koa `ctx.req`).
35
- * Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured it as
36
- * text or bytes. When a body parser already consumed the stream, the parsed `body` is encoded
37
- * again as JSON or a form.
34
+ * Reads a Node `http.IncomingMessage` (also Express `req`, or Koa `ctx.req` without a body
35
+ * parser). Reads the raw body from the stream, or from `rawBody`/`body` when middleware captured
36
+ * it as text or bytes. When a body parser already consumed the stream, the parsed `body` is
37
+ * encoded again as JSON or a form. Fastify parses the body before your handler runs: use
38
+ * {@link CallbackRequest.from} with the raw body there.
38
39
  */
39
40
  static async fromNodeRequest(request) {
40
41
  const headers = normalizeHeaders(request.headers);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A gateway-independent payment status. Every gateway's own status values are mapped onto these.
3
3
  */
4
- export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'cancelled' | 'expired' | 'unknown';
4
+ export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'canceled' | 'expired' | 'unknown';
5
5
  /**
6
6
  * The payment statuses, plus helpers.
7
7
  *
@@ -18,13 +18,13 @@ export declare const PaymentStatus: Readonly<{
18
18
  /** The payment was attempted and failed or was rejected. */
19
19
  Failed: "failed";
20
20
  /** The payment or order was canceled or closed before completing. */
21
- Cancelled: "cancelled";
21
+ Canceled: "canceled";
22
22
  /** The payment window ran out before the customer paid. */
23
23
  Expired: "expired";
24
24
  /** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
25
25
  Unknown: "unknown";
26
26
  /** Every status, in the order above. */
27
- values: readonly ["successful", "pending", "failed", "cancelled", "expired", "unknown"];
27
+ values: readonly ["successful", "pending", "failed", "canceled", "expired", "unknown"];
28
28
  /** Whether the status will not change any more: `false` for `pending` and `unknown`. */
29
29
  isFinal(status: PaymentStatus): boolean;
30
30
  }>;
@@ -14,7 +14,7 @@ export const PaymentStatus = Object.freeze({
14
14
  /** The payment was attempted and failed or was rejected. */
15
15
  Failed: 'failed',
16
16
  /** The payment or order was canceled or closed before completing. */
17
- Cancelled: 'cancelled',
17
+ Canceled: 'canceled',
18
18
  /** The payment window ran out before the customer paid. */
19
19
  Expired: 'expired',
20
20
  /** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
@@ -24,7 +24,7 @@ export const PaymentStatus = Object.freeze({
24
24
  'successful',
25
25
  'pending',
26
26
  'failed',
27
- 'cancelled',
27
+ 'canceled',
28
28
  'expired',
29
29
  'unknown',
30
30
  ]),
@@ -87,7 +87,8 @@ export declare class CyberSource {
87
87
  initiate(data: CyberSourcePaymentData): FormPayment;
88
88
  /**
89
89
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
90
- * page.
90
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
91
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
91
92
  */
92
93
  handleCallback(request: CallbackRequest): PaymentCallback;
93
94
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
@@ -3,7 +3,7 @@ import { CallbackRequest, losslessInput, PaymentCallback } from '../core/callbac
3
3
  import { hmacSha256Base64, randomHex, safeEqual } from '../core/crypto.js';
4
4
  import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
5
5
  import { SignatureVerificationError } from '../core/errors.js';
6
- import { toPlainObject } from '../core/json.js';
6
+ import { setKey, toPlainObject } from '../core/json.js';
7
7
  import { FormPayment } from '../core/results.js';
8
8
  import { resolveStatus } from '../core/status.js';
9
9
  import { toAmount, Validator } from '../core/validate.js';
@@ -74,7 +74,7 @@ const STATUSES = {
74
74
  REVIEW: 'pending',
75
75
  DECLINE: 'failed',
76
76
  ERROR: 'failed',
77
- CANCEL: 'cancelled',
77
+ CANCEL: 'canceled',
78
78
  };
79
79
  /**
80
80
  * CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
@@ -146,31 +146,39 @@ export class CyberSource {
146
146
  }
147
147
  /**
148
148
  * Verifies CyberSource's result post. The same check works for the browser post to your receipt
149
- * page.
149
+ * page. Only the fields listed in `signed_field_names` are trusted: `decision` and
150
+ * `req_reference_number` must be signed, and unsigned fields are left out of the result.
150
151
  */
151
152
  handleCallback(request) {
152
153
  const payload = losslessInput(request);
153
154
  const expected = this.sign(payload);
154
- if (expected === undefined || !safeEqual(expected, get(payload, 'signature'))) {
155
+ const names = signedNames(payload);
156
+ if (expected === undefined ||
157
+ !safeEqual(expected, get(payload, 'signature')) ||
158
+ !names.includes('decision') ||
159
+ !names.includes('req_reference_number')) {
155
160
  throw new SignatureVerificationError('CyberSource callback signature verification failed.', toPlainObject(payload));
156
161
  }
157
- const decision = trimmed(payload, 'decision').toUpperCase();
162
+ // A signature only covers the listed fields, so anything else in the post could have been
163
+ // added by the sender (e.g. a `decision` next to the signed request fields).
164
+ const signed = {};
165
+ for (const name of [...names, 'signature']) {
166
+ setKey(signed, name, payload[name]);
167
+ }
168
+ const decision = trimmed(signed, 'decision').toUpperCase();
158
169
  return new PaymentCallback({
159
- orderId: get(payload, 'req_reference_number'),
170
+ orderId: get(signed, 'req_reference_number'),
160
171
  status: resolveStatus(STATUSES, decision),
161
172
  gatewayStatus: decision,
162
- gatewayReference: optional(payload, 'transaction_id'),
163
- amount: get(payload, 'auth_amount') || optional(payload, 'req_amount'),
164
- raw: toPlainObject(payload),
173
+ gatewayReference: optional(signed, 'transaction_id'),
174
+ amount: get(signed, 'auth_amount') || optional(signed, 'req_amount'),
175
+ raw: toPlainObject(signed),
165
176
  });
166
177
  }
167
178
  /** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
168
179
  sign(fields) {
169
180
  const pairs = [];
170
- for (const name of get(fields, 'signed_field_names').split(',')) {
171
- if (name === '') {
172
- continue;
173
- }
181
+ for (const name of signedNames(fields)) {
174
182
  const value = scalarString(fields[name]);
175
183
  if (value === undefined) {
176
184
  return undefined;
@@ -182,3 +190,9 @@ export class CyberSource {
182
190
  : hmacSha256Base64(this.config.secretKey, pairs.join(','));
183
191
  }
184
192
  }
193
+ /** The names listed in `signed_field_names`, in order. */
194
+ function signedNames(fields) {
195
+ return get(fields, 'signed_field_names')
196
+ .split(',')
197
+ .filter((name) => name !== '');
198
+ }
@@ -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
  /**
@@ -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
  };
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.3",
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",
@@ -84,13 +84,14 @@ try {
84
84
  ```
85
85
 
86
86
  - Give the callback route the raw body: in Express use `express.raw({ type: '*/*' })` or mount it before `express.json()`.
87
+ - Fastify consumes the body before your handler: keep it as a string with a content-type parser (`parseAs: 'string'`) and build `CallbackRequest.from({ body, headers: request.headers, query: request.query })`.
87
88
  - Check AYA's browser return with `aya.verifyRedirect(request)`.
88
89
  - For production, store the verified call, acknowledge immediately, then process it once in the background.
89
90
 
90
91
  ### Check status and handle errors
91
92
 
92
93
  - `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
+ - Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `Canceled`, `Expired`, `Unknown`; `PaymentStatus.isFinal(status)`.
94
95
  - Gateway failures throw `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause` for network errors and aborts). All errors extend `PaymentError`.
95
96
 
96
97
  ## Test your app