@laranex/myanmar-payments 4.0.0-dev.1 → 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 +19 -4
- package/README.md +3 -3
- package/dist/cjs/aya-pay/aya-pay.d.ts +13 -9
- package/dist/cjs/aya-pay/aya-pay.js +16 -12
- package/dist/cjs/core/amount.d.ts +4 -3
- package/dist/cjs/core/amount.js +13 -4
- package/dist/cjs/core/callback.d.ts +14 -8
- package/dist/cjs/core/callback.js +19 -11
- package/dist/cjs/core/crypto.d.ts +6 -1
- package/dist/cjs/core/crypto.js +17 -3
- package/dist/cjs/core/env.d.ts +21 -5
- package/dist/cjs/core/env.js +49 -12
- package/dist/cjs/core/errors.d.ts +9 -3
- package/dist/cjs/core/errors.js +12 -4
- package/dist/cjs/core/http.d.ts +10 -11
- package/dist/cjs/core/http.js +7 -9
- package/dist/cjs/core/index.d.ts +2 -2
- package/dist/cjs/core/index.js +3 -2
- package/dist/cjs/core/json.d.ts +2 -2
- package/dist/cjs/core/json.js +3 -3
- package/dist/cjs/core/status.d.ts +3 -3
- package/dist/cjs/core/status.js +2 -2
- package/dist/cjs/core/values.d.ts +2 -0
- package/dist/cjs/core/values.js +5 -0
- package/dist/cjs/cyber-source/cyber-source.d.ts +12 -14
- package/dist/cjs/cyber-source/cyber-source.js +39 -26
- package/dist/cjs/kbz-pay/kbz-pay.d.ts +36 -12
- package/dist/cjs/kbz-pay/kbz-pay.js +54 -23
- package/dist/cjs/kbz-pay/signer.d.ts +4 -1
- package/dist/cjs/kbz-pay/signer.js +7 -2
- package/dist/cjs/wave-money/wave-money.d.ts +34 -16
- package/dist/cjs/wave-money/wave-money.js +38 -31
- package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +17 -13
- package/dist/cjs/yoma-mmqr/yoma-mmqr.js +21 -19
- package/dist/esm/aya-pay/aya-pay.d.ts +13 -9
- package/dist/esm/aya-pay/aya-pay.js +18 -14
- package/dist/esm/core/amount.d.ts +4 -3
- package/dist/esm/core/amount.js +13 -4
- package/dist/esm/core/callback.d.ts +14 -8
- package/dist/esm/core/callback.js +19 -11
- package/dist/esm/core/crypto.d.ts +6 -1
- package/dist/esm/core/crypto.js +17 -3
- package/dist/esm/core/env.d.ts +21 -5
- package/dist/esm/core/env.js +45 -10
- package/dist/esm/core/errors.d.ts +9 -3
- package/dist/esm/core/errors.js +12 -4
- package/dist/esm/core/http.d.ts +10 -11
- package/dist/esm/core/http.js +6 -8
- package/dist/esm/core/index.d.ts +2 -2
- package/dist/esm/core/index.js +2 -1
- package/dist/esm/core/json.d.ts +2 -2
- package/dist/esm/core/json.js +3 -3
- package/dist/esm/core/status.d.ts +3 -3
- package/dist/esm/core/status.js +2 -2
- package/dist/esm/core/values.d.ts +2 -0
- package/dist/esm/core/values.js +4 -0
- package/dist/esm/cyber-source/cyber-source.d.ts +12 -14
- package/dist/esm/cyber-source/cyber-source.js +41 -28
- package/dist/esm/kbz-pay/kbz-pay.d.ts +36 -12
- package/dist/esm/kbz-pay/kbz-pay.js +56 -25
- package/dist/esm/kbz-pay/signer.d.ts +4 -1
- package/dist/esm/kbz-pay/signer.js +8 -3
- package/dist/esm/wave-money/wave-money.d.ts +34 -16
- package/dist/esm/wave-money/wave-money.js +41 -34
- package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +17 -13
- package/dist/esm/yoma-mmqr/yoma-mmqr.js +23 -21
- package/package.json +2 -2
- package/skills/node-myanmar-payments/SKILL.md +26 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A gateway-independent payment status. Every gateway's own status values are mapped onto these.
|
|
3
3
|
*/
|
|
4
|
-
export type PaymentStatus = 'successful' | 'pending' | 'failed' | '
|
|
4
|
+
export type PaymentStatus = 'successful' | 'pending' | 'failed' | 'canceled' | 'expired' | 'unknown';
|
|
5
5
|
/**
|
|
6
6
|
* The payment statuses, plus helpers.
|
|
7
7
|
*
|
|
@@ -18,13 +18,13 @@ export declare const PaymentStatus: Readonly<{
|
|
|
18
18
|
/** The payment was attempted and failed or was rejected. */
|
|
19
19
|
Failed: "failed";
|
|
20
20
|
/** The payment or order was canceled or closed before completing. */
|
|
21
|
-
|
|
21
|
+
Canceled: "canceled";
|
|
22
22
|
/** The payment window ran out before the customer paid. */
|
|
23
23
|
Expired: "expired";
|
|
24
24
|
/** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
|
|
25
25
|
Unknown: "unknown";
|
|
26
26
|
/** Every status, in the order above. */
|
|
27
|
-
values: readonly ["successful", "pending", "failed", "
|
|
27
|
+
values: readonly ["successful", "pending", "failed", "canceled", "expired", "unknown"];
|
|
28
28
|
/** Whether the status will not change any more: `false` for `pending` and `unknown`. */
|
|
29
29
|
isFinal(status: PaymentStatus): boolean;
|
|
30
30
|
}>;
|
package/dist/esm/core/status.js
CHANGED
|
@@ -14,7 +14,7 @@ export const PaymentStatus = Object.freeze({
|
|
|
14
14
|
/** The payment was attempted and failed or was rejected. */
|
|
15
15
|
Failed: 'failed',
|
|
16
16
|
/** The payment or order was canceled or closed before completing. */
|
|
17
|
-
|
|
17
|
+
Canceled: 'canceled',
|
|
18
18
|
/** The payment window ran out before the customer paid. */
|
|
19
19
|
Expired: 'expired',
|
|
20
20
|
/** The gateway sent a status this package does not recognize. Inspect `gatewayStatus`. */
|
|
@@ -24,7 +24,7 @@ export const PaymentStatus = Object.freeze({
|
|
|
24
24
|
'successful',
|
|
25
25
|
'pending',
|
|
26
26
|
'failed',
|
|
27
|
-
'
|
|
27
|
+
'canceled',
|
|
28
28
|
'expired',
|
|
29
29
|
'unknown',
|
|
30
30
|
]),
|
|
@@ -14,3 +14,5 @@ export declare function optional(source: Record<string, unknown>, key: string):
|
|
|
14
14
|
export declare function trimmed(source: Record<string, unknown>, key: string): string;
|
|
15
15
|
/** The value at `key` when it is an object (not an array). @internal */
|
|
16
16
|
export declare function object(source: Record<string, unknown>, key: string): LosslessObject | undefined;
|
|
17
|
+
/** Whether `value` is an object or array, which a gateway never signs. @internal */
|
|
18
|
+
export declare function isNested(value: unknown): boolean;
|
package/dist/esm/core/values.js
CHANGED
|
@@ -36,3 +36,7 @@ export function object(source, key) {
|
|
|
36
36
|
const value = source[key];
|
|
37
37
|
return isLosslessObject(value) ? value : undefined;
|
|
38
38
|
}
|
|
39
|
+
/** Whether `value` is an object or array, which a gateway never signs. @internal */
|
|
40
|
+
export function isNested(value) {
|
|
41
|
+
return typeof value === 'object' && value !== null && !(value instanceof JsonNumber);
|
|
42
|
+
}
|
|
@@ -10,27 +10,24 @@ export interface CyberSourceConfigOptions {
|
|
|
10
10
|
accessKey: string;
|
|
11
11
|
/** The profile's secret key, which signs the fields. */
|
|
12
12
|
secretKey: string;
|
|
13
|
-
/**
|
|
14
|
-
sandbox?: boolean | undefined;
|
|
15
|
-
/** Overrides the Secure Acceptance base URL. */
|
|
13
|
+
/** Overrides the Secure Acceptance base URL, e.g. to point at the test environment. */
|
|
16
14
|
baseUrl?: string | undefined;
|
|
17
15
|
}
|
|
18
16
|
/**
|
|
19
|
-
* A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`.
|
|
17
|
+
* A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`. The
|
|
18
|
+
* URL defaults to production.
|
|
20
19
|
*/
|
|
21
20
|
export declare class CyberSourceConfig {
|
|
22
|
-
static readonly SANDBOX_URL = "https://testsecureacceptance.cybersource.com";
|
|
23
21
|
static readonly PRODUCTION_URL = "https://secureacceptance.cybersource.com";
|
|
24
22
|
readonly profileId: string;
|
|
25
23
|
readonly accessKey: string;
|
|
26
24
|
readonly secretKey: string;
|
|
27
|
-
readonly sandbox: boolean;
|
|
28
25
|
/** The base URL in use. */
|
|
29
26
|
readonly baseUrl: string;
|
|
30
27
|
constructor(options: CyberSourceConfigOptions);
|
|
31
28
|
/**
|
|
32
29
|
* Reads `CYBER_SOURCE_PROFILE_ID`, `CYBER_SOURCE_ACCESS_KEY`, `CYBER_SOURCE_SECRET_KEY`,
|
|
33
|
-
*
|
|
30
|
+
* and `CYBER_SOURCE_BASE_URL`.
|
|
34
31
|
*/
|
|
35
32
|
static fromEnv(env?: EnvSource): CyberSourceConfig;
|
|
36
33
|
}
|
|
@@ -61,12 +58,12 @@ export interface CyberSourcePaymentData {
|
|
|
61
58
|
returnUrl?: string | undefined;
|
|
62
59
|
/** The page shown on cancel (`override_custom_cancel_page`), at most 255 characters. */
|
|
63
60
|
cancelUrl?: string | undefined;
|
|
64
|
-
/** An ISO 4217 code
|
|
65
|
-
currency
|
|
66
|
-
/** What to do with the card
|
|
67
|
-
transactionType
|
|
68
|
-
/** The hosted page language, e.g. `en-us
|
|
69
|
-
locale
|
|
61
|
+
/** An ISO 4217 code, e.g. `MMK`. */
|
|
62
|
+
currency: string;
|
|
63
|
+
/** What to do with the card, e.g. `sale`. */
|
|
64
|
+
transactionType: CyberSourceTransactionType;
|
|
65
|
+
/** The hosted page language, e.g. `en-us`. */
|
|
66
|
+
locale: string;
|
|
70
67
|
}
|
|
71
68
|
/**
|
|
72
69
|
* CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
|
|
@@ -87,7 +84,8 @@ export declare class CyberSource {
|
|
|
87
84
|
initiate(data: CyberSourcePaymentData): FormPayment;
|
|
88
85
|
/**
|
|
89
86
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
90
|
-
* page.
|
|
87
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
88
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
91
89
|
*/
|
|
92
90
|
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
93
91
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
@@ -1,43 +1,39 @@
|
|
|
1
1
|
import {} from '../core/amount.js';
|
|
2
2
|
import { CallbackRequest, losslessInput, PaymentCallback } from '../core/callback.js';
|
|
3
3
|
import { hmacSha256Base64, randomHex, safeEqual } from '../core/crypto.js';
|
|
4
|
-
import { defaultEnv, envFirst,
|
|
4
|
+
import { defaultEnv, envFirst, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
|
|
5
5
|
import { SignatureVerificationError } from '../core/errors.js';
|
|
6
|
-
import { toPlainObject } from '../core/json.js';
|
|
6
|
+
import { setKey, toPlainObject } from '../core/json.js';
|
|
7
7
|
import { FormPayment } from '../core/results.js';
|
|
8
8
|
import { resolveStatus } from '../core/status.js';
|
|
9
9
|
import { toAmount, Validator } from '../core/validate.js';
|
|
10
10
|
import { get, optional, scalarString, trimmed } from '../core/values.js';
|
|
11
11
|
/**
|
|
12
|
-
* A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`.
|
|
12
|
+
* A CyberSource Secure Acceptance profile. A missing credential throws a `ConfigurationError`. The
|
|
13
|
+
* URL defaults to production.
|
|
13
14
|
*/
|
|
14
15
|
export class CyberSourceConfig {
|
|
15
|
-
static SANDBOX_URL = 'https://testsecureacceptance.cybersource.com';
|
|
16
16
|
static PRODUCTION_URL = 'https://secureacceptance.cybersource.com';
|
|
17
17
|
profileId;
|
|
18
18
|
accessKey;
|
|
19
19
|
secretKey;
|
|
20
|
-
sandbox;
|
|
21
20
|
/** The base URL in use. */
|
|
22
21
|
baseUrl;
|
|
23
22
|
constructor(options) {
|
|
24
23
|
this.profileId = requireSetting('cyber_source', 'profile_id', options.profileId);
|
|
25
24
|
this.accessKey = requireSetting('cyber_source', 'access_key', options.accessKey);
|
|
26
25
|
this.secretKey = requireSetting('cyber_source', 'secret_key', options.secretKey);
|
|
27
|
-
this.
|
|
28
|
-
this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
|
|
29
|
-
(this.sandbox ? CyberSourceConfig.SANDBOX_URL : CyberSourceConfig.PRODUCTION_URL));
|
|
26
|
+
this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ?? CyberSourceConfig.PRODUCTION_URL);
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
29
|
* Reads `CYBER_SOURCE_PROFILE_ID`, `CYBER_SOURCE_ACCESS_KEY`, `CYBER_SOURCE_SECRET_KEY`,
|
|
33
|
-
*
|
|
30
|
+
* and `CYBER_SOURCE_BASE_URL`.
|
|
34
31
|
*/
|
|
35
32
|
static fromEnv(env = defaultEnv()) {
|
|
36
33
|
return new CyberSourceConfig({
|
|
37
34
|
profileId: envFirst(env, 'CYBER_SOURCE_PROFILE_ID'),
|
|
38
35
|
accessKey: envFirst(env, 'CYBER_SOURCE_ACCESS_KEY'),
|
|
39
36
|
secretKey: envFirst(env, 'CYBER_SOURCE_SECRET_KEY'),
|
|
40
|
-
sandbox: envSandbox(env, 'CYBER_SOURCE_SANDBOX'),
|
|
41
37
|
baseUrl: envFirst(env, 'CYBER_SOURCE_BASE_URL'),
|
|
42
38
|
});
|
|
43
39
|
}
|
|
@@ -74,7 +70,7 @@ const STATUSES = {
|
|
|
74
70
|
REVIEW: 'pending',
|
|
75
71
|
DECLINE: 'failed',
|
|
76
72
|
ERROR: 'failed',
|
|
77
|
-
CANCEL: '
|
|
73
|
+
CANCEL: 'canceled',
|
|
78
74
|
};
|
|
79
75
|
/**
|
|
80
76
|
* CyberSource Secure Acceptance hosted checkout for card payments: signs the form and verifies the
|
|
@@ -110,9 +106,12 @@ export class CyberSource {
|
|
|
110
106
|
.string('cancelUrl', data.cancelUrl)
|
|
111
107
|
.url('cancelUrl', data.cancelUrl)
|
|
112
108
|
.max('cancelUrl', data.cancelUrl, 255)
|
|
113
|
-
.
|
|
114
|
-
.pattern('
|
|
115
|
-
.
|
|
109
|
+
.required('currency', data.currency)
|
|
110
|
+
.pattern('currency', data.currency, /^[A-Z]{3}$/, 'a three letter ISO 4217 code')
|
|
111
|
+
.required('transactionType', data.transactionType)
|
|
112
|
+
.when(!TRANSACTION_TYPES.includes(String(data.transactionType)), 'transactionType', 'The transactionType field is not a supported transaction type.')
|
|
113
|
+
.required('locale', data.locale)
|
|
114
|
+
.pattern('locale', data.locale, /^[a-z]{2}-[a-z]{2}$/, 'a locale code such as en-us')
|
|
116
115
|
.validate();
|
|
117
116
|
}
|
|
118
117
|
/**
|
|
@@ -127,11 +126,11 @@ export class CyberSource {
|
|
|
127
126
|
transaction_uuid: randomHex(),
|
|
128
127
|
signed_field_names: SIGNED_FIELDS.join(','),
|
|
129
128
|
signed_date_time: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
130
|
-
locale: data.locale
|
|
131
|
-
transaction_type: data.transactionType
|
|
129
|
+
locale: data.locale,
|
|
130
|
+
transaction_type: data.transactionType,
|
|
132
131
|
reference_number: data.orderId,
|
|
133
132
|
amount: String(toAmount(data.amount)),
|
|
134
|
-
currency: data.currency
|
|
133
|
+
currency: data.currency,
|
|
135
134
|
override_custom_receipt_page: data.returnUrl ?? '',
|
|
136
135
|
override_backoffice_post_url: data.callbackUrl,
|
|
137
136
|
override_custom_cancel_page: data.cancelUrl ?? '',
|
|
@@ -146,31 +145,39 @@ export class CyberSource {
|
|
|
146
145
|
}
|
|
147
146
|
/**
|
|
148
147
|
* Verifies CyberSource's result post. The same check works for the browser post to your receipt
|
|
149
|
-
* page.
|
|
148
|
+
* page. Only the fields listed in `signed_field_names` are trusted: `decision` and
|
|
149
|
+
* `req_reference_number` must be signed, and unsigned fields are left out of the result.
|
|
150
150
|
*/
|
|
151
151
|
handleCallback(request) {
|
|
152
152
|
const payload = losslessInput(request);
|
|
153
153
|
const expected = this.sign(payload);
|
|
154
|
-
|
|
154
|
+
const names = signedNames(payload);
|
|
155
|
+
if (expected === undefined ||
|
|
156
|
+
!safeEqual(expected, get(payload, 'signature')) ||
|
|
157
|
+
!names.includes('decision') ||
|
|
158
|
+
!names.includes('req_reference_number')) {
|
|
155
159
|
throw new SignatureVerificationError('CyberSource callback signature verification failed.', toPlainObject(payload));
|
|
156
160
|
}
|
|
157
|
-
|
|
161
|
+
// A signature only covers the listed fields, so anything else in the post could have been
|
|
162
|
+
// added by the sender (e.g. a `decision` next to the signed request fields).
|
|
163
|
+
const signed = {};
|
|
164
|
+
for (const name of [...names, 'signature']) {
|
|
165
|
+
setKey(signed, name, payload[name]);
|
|
166
|
+
}
|
|
167
|
+
const decision = trimmed(signed, 'decision').toUpperCase();
|
|
158
168
|
return new PaymentCallback({
|
|
159
|
-
orderId: get(
|
|
169
|
+
orderId: get(signed, 'req_reference_number'),
|
|
160
170
|
status: resolveStatus(STATUSES, decision),
|
|
161
171
|
gatewayStatus: decision,
|
|
162
|
-
gatewayReference: optional(
|
|
163
|
-
amount: get(
|
|
164
|
-
raw: toPlainObject(
|
|
172
|
+
gatewayReference: optional(signed, 'transaction_id'),
|
|
173
|
+
amount: get(signed, 'auth_amount') || optional(signed, 'req_amount'),
|
|
174
|
+
raw: toPlainObject(signed),
|
|
165
175
|
});
|
|
166
176
|
}
|
|
167
177
|
/** Signs the fields listed in `signed_field_names`; `undefined` when a listed field is missing. */
|
|
168
178
|
sign(fields) {
|
|
169
179
|
const pairs = [];
|
|
170
|
-
for (const name of
|
|
171
|
-
if (name === '') {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
180
|
+
for (const name of signedNames(fields)) {
|
|
174
181
|
const value = scalarString(fields[name]);
|
|
175
182
|
if (value === undefined) {
|
|
176
183
|
return undefined;
|
|
@@ -182,3 +189,9 @@ export class CyberSource {
|
|
|
182
189
|
: hmacSha256Base64(this.config.secretKey, pairs.join(','));
|
|
183
190
|
}
|
|
184
191
|
}
|
|
192
|
+
/** The names listed in `signed_field_names`, in order. */
|
|
193
|
+
function signedNames(fields) {
|
|
194
|
+
return get(fields, 'signed_field_names')
|
|
195
|
+
.split(',')
|
|
196
|
+
.filter((name) => name !== '');
|
|
197
|
+
}
|
|
@@ -12,33 +12,46 @@ export interface KbzPayConfigOptions {
|
|
|
12
12
|
appKey: string;
|
|
13
13
|
/** The `merch_code` KBZ issued. */
|
|
14
14
|
merchantCode: string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Seconds before the default HTTP client gives up on a request, a whole number greater than 0
|
|
17
|
+
* (or its text). Defaults to 30 (`DEFAULT_TIMEOUT_SECONDS`) when unset or blank. A
|
|
18
|
+
* client you pass keeps its own timeout.
|
|
19
|
+
*/
|
|
20
|
+
timeoutSeconds?: number | string | null | undefined;
|
|
21
|
+
/** Overrides the API base URL, e.g. to point at UAT or go through a proxy. */
|
|
18
22
|
apiUrl?: string | undefined;
|
|
19
23
|
/** Overrides the PWA checkout URL. A trailing `#` or `#/` is normalized to `#/`. */
|
|
20
24
|
pwaUrl?: string | undefined;
|
|
25
|
+
/**
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
timeToLiveMinutes?: number | string | null | undefined;
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
|
-
* KBZ Pay credentials and endpoints. A missing
|
|
33
|
+
* KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
|
|
34
|
+
* default to production; UAT and production issue separate credentials.
|
|
24
35
|
*/
|
|
25
36
|
export declare class KbzPayConfig {
|
|
26
|
-
static readonly SANDBOX_API_URL = "http://api-uat.kbzpay.com/payment/gateway/uat";
|
|
27
37
|
static readonly PRODUCTION_API_URL = "https://api.kbzpay.com/payment/gateway";
|
|
28
|
-
static readonly SANDBOX_PWA_URL = "https://static.kbzpay.com/pgw/uat/pwa/#/";
|
|
29
38
|
static readonly PRODUCTION_PWA_URL = "https://wap.kbzpay.com/pgw/pwa/#/";
|
|
30
39
|
readonly appId: string;
|
|
31
40
|
readonly appKey: string;
|
|
32
41
|
readonly merchantCode: string;
|
|
33
|
-
|
|
42
|
+
/** Seconds before the default HTTP client gives up on a request. */
|
|
43
|
+
readonly timeoutSeconds: number;
|
|
34
44
|
/** The API base URL in use. */
|
|
35
45
|
readonly apiUrl: string;
|
|
36
46
|
/** The PWA checkout URL in use, always ending in `/`. */
|
|
37
47
|
readonly pwaUrl: string;
|
|
48
|
+
/** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
|
|
49
|
+
readonly timeToLiveMinutes: number | undefined;
|
|
38
50
|
constructor(options: KbzPayConfigOptions);
|
|
39
51
|
/**
|
|
40
|
-
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
41
|
-
* `KBZ_PAY_BASE_URL` and
|
|
52
|
+
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
53
|
+
* `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
|
|
54
|
+
* `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
|
|
42
55
|
*/
|
|
43
56
|
static fromEnv(env?: EnvSource): KbzPayConfig;
|
|
44
57
|
}
|
|
@@ -54,8 +67,11 @@ export interface KbzPayPaymentData {
|
|
|
54
67
|
callbackUrl: string;
|
|
55
68
|
/** The product name shown to the customer. */
|
|
56
69
|
title?: string | undefined;
|
|
57
|
-
/**
|
|
58
|
-
|
|
70
|
+
/**
|
|
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
|
+
*/
|
|
74
|
+
timeToLiveMinutes?: number | undefined;
|
|
59
75
|
/** Free text KBZ sends back unchanged in the callback, at most 512 characters once URL-encoded. */
|
|
60
76
|
callbackInfo?: string | undefined;
|
|
61
77
|
}
|
|
@@ -79,13 +95,21 @@ export declare class KbzPay {
|
|
|
79
95
|
*
|
|
80
96
|
* The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
|
|
81
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.
|
|
82
100
|
*/
|
|
83
101
|
pwa(data: KbzPayPaymentData, options?: RequestOptions): Promise<RedirectPayment>;
|
|
84
|
-
/**
|
|
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
|
+
*/
|
|
85
107
|
qr(data: KbzPayPaymentData, options?: RequestOptions): Promise<QrPayment>;
|
|
86
108
|
/**
|
|
87
109
|
* Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
|
|
88
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.
|
|
89
113
|
*/
|
|
90
114
|
app(data: KbzPayPaymentData, options?: RequestOptions): Promise<AppPayment>;
|
|
91
115
|
/** Asks KBZ Pay for the current state of an order (`queryorder`). */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {} from '../core/amount.js';
|
|
2
2
|
import { Acknowledgement, CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
|
|
3
3
|
import { queryEscape, randomHex } from '../core/crypto.js';
|
|
4
|
-
import { defaultEnv, envFirst,
|
|
5
|
-
import { ApiError, SignatureVerificationError } from '../core/errors.js';
|
|
4
|
+
import { defaultEnv, envFirst, HTTP_TIMEOUT_VARIABLE, optionalSetting, requireSetting, resolveTimeout, trimUrl, } from '../core/env.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,44 +10,65 @@ 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_minutes` is: KBZ counts whole minutes, up to 120. */
|
|
14
|
+
const TIME_TO_LIVE_REQUIREMENT = 'a whole number from 1 to 120';
|
|
15
|
+
function resolveTimeToLive(value) {
|
|
16
|
+
if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
let minutes;
|
|
20
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
21
|
+
minutes = value;
|
|
22
|
+
}
|
|
23
|
+
else if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) {
|
|
24
|
+
minutes = Number.parseInt(value.trim(), 10);
|
|
25
|
+
}
|
|
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
|
+
}
|
|
29
|
+
return minutes;
|
|
30
|
+
}
|
|
13
31
|
/**
|
|
14
|
-
* KBZ Pay credentials and endpoints. A missing
|
|
32
|
+
* KBZ Pay credentials and endpoints. A missing setting throws a `ConfigurationError`. The URLs
|
|
33
|
+
* default to production; UAT and production issue separate credentials.
|
|
15
34
|
*/
|
|
16
35
|
export class KbzPayConfig {
|
|
17
|
-
static SANDBOX_API_URL = 'http://api-uat.kbzpay.com/payment/gateway/uat';
|
|
18
36
|
static PRODUCTION_API_URL = 'https://api.kbzpay.com/payment/gateway';
|
|
19
|
-
static SANDBOX_PWA_URL = 'https://static.kbzpay.com/pgw/uat/pwa/#/';
|
|
20
37
|
static PRODUCTION_PWA_URL = 'https://wap.kbzpay.com/pgw/pwa/#/';
|
|
21
38
|
appId;
|
|
22
39
|
appKey;
|
|
23
40
|
merchantCode;
|
|
24
|
-
|
|
41
|
+
/** Seconds before the default HTTP client gives up on a request. */
|
|
42
|
+
timeoutSeconds;
|
|
25
43
|
/** The API base URL in use. */
|
|
26
44
|
apiUrl;
|
|
27
45
|
/** The PWA checkout URL in use, always ending in `/`. */
|
|
28
46
|
pwaUrl;
|
|
47
|
+
/** Minutes the customer has to pay each order unless the payment sets its own; `undefined` when unset. */
|
|
48
|
+
timeToLiveMinutes;
|
|
29
49
|
constructor(options) {
|
|
30
50
|
this.appId = requireSetting('kbz_pay', 'app_id', options.appId);
|
|
31
51
|
this.appKey = requireSetting('kbz_pay', 'app_key', options.appKey);
|
|
32
52
|
this.merchantCode = requireSetting('kbz_pay', 'merchant_code', options.merchantCode);
|
|
33
|
-
this.
|
|
34
|
-
this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ??
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
(this.sandbox ? KbzPayConfig.SANDBOX_PWA_URL : KbzPayConfig.PRODUCTION_PWA_URL))}/`;
|
|
53
|
+
this.timeoutSeconds = resolveTimeout('kbz_pay', options.timeoutSeconds);
|
|
54
|
+
this.apiUrl = trimUrl(optionalSetting(options.apiUrl) ?? KbzPayConfig.PRODUCTION_API_URL);
|
|
55
|
+
this.pwaUrl = `${trimUrl(optionalSetting(options.pwaUrl) ?? KbzPayConfig.PRODUCTION_PWA_URL)}/`;
|
|
56
|
+
this.timeToLiveMinutes = resolveTimeToLive(options.timeToLiveMinutes);
|
|
38
57
|
}
|
|
39
58
|
/**
|
|
40
|
-
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
41
|
-
* `KBZ_PAY_BASE_URL` and
|
|
59
|
+
* Reads `KBZ_PAY_APP_ID`, `KBZ_PAY_APP_KEY`, `KBZ_PAY_MERCHANT_CODE`,
|
|
60
|
+
* `MYANMAR_PAYMENTS_HTTP_TIMEOUT`, `KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL` and
|
|
61
|
+
* `KBZ_PAY_TIME_TO_LIVE_IN_MINUTES`.
|
|
42
62
|
*/
|
|
43
63
|
static fromEnv(env = defaultEnv()) {
|
|
44
64
|
return new KbzPayConfig({
|
|
45
65
|
appId: envFirst(env, 'KBZ_PAY_APP_ID'),
|
|
46
66
|
appKey: envFirst(env, 'KBZ_PAY_APP_KEY'),
|
|
47
67
|
merchantCode: envFirst(env, 'KBZ_PAY_MERCHANT_CODE'),
|
|
48
|
-
|
|
68
|
+
timeoutSeconds: envFirst(env, HTTP_TIMEOUT_VARIABLE),
|
|
49
69
|
apiUrl: envFirst(env, 'KBZ_PAY_BASE_URL'),
|
|
50
70
|
pwaUrl: envFirst(env, 'KBZ_PAY_PWA_BASE_REDIRECT_URL'),
|
|
71
|
+
timeToLiveMinutes: envFirst(env, 'KBZ_PAY_TIME_TO_LIVE_IN_MINUTES'),
|
|
51
72
|
});
|
|
52
73
|
}
|
|
53
74
|
}
|
|
@@ -57,7 +78,7 @@ const STATUSES = {
|
|
|
57
78
|
WAIT_PAY: 'pending',
|
|
58
79
|
PAYING: 'pending',
|
|
59
80
|
PAY_FAILED: 'failed',
|
|
60
|
-
ORDER_CLOSED: '
|
|
81
|
+
ORDER_CLOSED: 'canceled',
|
|
61
82
|
ORDER_EXPIRED: 'expired',
|
|
62
83
|
};
|
|
63
84
|
/**
|
|
@@ -73,7 +94,7 @@ export class KbzPay {
|
|
|
73
94
|
constructor(config, options = {}) {
|
|
74
95
|
this.config = config instanceof KbzPayConfig ? config : new KbzPayConfig(config);
|
|
75
96
|
this.signer = new KbzPaySigner(this.config.appKey);
|
|
76
|
-
this.transport = new Transport(httpClientFrom(options));
|
|
97
|
+
this.transport = new Transport(httpClientFrom(options, this.config.timeoutSeconds));
|
|
77
98
|
}
|
|
78
99
|
/** A gateway configured from the `KBZ_PAY_*` environment variables. */
|
|
79
100
|
static fromEnv(env = defaultEnv(), options = {}) {
|
|
@@ -92,7 +113,7 @@ export class KbzPay {
|
|
|
92
113
|
.max('callbackUrl', callbackUrl, 512)
|
|
93
114
|
.when(callbackUrl.includes('?'), 'callbackUrl', 'The callbackUrl field must not contain a query string.')
|
|
94
115
|
.string('title', data.title)
|
|
95
|
-
.between('
|
|
116
|
+
.between('timeToLiveMinutes', data.timeToLiveMinutes, 1, 120)
|
|
96
117
|
.string('callbackInfo', data.callbackInfo)
|
|
97
118
|
.max('callbackInfo', typeof data.callbackInfo === 'string' ? queryEscape(data.callbackInfo) : undefined, 512)
|
|
98
119
|
.validate();
|
|
@@ -102,6 +123,8 @@ export class KbzPay {
|
|
|
102
123
|
*
|
|
103
124
|
* The redirect only works on a phone with the KBZ Pay app installed, and its Referer must match
|
|
104
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.
|
|
105
128
|
*/
|
|
106
129
|
async pwa(data, options = {}) {
|
|
107
130
|
const order = await this.precreate(data, 'PWAAPP', options);
|
|
@@ -113,7 +136,11 @@ export class KbzPay {
|
|
|
113
136
|
raw: toPlainObject(order.response),
|
|
114
137
|
});
|
|
115
138
|
}
|
|
116
|
-
/**
|
|
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
|
+
*/
|
|
117
144
|
async qr(data, options = {}) {
|
|
118
145
|
const order = await this.precreate(data, 'PAY_BY_QRCODE', options);
|
|
119
146
|
const qrCode = get(order.response, 'qrCode');
|
|
@@ -125,9 +152,7 @@ export class KbzPay {
|
|
|
125
152
|
return new QrPayment({
|
|
126
153
|
orderId: data.orderId,
|
|
127
154
|
qrString: qrCode,
|
|
128
|
-
expiresAt:
|
|
129
|
-
? undefined
|
|
130
|
-
: new Date(Date.now() + data.timeoutMinutes * 60_000),
|
|
155
|
+
expiresAt: new Date(Date.now() + order.timeToLiveMinutes * 60_000),
|
|
131
156
|
reference: order.prepayId,
|
|
132
157
|
raw: toPlainObject(order.response),
|
|
133
158
|
});
|
|
@@ -135,6 +160,8 @@ export class KbzPay {
|
|
|
135
160
|
/**
|
|
136
161
|
* Creates an order and returns the signed values your mobile app passes to `KBZPay.startPay()`.
|
|
137
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.
|
|
138
165
|
*/
|
|
139
166
|
async app(data, options = {}) {
|
|
140
167
|
const order = await this.precreate(data, 'APP', options);
|
|
@@ -187,6 +214,12 @@ export class KbzPay {
|
|
|
187
214
|
}
|
|
188
215
|
async precreate(data, tradeType, options) {
|
|
189
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
|
+
}
|
|
190
223
|
const biz = {
|
|
191
224
|
appid: this.config.appId,
|
|
192
225
|
merch_code: this.config.merchantCode,
|
|
@@ -198,9 +231,7 @@ export class KbzPay {
|
|
|
198
231
|
if (data.title) {
|
|
199
232
|
biz.title = data.title;
|
|
200
233
|
}
|
|
201
|
-
|
|
202
|
-
biz.timeout_express = `${data.timeoutMinutes}m`;
|
|
203
|
-
}
|
|
234
|
+
biz.timeout_express = `${timeToLiveMinutes}m`;
|
|
204
235
|
if (data.callbackInfo) {
|
|
205
236
|
biz.callback_info = queryEscape(data.callbackInfo);
|
|
206
237
|
}
|
|
@@ -211,7 +242,7 @@ export class KbzPay {
|
|
|
211
242
|
if (prepayId === '') {
|
|
212
243
|
throw new ApiError('KBZ Pay did not return a prepay_id.', { raw: toPlainObject(response) });
|
|
213
244
|
}
|
|
214
|
-
return { prepayId, nonce, timestamp: time, response };
|
|
245
|
+
return { prepayId, nonce, timestamp: time, timeToLiveMinutes, response };
|
|
215
246
|
}
|
|
216
247
|
/** Sends a signed request and returns the `Response` object, throwing when KBZ reports an error. */
|
|
217
248
|
async call(endpoint, method, version, biz, extra, nonce, time, signal) {
|
|
@@ -10,6 +10,9 @@ export declare class KbzPaySigner {
|
|
|
10
10
|
signString(fields: Readonly<Record<string, unknown>>): string;
|
|
11
11
|
/** The uppercase SHA-256 signature of `fields`. */
|
|
12
12
|
sign(fields: Readonly<Record<string, unknown>>): string;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
|
|
15
|
+
* fields holding an object or array fail rather than being partly trusted.
|
|
16
|
+
*/
|
|
14
17
|
verify(fields: Readonly<Record<string, unknown>>): boolean;
|
|
15
18
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { safeEqual, sha256Hex } from '../core/crypto.js';
|
|
2
|
-
import { scalarString } from '../core/values.js';
|
|
2
|
+
import { isNested, scalarString } from '../core/values.js';
|
|
3
3
|
/**
|
|
4
4
|
* KBZ Pay's signature: every non-empty scalar field except `sign` and `sign_type`, sorted by key,
|
|
5
5
|
* joined as raw `key=value` pairs (not URL-encoded), with `&key=<app key>` appended, hashed with
|
|
@@ -24,9 +24,14 @@ export class KbzPaySigner {
|
|
|
24
24
|
sign(fields) {
|
|
25
25
|
return sha256Hex(`${this.signString(fields)}&key=${this.appKey}`).toUpperCase();
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Whether `fields.sign` matches, compared in constant time. Nested values are never signed, so
|
|
29
|
+
* fields holding an object or array fail rather than being partly trusted.
|
|
30
|
+
*/
|
|
28
31
|
verify(fields) {
|
|
29
32
|
const sign = fields.sign;
|
|
30
|
-
return typeof sign === 'string' &&
|
|
33
|
+
return (typeof sign === 'string' &&
|
|
34
|
+
!Object.values(fields).some(isNested) &&
|
|
35
|
+
safeEqual(this.sign(fields), sign.toUpperCase()));
|
|
31
36
|
}
|
|
32
37
|
}
|