@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 +4 -4
- package/dist/cjs/core/env.d.ts +7 -0
- package/dist/cjs/core/env.js +13 -0
- package/dist/cjs/kbz-pay/kbz-pay.d.ts +19 -11
- package/dist/cjs/kbz-pay/kbz-pay.js +32 -24
- package/dist/cjs/wave-money/wave-money.d.ts +11 -6
- package/dist/cjs/wave-money/wave-money.js +11 -4
- package/dist/esm/core/env.d.ts +7 -0
- package/dist/esm/core/env.js +12 -0
- package/dist/esm/kbz-pay/kbz-pay.d.ts +19 -11
- package/dist/esm/kbz-pay/kbz-pay.js +33 -25
- package/dist/esm/wave-money/wave-money.d.ts +11 -6
- package/dist/esm/wave-money/wave-money.js +13 -6
- package/package.json +1 -1
- package/skills/node-myanmar-payments/SKILL.md +2 -2
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 `
|
|
26
|
-
- In Node: config
|
|
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 `
|
|
36
|
-
- `
|
|
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.
|
package/dist/cjs/core/env.d.ts
CHANGED
|
@@ -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. */
|
package/dist/cjs/core/env.js
CHANGED
|
@@ -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
|
-
*
|
|
27
|
-
* A payment's own `
|
|
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
|
-
|
|
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
|
-
/**
|
|
48
|
-
readonly
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
71
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 `
|
|
16
|
-
const TIME_TO_LIVE_REQUIREMENT = 'a
|
|
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
|
|
21
|
+
let minutes;
|
|
22
22
|
if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
23
|
-
|
|
23
|
+
minutes = value;
|
|
24
24
|
}
|
|
25
25
|
else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
|
|
26
|
-
|
|
26
|
+
minutes = Number.parseInt(value.trim(), 10);
|
|
27
27
|
}
|
|
28
|
-
if (
|
|
29
|
-
throw new errors_js_1.ConfigurationError('kbz_pay', '
|
|
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
|
|
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
|
-
/**
|
|
50
|
-
|
|
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.
|
|
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
|
-
* `
|
|
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
|
-
|
|
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('
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
15
|
-
|
|
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.
|
|
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(
|
|
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
|
package/dist/esm/core/env.d.ts
CHANGED
|
@@ -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. */
|
package/dist/esm/core/env.js
CHANGED
|
@@ -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
|
-
*
|
|
27
|
-
* A payment's own `
|
|
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
|
-
|
|
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
|
-
/**
|
|
48
|
-
readonly
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
71
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 `
|
|
14
|
-
const TIME_TO_LIVE_REQUIREMENT = 'a
|
|
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
|
|
19
|
+
let minutes;
|
|
20
20
|
if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
21
|
-
|
|
21
|
+
minutes = value;
|
|
22
22
|
}
|
|
23
23
|
else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
|
|
24
|
-
|
|
24
|
+
minutes = Number.parseInt(value.trim(), 10);
|
|
25
25
|
}
|
|
26
|
-
if (
|
|
27
|
-
throw new ConfigurationError('kbz_pay', '
|
|
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
|
|
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
|
-
/**
|
|
48
|
-
|
|
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.
|
|
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
|
-
* `
|
|
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
|
-
|
|
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('
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
15
|
-
|
|
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,
|
|
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 =
|
|
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(
|
|
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.
|
|
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. `
|
|
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
|
|
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.
|