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

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
@@ -22,8 +22,8 @@ Initial release. The version number matches the other Laranex Myanmar payments p
22
22
  - Requires Node.js 20 or higher.
23
23
 
24
24
  ### Changed since the pre-releases
25
- - No sandbox switch: the sandbox setting (`sandbox`) and the `*_SANDBOX` variables are removed, and every gateway defaults to its production endpoints. The HTTP timeout (`timeout_in_seconds`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`) of every gateway that calls an API is optional and defaults to 30 seconds. Every other setting is required: Wave Money's `time_to_live_in_seconds` and `merchant_name` (no `APP_NAME` fallback), Yoma MMQR's `api_version`, and CyberSource's `currency`, `locale` and `transactionType` on each payment. A missing setting throws the configuration error naming it; a time setting that is not a whole number greater than 0 throws `The <gateway> configuration [<key>] must be a whole number greater than 0.` Upgrading: delete `*_SANDBOX`; to keep testing against UAT, set the URL overrides to the UAT URLs listed in the docs; set the newly required settings.
26
- - In Node: config options `timeToLiveSeconds` and `apiVersion` are required and take a whole number or its text; `timeoutSeconds` (KBZ Pay, Wave Money, AYA Pay, Yoma MMQR) is optional, takes a whole number or its text and defaults to the new `DEFAULT_TIMEOUT_SECONDS` (30) when unset or blank; the `SANDBOX_*`, `DEFAULT_TIME_TO_LIVE_SECONDS` and `DEFAULT_API_VERSION` constants are removed. `GatewayOptions.timeoutMs` (so also the `MyanmarPayments` option) and `DEFAULT_TIMEOUT_MS` are removed; `FetchHttpClient` requires `timeoutMs`. `ConfigurationError` takes a third `invalid` argument for the whole-number message.
25
+ - No sandbox switch: the sandbox setting (`sandbox`) and the `*_SANDBOX` variables are removed, and every gateway defaults to its production endpoints. The HTTP timeout (`timeout_in_seconds`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`) of every gateway that calls an API is optional and defaults to 30 seconds. Every other setting is required: Wave Money's `merchant_name` (no `APP_NAME` fallback), Yoma MMQR's `api_version`, and CyberSource's `currency`, `locale` and `transactionType` on each payment. A missing setting throws the configuration error naming it; a time setting that is not a whole number greater than 0 throws `The <gateway> configuration [<key>] must be a whole number greater than 0.` Upgrading: delete `*_SANDBOX`; to keep testing against UAT, set the URL overrides to the UAT URLs listed in the docs; set the newly required settings.
26
+ - In Node: the config option `apiVersion` is required; Wave Money's `timeToLiveSeconds` is optional and takes a whole number or its text; `timeoutSeconds` (KBZ Pay, Wave Money, AYA Pay, Yoma MMQR) is optional, takes a whole number or its text and defaults to the new `DEFAULT_TIMEOUT_SECONDS` (30) when unset or blank; the `SANDBOX_*`, `DEFAULT_TIME_TO_LIVE_SECONDS` and `DEFAULT_API_VERSION` constants are removed. `GatewayOptions.timeoutMs` (so also the `MyanmarPayments` option) and `DEFAULT_TIMEOUT_MS` are removed; `FetchHttpClient` requires `timeoutMs`. `ConfigurationError` takes a third `invalid` argument for the whole-number message.
27
27
  - `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.
28
28
  - Callback values that are JSON numbers are kept as their exact text, as strings, in `raw`, `parsedBody()`, `input()`, `queryInput()` and the `raw` of errors (`1000.50` is `'1000.50'`, not `1000.5`), like the PHP SDK.
29
29
  - A nested value (object or array) in a signed or hashed callback field now fails verification: KBZ Pay (any callback field), Wave Money (the hashed fields), AYA Pay (the signed payload fields) and Yoma MMQR (`status`). Before, KBZ Pay skipped it, Wave Money hashed it as `null` and AYA Pay signed it as empty.
@@ -32,5 +32,5 @@ Initial release. The version number matches the other Laranex Myanmar payments p
32
32
  - `Amount.equals()` compares by value, ignoring leading zeros too (`'01000'` equals `Amount.kyat(1000)`); text that is not plain digits with an optional fraction is never equal.
33
33
  - `CallbackRequest.rawBody` holds the exact bytes received (a string body is encoded as UTF-8), next to `body` as text, like Python's `raw_body` and Go's `Body`.
34
34
  - `WaveMoneyPaymentData` takes optional `merchantName` and `timeToLiveSeconds`, which override the configured `merchantName` and `timeToLiveSeconds` for one payment (request hash included); a blank `merchantName` or a `timeToLiveSeconds` that is not a whole number greater than 0 throws `InvalidPaymentDataError`, like the PHP SDK.
35
- - `KbzPayPaymentData.timeoutMinutes` is renamed to `timeToLiveSeconds` and takes seconds, like Wave Money: 60 to 7200, in whole minutes (a multiple of 60), because KBZ's `timeout_express` counts minutes (`1800` is sent as `30m`). Upgrading: multiply the old value by 60.
36
- - `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS` (`KbzPayConfig`'s `timeToLiveSeconds`) sets the time to live of every KBZ Pay order, like Wave Money's; a payment's own `timeToLiveSeconds` overrides it. It is optional: unset or blank leaves it to KBZ (7200). A value that is not a multiple of 60 from 60 to 7200 throws `ConfigurationError`: `The kbz_pay configuration [time_to_live_in_seconds] must be a multiple of 60 from 60 to 7200.` `ConfigurationError` takes an optional `requirement` for that message.
35
+ - `KbzPayPaymentData.timeoutMinutes` is renamed to `timeToLiveMinutes` (still 1 to 120 minutes, the unit of KBZ's `timeout_express`), matching the new setting.
36
+ - `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES` (`KbzPayConfig`'s `timeToLiveMinutes`) sets the time to live of every KBZ Pay order; a payment's own `timeToLiveMinutes` overrides it. It is optional, and so is Wave Money's `timeToLiveSeconds` (`WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS`): a payment that sets no time to live while its gateway has none configured throws `InvalidPaymentDataError` (`The timeToLiveMinutes field is required when the kbz_pay configuration has no [time_to_live_in_minutes].`, and the same for Wave Money's `timeToLiveSeconds`) before any request is sent. A value that is not a whole number from 1 to 120 throws `ConfigurationError`: `The kbz_pay configuration [time_to_live_in_minutes] must be a whole number from 1 to 120.` `ConfigurationError` takes an optional `requirement` for that message.
@@ -9,6 +9,13 @@ export declare function envFirst(env: EnvSource, ...keys: string[]): string;
9
9
  * @internal
10
10
  */
11
11
  export declare function requireSeconds(gateway: string, key: string, value: unknown): number;
12
+ /**
13
+ * An optional whole number of seconds: `undefined` when unset or blank, otherwise the same rules
14
+ * as {@link requireSeconds}.
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function optionalSeconds(gateway: string, key: string, value: unknown): number | undefined;
12
19
  /** The variable holding the HTTP timeout of every gateway that calls an API. @internal */
13
20
  export declare const HTTP_TIMEOUT_VARIABLE = "MYANMAR_PAYMENTS_HTTP_TIMEOUT";
14
21
  /** Seconds before the default HTTP client gives up on a request when no timeout is configured. */
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_TIMEOUT_SECONDS = exports.HTTP_TIMEOUT_VARIABLE = void 0;
4
4
  exports.envFirst = envFirst;
5
5
  exports.requireSeconds = requireSeconds;
6
+ exports.optionalSeconds = optionalSeconds;
6
7
  exports.resolveTimeout = resolveTimeout;
7
8
  exports.requireSetting = requireSetting;
8
9
  exports.optionalSetting = optionalSetting;
@@ -41,6 +42,18 @@ function requireSeconds(gateway, key, value) {
41
42
  }
42
43
  return seconds;
43
44
  }
45
+ /**
46
+ * An optional whole number of seconds: `undefined` when unset or blank, otherwise the same rules
47
+ * as {@link requireSeconds}.
48
+ *
49
+ * @internal
50
+ */
51
+ function optionalSeconds(gateway, key, value) {
52
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
53
+ return undefined;
54
+ }
55
+ return requireSeconds(gateway, key, value);
56
+ }
44
57
  /** The variable holding the HTTP timeout of every gateway that calls an API. @internal */
45
58
  exports.HTTP_TIMEOUT_VARIABLE = 'MYANMAR_PAYMENTS_HTTP_TIMEOUT';
46
59
  /** Seconds before the default HTTP client gives up on a request when no timeout is configured. */
@@ -23,10 +23,11 @@ export interface KbzPayConfigOptions {
23
23
  /** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
24
24
  pwaUrl?: string | undefined;
25
25
  /**
26
- * Seconds the customer has to pay each order, a multiple of 60 from 60 to 7200 (or its text).
27
- * A payment's own `timeToLiveSeconds` overrides it; unset or blank leaves it to KBZ (7200).
26
+ * Minutes the customer has to pay each order, a whole number from 1 to 120 (or its text).
27
+ * A payment's own `timeToLiveMinutes` overrides it. Unset or blank is allowed, but then every
28
+ * payment must set its own.
28
29
  */
29
- timeToLiveSeconds?: number | string | null | undefined;
30
+ timeToLiveMinutes?: number | string | null | undefined;
30
31
  }
31
32
  /**
32
33
  * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
@@ -44,13 +45,13 @@ export declare class KbzPayConfig {
44
45
  readonly apiUrl: string;
45
46
  /** The PWA checkout URL in use, always ending in `/`. */
46
47
  readonly pwaUrl: string;
47
- /** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
48
- readonly timeToLiveSeconds: number | undefined;
48
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
49
+ readonly timeToLiveMinutes: number | undefined;
49
50
  constructor(options: KbzPayConfigOptions);
50
51
  /**
51
52
  * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
52
53
  * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
53
- * `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
54
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
54
55
  */
55
56
  static fromEnv(env?: EnvSource): KbzPayConfig;
56
57
  }
@@ -67,11 +68,10 @@ export interface KbzPayPaymentData {
67
68
  /** The product name shown to the customer. */
68
69
  title?: string | undefined;
69
70
  /**
70
- * Seconds the customer has to pay before the order expires, 60 to 7200 in whole minutes (a multiple
71
- * of 60). KBZ counts minutes, so 1800 is sent as `30m`. Overrides the config's `timeToLiveSeconds`;
72
- * when both are unset KBZ allows 7200 (120 minutes).
71
+ * Minutes the customer has to pay before the order expires, 1 to 120. Overrides the config's
72
+ * `timeToLiveMinutes`; one of the two is required.
73
73
  */
74
- timeToLiveSeconds?: number | undefined;
74
+ timeToLiveMinutes?: number | undefined;
75
75
  /** Free text KBZ sends back unchanged in the callback, at most 512 characters once URL-encoded. */
76
76
  callbackInfo?: string | undefined;
77
77
  }
@@ -95,13 +95,21 @@ export declare class KbzPay {
95
95
  *
96
96
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
97
97
  * the URL registered with KBZ.
98
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
99
+ * a time to live.
98
100
  */
99
101
  pwa(data: KbzPayPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
100
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
102
+ /**
103
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
104
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
105
+ * a time to live.
106
+ */
101
107
  qr(data: KbzPayPaymentData, options?: RequestOptions): Promise<QrPayment>;
102
108
  /**
103
109
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
104
110
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
111
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
112
+ * a time to live.
105
113
  */
106
114
  app(data: KbzPayPaymentData, options?: RequestOptions): Promise<AppPayment>;
107
115
  /** Asks KBZ Pay for the current state of an order (`queryorder`). */
@@ -12,23 +12,23 @@ const status_js_1 = require("../core/status.js");
12
12
  const validate_js_1 = require("../core/validate.js");
13
13
  const values_js_1 = require("../core/values.js");
14
14
  const signer_js_1 = require("./signer.js");
15
- /** What a valid KBZ Pay `time_to_live_in_seconds` is: KBZ counts whole minutes, up to 120. */
16
- const TIME_TO_LIVE_REQUIREMENT = 'a multiple of 60 from 60 to 7200';
15
+ /** What a valid KBZ Pay `time_to_live_in_minutes` is: KBZ counts whole minutes, up to 120. */
16
+ const TIME_TO_LIVE_REQUIREMENT = 'a whole number from 1 to 120';
17
17
  function resolveTimeToLive(value) {
18
18
  if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
19
19
  return undefined;
20
20
  }
21
- let seconds;
21
+ let minutes;
22
22
  if (typeof value === 'number' && Number.isSafeInteger(value)) {
23
- seconds = value;
23
+ minutes = value;
24
24
  }
25
25
  else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
26
- seconds = Number.parseInt(value.trim(), 10);
26
+ minutes = Number.parseInt(value.trim(), 10);
27
27
  }
28
- if (seconds === undefined || seconds < 60 || seconds > 7200 || seconds % 60 !== 0) {
29
- throw new errors_js_1.ConfigurationError('kbz_pay', 'time_to_live_in_seconds', true, TIME_TO_LIVE_REQUIREMENT);
28
+ if (minutes === undefined || minutes < 1 || minutes > 120) {
29
+ throw new errors_js_1.ConfigurationError('kbz_pay', 'time_to_live_in_minutes', true, TIME_TO_LIVE_REQUIREMENT);
30
30
  }
31
- return seconds;
31
+ return minutes;
32
32
  }
33
33
  /**
34
34
  * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
@@ -46,8 +46,8 @@ class KbzPayConfig {
46
46
  apiUrl;
47
47
  /** The PWA checkout URL in use, always ending in `/`. */
48
48
  pwaUrl;
49
- /** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
50
- timeToLiveSeconds;
49
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
50
+ timeToLiveMinutes;
51
51
  constructor(options) {
52
52
  this.appId = (0, env_js_1.requireSetting)('kbz_pay', 'app_id', options.appId);
53
53
  this.appKey = (0, env_js_1.requireSetting)('kbz_pay', 'app_key', options.appKey);
@@ -55,12 +55,12 @@ class KbzPayConfig {
55
55
  this.timeoutSeconds = (0, env_js_1.resolveTimeout)('kbz_pay', options.timeoutSeconds);
56
56
  this.apiUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
57
57
  this.pwaUrl = `${(0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
58
- this.timeToLiveSeconds = resolveTimeToLive(options.timeToLiveSeconds);
58
+ this.timeToLiveMinutes = resolveTimeToLive(options.timeToLiveMinutes);
59
59
  }
60
60
  /**
61
61
  * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
62
62
  * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
63
- * `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
63
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
64
64
  */
65
65
  static fromEnv(env = (0, env_js_1.defaultEnv)()) {
66
66
  return new KbzPayConfig({
@@ -70,7 +70,7 @@ class KbzPayConfig {
70
70
  timeoutSeconds: (0, env_js_1.envFirst)(env, env_js_1.HTTP_TIMEOUT_VARIABLE),
71
71
  apiUrl: (0, env_js_1.envFirst)(env, 'KBZ_PAY_BASE_URL'),
72
72
  pwaUrl: (0, env_js_1.envFirst)(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
73
- timeToLiveSeconds: (0, env_js_1.envFirst)(env, 'KBZ_PAY_TIME_TO_LIVE_IN_SECONDS'),
73
+ timeToLiveMinutes: (0, env_js_1.envFirst)(env, 'KBZ_PAY_TIME_TO_LIVE_IN_MINUTES'),
74
74
  });
75
75
  }
76
76
  }
@@ -116,8 +116,7 @@ class KbzPay {
116
116
  .max('callbackUrl', callbackUrl, 512)
117
117
  .when(callbackUrl.includes('?'), 'callbackUrl', 'The callbackUrl field must not contain a query string.')
118
118
  .string('title', data.title)
119
- .between('timeToLiveSeconds', data.timeToLiveSeconds, 60, 7200)
120
- .when(Number.isInteger(data.timeToLiveSeconds) && data.timeToLiveSeconds % 60 !== 0, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be a multiple of 60.')
119
+ .between('timeToLiveMinutes', data.timeToLiveMinutes, 1, 120)
121
120
  .string('callbackInfo', data.callbackInfo)
122
121
  .max('callbackInfo', typeof data.callbackInfo === 'string' ? (0, crypto_js_1.queryEscape)(data.callbackInfo) : undefined, 512)
123
122
  .validate();
@@ -127,6 +126,8 @@ class KbzPay {
127
126
  *
128
127
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
129
128
  * the URL registered with KBZ.
129
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
130
+ * a time to live.
130
131
  */
131
132
  async pwa(data, options = {}) {
132
133
  const order = await this.precreate(data, 'PWAAPP', options);
@@ -138,7 +139,11 @@ class KbzPay {
138
139
  raw: (0, json_js_1.toPlainObject)(order.response),
139
140
  });
140
141
  }
141
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
142
+ /**
143
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
144
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
145
+ * a time to live.
146
+ */
142
147
  async qr(data, options = {}) {
143
148
  const order = await this.precreate(data, 'PAY_BY_QRCODE', options);
144
149
  const qrCode = (0, values_js_1.get)(order.response, 'qrCode');
@@ -150,9 +155,7 @@ class KbzPay {
150
155
  return new results_js_1.QrPayment({
151
156
  orderId: data.orderId,
152
157
  qrString: qrCode,
153
- expiresAt: order.timeToLiveSeconds === undefined
154
- ? undefined
155
- : new Date(Date.now() + order.timeToLiveSeconds * 1000),
158
+ expiresAt: new Date(Date.now() + order.timeToLiveMinutes * 60_000),
156
159
  reference: order.prepayId,
157
160
  raw: (0, json_js_1.toPlainObject)(order.response),
158
161
  });
@@ -160,6 +163,8 @@ class KbzPay {
160
163
  /**
161
164
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
162
165
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
166
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
167
+ * a time to live.
163
168
  */
164
169
  async app(data, options = {}) {
165
170
  const order = await this.precreate(data, 'APP', options);
@@ -212,6 +217,12 @@ class KbzPay {
212
217
  }
213
218
  async precreate(data, tradeType, options) {
214
219
  KbzPay.validate(data);
220
+ const timeToLiveMinutes = data.timeToLiveMinutes ?? this.config.timeToLiveMinutes;
221
+ if (timeToLiveMinutes === undefined) {
222
+ throw new errors_js_1.InvalidPaymentDataError({
223
+ timeToLiveMinutes: 'The timeToLiveMinutes field is required when the kbz_pay configuration has no [time_to_live_in_minutes].',
224
+ });
225
+ }
215
226
  const biz = {
216
227
  appid: this.config.appId,
217
228
  merch_code: this.config.merchantCode,
@@ -223,10 +234,7 @@ class KbzPay {
223
234
  if (data.title) {
224
235
  biz.title = data.title;
225
236
  }
226
- const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
227
- if (timeToLiveSeconds !== undefined) {
228
- biz.timeout_express = `${timeToLiveSeconds / 60}m`;
229
- }
237
+ biz.timeout_express = `${timeToLiveMinutes}m`;
230
238
  if (data.callbackInfo) {
231
239
  biz.callback_info = (0, crypto_js_1.queryEscape)(data.callbackInfo);
232
240
  }
@@ -237,7 +245,7 @@ class KbzPay {
237
245
  if (prepayId === '') {
238
246
  throw new errors_js_1.ApiError('KBZ Pay did not return a prepay_id.', { raw: (0, json_js_1.toPlainObject)(response) });
239
247
  }
240
- return { prepayId, nonce, timestamp: time, timeToLiveSeconds, response };
248
+ return { prepayId, nonce, timestamp: time, timeToLiveMinutes, response };
241
249
  }
242
250
  /** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
243
251
  async call(endpoint, method, version, biz, extra, nonce, time, signal) {
@@ -11,8 +11,12 @@ export interface WaveMoneyConfigOptions {
11
11
  secretKey: string;
12
12
  /** Your business name, shown on Wave's payment page. */
13
13
  merchantName: string;
14
- /** How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). */
15
- timeToLiveSeconds: number | string;
14
+ /**
15
+ * How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). A
16
+ * payment's own `timeToLiveSeconds` overrides it. Unset or blank is allowed, but then every
17
+ * payment must set its own.
18
+ */
19
+ timeToLiveSeconds?: number | string | null | undefined;
16
20
  /**
17
21
  * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
18
22
  * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
@@ -34,8 +38,8 @@ export declare class WaveMoneyConfig {
34
38
  readonly merchantId: string;
35
39
  readonly secretKey: string;
36
40
  readonly merchantName: string;
37
- /** Seconds the customer has to pay. */
38
- readonly timeToLiveSeconds: number;
41
+ /** Seconds the customer has to pay unless the payment sets its own; `undefined` when unset. */
42
+ readonly timeToLiveSeconds: number | undefined;
39
43
  /** Seconds before the default HTTP client gives up on a request. */
40
44
  readonly timeoutSeconds: number;
41
45
  /** The API base URL in use. */
@@ -86,7 +90,7 @@ export interface WaveMoneyPaymentData {
86
90
  merchantName?: string | null | undefined;
87
91
  /**
88
92
  * How long the customer has to pay this payment, in seconds: a whole number greater than 0.
89
- * Defaults to the configured `timeToLiveSeconds` when unset.
93
+ * Defaults to the configured `timeToLiveSeconds` when unset; one of the two is required.
90
94
  */
91
95
  timeToLiveSeconds?: number | null | undefined;
92
96
  }
@@ -111,7 +115,8 @@ export declare class WaveMoney {
111
115
  /**
112
116
  * Creates a payment request and returns Wave's page to redirect the customer to. Once `data`
113
117
  * passes validation it fills `data.merchantReferenceId` with a random id when empty, so store it
114
- * afterwards. Invalid data is left untouched.
118
+ * afterwards. Invalid data is left untouched. Throws `InvalidPaymentDataError` before any request
119
+ * when neither the payment nor the config sets a time to live.
115
120
  */
116
121
  initiate(data: WaveMoneyPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
117
122
  /**
@@ -22,7 +22,7 @@ class WaveMoneyConfig {
22
22
  merchantId;
23
23
  secretKey;
24
24
  merchantName;
25
- /** Seconds the customer has to pay. */
25
+ /** Seconds the customer has to pay unless the payment sets its own; `undefined` when unset. */
26
26
  timeToLiveSeconds;
27
27
  /** Seconds before the default HTTP client gives up on a request. */
28
28
  timeoutSeconds;
@@ -34,7 +34,7 @@ class WaveMoneyConfig {
34
34
  this.merchantId = (0, env_js_1.requireSetting)('wave_money', 'merchant_id', options.merchantId);
35
35
  this.secretKey = (0, env_js_1.requireSetting)('wave_money', 'secret_key', options.secretKey);
36
36
  this.merchantName = (0, env_js_1.requireSetting)('wave_money', 'merchant_name', options.merchantName);
37
- this.timeToLiveSeconds = (0, env_js_1.requireSeconds)('wave_money', 'time_to_live_in_seconds', options.timeToLiveSeconds);
37
+ this.timeToLiveSeconds = (0, env_js_1.optionalSeconds)('wave_money', 'time_to_live_in_seconds', options.timeToLiveSeconds);
38
38
  this.timeoutSeconds = (0, env_js_1.resolveTimeout)('wave_money', options.timeoutSeconds);
39
39
  this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ?? WaveMoneyConfig.PRODUCTION_URL);
40
40
  this.authenticateUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.authenticateUrl) ?? WaveMoneyConfig.PRODUCTION_AUTHENTICATE_URL);
@@ -149,15 +149,22 @@ class WaveMoney {
149
149
  /**
150
150
  * Creates a payment request and returns Wave's page to redirect the customer to. Once `data`
151
151
  * passes validation it fills `data.merchantReferenceId` with a random id when empty, so store it
152
- * afterwards. Invalid data is left untouched.
152
+ * afterwards. Invalid data is left untouched. Throws `InvalidPaymentDataError` before any request
153
+ * when neither the payment nor the config sets a time to live.
153
154
  */
154
155
  async initiate(data, options = {}) {
155
156
  WaveMoney.validate(data);
157
+ const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
158
+ if (timeToLiveSeconds === undefined) {
159
+ throw new errors_js_1.InvalidPaymentDataError({
160
+ timeToLiveSeconds: 'The timeToLiveSeconds field is required when the wave_money configuration has no [time_to_live_in_seconds].',
161
+ });
162
+ }
156
163
  if (!data.merchantReferenceId) {
157
164
  data.merchantReferenceId = (0, crypto_js_1.randomHex)();
158
165
  }
159
166
  const amount = WaveMoney.resolvedAmount(data).toString();
160
- const ttl = String(data.timeToLiveSeconds ?? this.config.timeToLiveSeconds);
167
+ const ttl = String(timeToLiveSeconds);
161
168
  // Wave documents items as [{"name": "...", "amount": 1000}] with a numeric amount; the amount
162
169
  // text is written as a JSON number without passing through a float.
163
170
  const items = `[${data.items
@@ -9,6 +9,13 @@ export declare function envFirst(env: EnvSource, ...keys: string[]): string;
9
9
  * @internal
10
10
  */
11
11
  export declare function requireSeconds(gateway: string, key: string, value: unknown): number;
12
+ /**
13
+ * An optional whole number of seconds: `undefined` when unset or blank, otherwise the same rules
14
+ * as {@link requireSeconds}.
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function optionalSeconds(gateway: string, key: string, value: unknown): number | undefined;
12
19
  /** The variable holding the HTTP timeout of every gateway that calls an API. @internal */
13
20
  export declare const HTTP_TIMEOUT_VARIABLE = "MYANMAR_PAYMENTS_HTTP_TIMEOUT";
14
21
  /** Seconds before the default HTTP client gives up on a request when no timeout is configured. */
@@ -31,6 +31,18 @@ export function requireSeconds(gateway, key, value) {
31
31
  }
32
32
  return seconds;
33
33
  }
34
+ /**
35
+ * An optional whole number of seconds: `undefined` when unset or blank, otherwise the same rules
36
+ * as {@link requireSeconds}.
37
+ *
38
+ * @internal
39
+ */
40
+ export function optionalSeconds(gateway, key, value) {
41
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
42
+ return undefined;
43
+ }
44
+ return requireSeconds(gateway, key, value);
45
+ }
34
46
  /** The variable holding the HTTP timeout of every gateway that calls an API. @internal */
35
47
  export const HTTP_TIMEOUT_VARIABLE = 'MYANMAR_PAYMENTS_HTTP_TIMEOUT';
36
48
  /** Seconds before the default HTTP client gives up on a request when no timeout is configured. */
@@ -23,10 +23,11 @@ export interface KbzPayConfigOptions {
23
23
  /** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
24
24
  pwaUrl?: string | undefined;
25
25
  /**
26
- * Seconds the customer has to pay each order, a multiple of 60 from 60 to 7200 (or its text).
27
- * A payment's own `timeToLiveSeconds` overrides it; unset or blank leaves it to KBZ (7200).
26
+ * Minutes the customer has to pay each order, a whole number from 1 to 120 (or its text).
27
+ * A payment's own `timeToLiveMinutes` overrides it. Unset or blank is allowed, but then every
28
+ * payment must set its own.
28
29
  */
29
- timeToLiveSeconds?: number | string | null | undefined;
30
+ timeToLiveMinutes?: number | string | null | undefined;
30
31
  }
31
32
  /**
32
33
  * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
@@ -44,13 +45,13 @@ export declare class KbzPayConfig {
44
45
  readonly apiUrl: string;
45
46
  /** The PWA checkout URL in use, always ending in `/`. */
46
47
  readonly pwaUrl: string;
47
- /** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
48
- readonly timeToLiveSeconds: number | undefined;
48
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
49
+ readonly timeToLiveMinutes: number | undefined;
49
50
  constructor(options: KbzPayConfigOptions);
50
51
  /**
51
52
  * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
52
53
  * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
53
- * `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
54
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
54
55
  */
55
56
  static fromEnv(env?: EnvSource): KbzPayConfig;
56
57
  }
@@ -67,11 +68,10 @@ export interface KbzPayPaymentData {
67
68
  /** The product name shown to the customer. */
68
69
  title?: string | undefined;
69
70
  /**
70
- * Seconds the customer has to pay before the order expires, 60 to 7200 in whole minutes (a multiple
71
- * of 60). KBZ counts minutes, so 1800 is sent as `30m`. Overrides the config's `timeToLiveSeconds`;
72
- * when both are unset KBZ allows 7200 (120 minutes).
71
+ * Minutes the customer has to pay before the order expires, 1 to 120. Overrides the config's
72
+ * `timeToLiveMinutes`; one of the two is required.
73
73
  */
74
- timeToLiveSeconds?: number | undefined;
74
+ timeToLiveMinutes?: number | undefined;
75
75
  /** Free text KBZ sends back unchanged in the callback, at most 512 characters once URL-encoded. */
76
76
  callbackInfo?: string | undefined;
77
77
  }
@@ -95,13 +95,21 @@ export declare class KbzPay {
95
95
  *
96
96
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
97
97
  * the URL registered with KBZ.
98
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
99
+ * a time to live.
98
100
  */
99
101
  pwa(data: KbzPayPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
100
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
102
+ /**
103
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
104
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
105
+ * a time to live.
106
+ */
101
107
  qr(data: KbzPayPaymentData, options?: RequestOptions): Promise<QrPayment>;
102
108
  /**
103
109
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
104
110
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
111
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
112
+ * a time to live.
105
113
  */
106
114
  app(data: KbzPayPaymentData, options?: RequestOptions): Promise<AppPayment>;
107
115
  /** Asks KBZ Pay for the current state of an order (`queryorder`). */
@@ -2,7 +2,7 @@ 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
4
  import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSetting, resolveTimeout, trimUrl, } from '../core/env.js';
5
- import { ApiError, ConfigurationError, SignatureVerificationError } from '../core/errors.js';
5
+ import { ApiError, ConfigurationError, InvalidPaymentDataError, SignatureVerificationError, } from '../core/errors.js';
6
6
  import { httpClientFrom, Transport, } from '../core/http.js';
7
7
  import { toPlainObject } from '../core/json.js';
8
8
  import { AppPayment, QrPayment, RedirectPayment } from '../core/results.js';
@@ -10,23 +10,23 @@ import { resolveStatus } from '../core/status.js';
10
10
  import { toAmount, Validator } from '../core/validate.js';
11
11
  import { get, object, optional, trimmed } from '../core/values.js';
12
12
  import { KbzPaySigner } from './signer.js';
13
- /** What a valid KBZ Pay `time_to_live_in_seconds` is: KBZ counts whole minutes, up to 120. */
14
- const TIME_TO_LIVE_REQUIREMENT = 'a multiple of 60 from 60 to 7200';
13
+ /** What a valid KBZ Pay `time_to_live_in_minutes` is: KBZ counts whole minutes, up to 120. */
14
+ const TIME_TO_LIVE_REQUIREMENT = 'a whole number from 1 to 120';
15
15
  function resolveTimeToLive(value) {
16
16
  if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
17
17
  return undefined;
18
18
  }
19
- let seconds;
19
+ let minutes;
20
20
  if (typeof value === 'number' && Number.isSafeInteger(value)) {
21
- seconds = value;
21
+ minutes = value;
22
22
  }
23
23
  else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
24
- seconds = Number.parseInt(value.trim(), 10);
24
+ minutes = Number.parseInt(value.trim(), 10);
25
25
  }
26
- if (seconds === undefined || seconds < 60 || seconds > 7200 || seconds % 60 !== 0) {
27
- throw new ConfigurationError('kbz_pay', 'time_to_live_in_seconds', true, TIME_TO_LIVE_REQUIREMENT);
26
+ if (minutes === undefined || minutes < 1 || minutes > 120) {
27
+ throw new ConfigurationError('kbz_pay', 'time_to_live_in_minutes', true, TIME_TO_LIVE_REQUIREMENT);
28
28
  }
29
- return seconds;
29
+ return minutes;
30
30
  }
31
31
  /**
32
32
  * KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
@@ -44,8 +44,8 @@ export class KbzPayConfig {
44
44
  apiUrl;
45
45
  /** The PWA checkout URL in use, always ending in `/`. */
46
46
  pwaUrl;
47
- /** Seconds the customer has to pay each order unless the payment sets its own; `undefined` leaves it to KBZ. */
48
- timeToLiveSeconds;
47
+ /** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
48
+ timeToLiveMinutes;
49
49
  constructor(options) {
50
50
  this.appId = requireSetting('kbz_pay', 'app_id', options.appId);
51
51
  this.appKey = requireSetting('kbz_pay', 'app_key', options.appKey);
@@ -53,12 +53,12 @@ export class KbzPayConfig {
53
53
  this.timeoutSeconds = resolveTimeout('kbz_pay', options.timeoutSeconds);
54
54
  this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
55
55
  this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
56
- this.timeToLiveSeconds = resolveTimeToLive(options.timeToLiveSeconds);
56
+ this.timeToLiveMinutes = resolveTimeToLive(options.timeToLiveMinutes);
57
57
  }
58
58
  /**
59
59
  * Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
60
60
  * `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
61
- * `KBZ_PAY_TIME_TO_LIVE_IN_SECONDS`.
61
+ * `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
62
62
  */
63
63
  static fromEnv(env = defaultEnv()) {
64
64
  return new KbzPayConfig({
@@ -68,7 +68,7 @@ export class KbzPayConfig {
68
68
  timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
69
69
  apiUrl: envFirst(env, 'KBZ_PAY_BASE_URL'),
70
70
  pwaUrl: envFirst(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
71
- timeToLiveSeconds: envFirst(env, 'KBZ_PAY_TIME_TO_LIVE_IN_SECONDS'),
71
+ timeToLiveMinutes: envFirst(env, 'KBZ_PAY_TIME_TO_LIVE_IN_MINUTES'),
72
72
  });
73
73
  }
74
74
  }
@@ -113,8 +113,7 @@ export class KbzPay {
113
113
  .max('callbackUrl', callbackUrl, 512)
114
114
  .when(callbackUrl.includes('?'), 'callbackUrl', 'The callbackUrl field must not contain a query string.')
115
115
  .string('title', data.title)
116
- .between('timeToLiveSeconds', data.timeToLiveSeconds, 60, 7200)
117
- .when(Number.isInteger(data.timeToLiveSeconds) && data.timeToLiveSeconds % 60 !== 0, 'timeToLiveSeconds', 'The timeToLiveSeconds field must be a multiple of 60.')
116
+ .between('timeToLiveMinutes', data.timeToLiveMinutes, 1, 120)
118
117
  .string('callbackInfo', data.callbackInfo)
119
118
  .max('callbackInfo', typeof data.callbackInfo === 'string' ? queryEscape(data.callbackInfo) : undefined, 512)
120
119
  .validate();
@@ -124,6 +123,8 @@ export class KbzPay {
124
123
  *
125
124
  * The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
126
125
  * the URL registered with KBZ.
126
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
127
+ * a time to live.
127
128
  */
128
129
  async pwa(data, options = {}) {
129
130
  const order = await this.precreate(data, 'PWAAPP', options);
@@ -135,7 +136,11 @@ export class KbzPay {
135
136
  raw: toPlainObject(order.response),
136
137
  });
137
138
  }
138
- /** Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app. */
139
+ /**
140
+ * Creates an order and returns a QR payload for the customer to scan with the KBZ Pay app.
141
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
142
+ * a time to live.
143
+ */
139
144
  async qr(data, options = {}) {
140
145
  const order = await this.precreate(data, 'PAY_BY_QRCODE', options);
141
146
  const qrCode = get(order.response, 'qrCode');
@@ -147,9 +152,7 @@ export class KbzPay {
147
152
  return new QrPayment({
148
153
  orderId: data.orderId,
149
154
  qrString: qrCode,
150
- expiresAt: order.timeToLiveSeconds === undefined
151
- ? undefined
152
- : new Date(Date.now() + order.timeToLiveSeconds * 1000),
155
+ expiresAt: new Date(Date.now() + order.timeToLiveMinutes * 60_000),
153
156
  reference: order.prepayId,
154
157
  raw: toPlainObject(order.response),
155
158
  });
@@ -157,6 +160,8 @@ export class KbzPay {
157
160
  /**
158
161
  * Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
159
162
  * The SDK's own result only means the payment screen closed; rely on the callback or `status()`.
163
+ * Throws `InvalidPaymentDataError` before any request when neither the payment nor the config sets
164
+ * a time to live.
160
165
  */
161
166
  async app(data, options = {}) {
162
167
  const order = await this.precreate(data, 'APP', options);
@@ -209,6 +214,12 @@ export class KbzPay {
209
214
  }
210
215
  async precreate(data, tradeType, options) {
211
216
  KbzPay.validate(data);
217
+ const timeToLiveMinutes = data.timeToLiveMinutes ?? this.config.timeToLiveMinutes;
218
+ if (timeToLiveMinutes === undefined) {
219
+ throw new InvalidPaymentDataError({
220
+ timeToLiveMinutes: 'The timeToLiveMinutes field is required when the kbz_pay configuration has no [time_to_live_in_minutes].',
221
+ });
222
+ }
212
223
  const biz = {
213
224
  appid: this.config.appId,
214
225
  merch_code: this.config.merchantCode,
@@ -220,10 +231,7 @@ export class KbzPay {
220
231
  if (data.title) {
221
232
  biz.title = data.title;
222
233
  }
223
- const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
224
- if (timeToLiveSeconds !== undefined) {
225
- biz.timeout_express = `${timeToLiveSeconds / 60}m`;
226
- }
234
+ biz.timeout_express = `${timeToLiveMinutes}m`;
227
235
  if (data.callbackInfo) {
228
236
  biz.callback_info = queryEscape(data.callbackInfo);
229
237
  }
@@ -234,7 +242,7 @@ export class KbzPay {
234
242
  if (prepayId === '') {
235
243
  throw new ApiError('KBZ Pay did not return a prepay_id.', { raw: toPlainObject(response) });
236
244
  }
237
- return { prepayId, nonce, timestamp: time, timeToLiveSeconds, response };
245
+ return { prepayId, nonce, timestamp: time, timeToLiveMinutes, response };
238
246
  }
239
247
  /** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
240
248
  async call(endpoint, method, version, biz, extra, nonce, time, signal) {
@@ -11,8 +11,12 @@ export interface WaveMoneyConfigOptions {
11
11
  secretKey: string;
12
12
  /** Your business name, shown on Wave's payment page. */
13
13
  merchantName: string;
14
- /** How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). */
15
- timeToLiveSeconds: number | string;
14
+ /**
15
+ * How long the customer has to pay, in seconds: a whole number greater than 0 (or its text). A
16
+ * payment's own `timeToLiveSeconds` overrides it. Unset or blank is allowed, but then every
17
+ * payment must set its own.
18
+ */
19
+ timeToLiveSeconds?: number | string | null | undefined;
16
20
  /**
17
21
  * Seconds before the default HTTP client gives up on a request, a whole number greater than 0
18
22
  * (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
@@ -34,8 +38,8 @@ export declare class WaveMoneyConfig {
34
38
  readonly merchantId: string;
35
39
  readonly secretKey: string;
36
40
  readonly merchantName: string;
37
- /** Seconds the customer has to pay. */
38
- readonly timeToLiveSeconds: number;
41
+ /** Seconds the customer has to pay unless the payment sets its own; `undefined` when unset. */
42
+ readonly timeToLiveSeconds: number | undefined;
39
43
  /** Seconds before the default HTTP client gives up on a request. */
40
44
  readonly timeoutSeconds: number;
41
45
  /** The API base URL in use. */
@@ -86,7 +90,7 @@ export interface WaveMoneyPaymentData {
86
90
  merchantName?: string | null | undefined;
87
91
  /**
88
92
  * How long the customer has to pay this payment, in seconds: a whole number greater than 0.
89
- * Defaults to the configured `timeToLiveSeconds` when unset.
93
+ * Defaults to the configured `timeToLiveSeconds` when unset; one of the two is required.
90
94
  */
91
95
  timeToLiveSeconds?: number | null | undefined;
92
96
  }
@@ -111,7 +115,8 @@ export declare class WaveMoney {
111
115
  /**
112
116
  * Creates a payment request and returns Wave's page to redirect the customer to. Once `data`
113
117
  * passes validation it fills `data.merchantReferenceId` with a random id when empty, so store it
114
- * afterwards. Invalid data is left untouched.
118
+ * afterwards. Invalid data is left untouched. Throws `InvalidPaymentDataError` before any request
119
+ * when neither the payment nor the config sets a time to live.
115
120
  */
116
121
  initiate(data: WaveMoneyPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
117
122
  /**
@@ -1,8 +1,8 @@
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, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSeconds, requireSetting, resolveTimeout, trimUrl, } from '../core/env.js';
5
- import { ApiError, SignatureVerificationError } from '../core/errors.js';
4
+ import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, optionalSeconds, requireSetting, resolveTimeout, trimUrl, } from '../core/env.js';
5
+ import { ApiError, InvalidPaymentDataError, 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';
@@ -19,7 +19,7 @@ export class WaveMoneyConfig {
19
19
  merchantId;
20
20
  secretKey;
21
21
  merchantName;
22
- /** Seconds the customer has to pay. */
22
+ /** Seconds the customer has to pay unless the payment sets its own; `undefined` when unset. */
23
23
  timeToLiveSeconds;
24
24
  /** Seconds before the default HTTP client gives up on a request. */
25
25
  timeoutSeconds;
@@ -31,7 +31,7 @@ export class WaveMoneyConfig {
31
31
  this.merchantId = requireSetting('wave_money', 'merchant_id', options.merchantId);
32
32
  this.secretKey = requireSetting('wave_money', 'secret_key', options.secretKey);
33
33
  this.merchantName = requireSetting('wave_money', 'merchant_name', options.merchantName);
34
- this.timeToLiveSeconds = requireSeconds('wave_money', 'time_to_live_in_seconds', options.timeToLiveSeconds);
34
+ this.timeToLiveSeconds = optionalSeconds('wave_money', 'time_to_live_in_seconds', options.timeToLiveSeconds);
35
35
  this.timeoutSeconds = resolveTimeout('wave_money', options.timeoutSeconds);
36
36
  this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? WaveMoneyConfig.PRODUCTION_URL);
37
37
  this.authenticateUrl = trimUrl(optionalSetting(options.authenticateUrl) ?? WaveMoneyConfig.PRODUCTION_AUTHENTICATE_URL);
@@ -145,15 +145,22 @@ export class WaveMoney {
145
145
  /**
146
146
  * Creates a payment request and returns Wave's page to redirect the customer to. Once `data`
147
147
  * passes validation it fills `data.merchantReferenceId` with a random id when empty, so store it
148
- * afterwards. Invalid data is left untouched.
148
+ * afterwards. Invalid data is left untouched. Throws `InvalidPaymentDataError` before any request
149
+ * when neither the payment nor the config sets a time to live.
149
150
  */
150
151
  async initiate(data, options = {}) {
151
152
  WaveMoney.validate(data);
153
+ const timeToLiveSeconds = data.timeToLiveSeconds ?? this.config.timeToLiveSeconds;
154
+ if (timeToLiveSeconds === undefined) {
155
+ throw new InvalidPaymentDataError({
156
+ timeToLiveSeconds: 'The timeToLiveSeconds field is required when the wave_money configuration has no [time_to_live_in_seconds].',
157
+ });
158
+ }
152
159
  if (!data.merchantReferenceId) {
153
160
  data.merchantReferenceId = randomHex();
154
161
  }
155
162
  const amount = WaveMoney.resolvedAmount(data).toString();
156
- const ttl = String(data.timeToLiveSeconds ?? this.config.timeToLiveSeconds);
163
+ const ttl = String(timeToLiveSeconds);
157
164
  // Wave documents items as [{"name": "...", "amount": 1000}] with a numeric amount; the amount
158
165
  // text is written as a JSON number without passing through a float.
159
166
  const items = `[${data.items
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laranex/myanmar-payments",
3
- "version": "4.0.0-dev.10",
3
+ "version": "4.0.0-dev.11",
4
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",
@@ -23,7 +23,7 @@ Requires Node.js 20+ and has no runtime dependencies (global `fetch`, `node:cryp
23
23
 
24
24
  ## Configure
25
25
 
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`).
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. Time to live: `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES` (KBZ's `timeToLiveMinutes`, 1 to 120) and `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS` (Wave's `timeToLiveSeconds`, greater than 0) are optional defaults; a payment's `timeToLiveMinutes` / `timeToLiveSeconds` overrides them, and a payment with neither throws `InvalidPaymentDataError` before any request. Every other setting is required, with no defaults: credentials, Wave's `merchantName`, and Yoma's `apiVersion` (e.g. `v1rc`).
27
27
 
28
28
  ```ts
29
29
  import { MyanmarPayments } from '@laranex/myanmar-payments';
@@ -58,7 +58,7 @@ const payment = await kbz.pwa({
58
58
  res.writeHead(302, { Location: payment.url }).end();
59
59
  ```
60
60
 
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).
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 merchant name setting stays required).
62
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.
63
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)`.
64
64
  - `AppPayment` from `kbz.app(data)`: `res.json(payment)` sends `orderId`, `orderInfo`, `sign` and `signType` to your mobile app.