@laranex/myanmar-payments 0.0.0-stage → 4.0.0-dev.0
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 +21 -0
- package/LICENSE.md +21 -0
- package/README.md +10 -2
- package/dist/cjs/aya-pay/aya-pay.d.ts +122 -0
- package/dist/cjs/aya-pay/aya-pay.js +301 -0
- package/dist/cjs/aya-pay/index.d.ts +1 -0
- package/dist/cjs/aya-pay/index.js +8 -0
- package/dist/cjs/core/amount.d.ts +57 -0
- package/dist/cjs/core/amount.js +107 -0
- package/dist/cjs/core/cache.d.ts +26 -0
- package/dist/cjs/core/cache.js +35 -0
- package/dist/cjs/core/callback.d.ts +180 -0
- package/dist/cjs/core/callback.js +304 -0
- package/dist/cjs/core/crypto.d.ts +19 -0
- package/dist/cjs/core/crypto.js +50 -0
- package/dist/cjs/core/env.d.ts +21 -0
- package/dist/cjs/core/env.js +54 -0
- package/dist/cjs/core/errors.d.ts +56 -0
- package/dist/cjs/core/errors.js +78 -0
- package/dist/cjs/core/http.d.ts +81 -0
- package/dist/cjs/core/http.js +128 -0
- package/dist/cjs/core/index.d.ts +8 -0
- package/dist/cjs/core/index.js +30 -0
- package/dist/cjs/core/json.d.ts +44 -0
- package/dist/cjs/core/json.js +254 -0
- package/dist/cjs/core/results.d.ts +122 -0
- package/dist/cjs/core/results.js +147 -0
- package/dist/cjs/core/status.d.ts +50 -0
- package/dist/cjs/core/status.js +60 -0
- package/dist/cjs/core/validate.d.ts +46 -0
- package/dist/cjs/core/validate.js +148 -0
- package/dist/cjs/core/values.d.ts +16 -0
- package/dist/cjs/core/values.js +45 -0
- package/dist/cjs/cyber-source/cyber-source.d.ts +95 -0
- package/dist/cjs/cyber-source/cyber-source.js +188 -0
- package/dist/cjs/cyber-source/index.d.ts +1 -0
- package/dist/cjs/cyber-source/index.js +7 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/kbz-pay/index.d.ts +2 -0
- package/dist/cjs/kbz-pay/index.js +8 -0
- package/dist/cjs/kbz-pay/kbz-pay.d.ts +102 -0
- package/dist/cjs/kbz-pay/kbz-pay.js +266 -0
- package/dist/cjs/kbz-pay/signer.d.ts +15 -0
- package/dist/cjs/kbz-pay/signer.js +36 -0
- package/dist/cjs/myanmar-payments.d.ts +50 -0
- package/dist/cjs/myanmar-payments.js +73 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/wave-money/index.d.ts +1 -0
- package/dist/cjs/wave-money/index.js +6 -0
- package/dist/cjs/wave-money/wave-money.d.ts +111 -0
- package/dist/cjs/wave-money/wave-money.js +251 -0
- package/dist/cjs/yoma-mmqr/index.d.ts +1 -0
- package/dist/cjs/yoma-mmqr/index.js +6 -0
- package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +105 -0
- package/dist/cjs/yoma-mmqr/yoma-mmqr.js +233 -0
- package/dist/esm/aya-pay/aya-pay.d.ts +122 -0
- package/dist/esm/aya-pay/aya-pay.js +296 -0
- package/dist/esm/aya-pay/index.d.ts +1 -0
- package/dist/esm/aya-pay/index.js +1 -0
- package/dist/esm/core/amount.d.ts +57 -0
- package/dist/esm/core/amount.js +103 -0
- package/dist/esm/core/cache.d.ts +26 -0
- package/dist/esm/core/cache.js +31 -0
- package/dist/esm/core/callback.d.ts +180 -0
- package/dist/esm/core/callback.js +294 -0
- package/dist/esm/core/crypto.d.ts +19 -0
- package/dist/esm/core/crypto.js +41 -0
- package/dist/esm/core/env.d.ts +21 -0
- package/dist/esm/core/env.js +45 -0
- package/dist/esm/core/errors.d.ts +56 -0
- package/dist/esm/core/errors.js +70 -0
- package/dist/esm/core/http.d.ts +81 -0
- package/dist/esm/core/http.js +121 -0
- package/dist/esm/core/index.d.ts +8 -0
- package/dist/esm/core/index.js +7 -0
- package/dist/esm/core/json.d.ts +44 -0
- package/dist/esm/core/json.js +244 -0
- package/dist/esm/core/results.d.ts +122 -0
- package/dist/esm/core/results.js +139 -0
- package/dist/esm/core/status.d.ts +50 -0
- package/dist/esm/core/status.js +56 -0
- package/dist/esm/core/validate.d.ts +46 -0
- package/dist/esm/core/validate.js +142 -0
- package/dist/esm/core/values.d.ts +16 -0
- package/dist/esm/core/values.js +38 -0
- package/dist/esm/cyber-source/cyber-source.d.ts +95 -0
- package/dist/esm/cyber-source/cyber-source.js +184 -0
- package/dist/esm/cyber-source/index.d.ts +1 -0
- package/dist/esm/cyber-source/index.js +1 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/kbz-pay/index.d.ts +2 -0
- package/dist/esm/kbz-pay/index.js +2 -0
- package/dist/esm/kbz-pay/kbz-pay.d.ts +102 -0
- package/dist/esm/kbz-pay/kbz-pay.js +262 -0
- package/dist/esm/kbz-pay/signer.d.ts +15 -0
- package/dist/esm/kbz-pay/signer.js +32 -0
- package/dist/esm/myanmar-payments.d.ts +50 -0
- package/dist/esm/myanmar-payments.js +69 -0
- package/dist/esm/wave-money/index.d.ts +1 -0
- package/dist/esm/wave-money/index.js +1 -0
- package/dist/esm/wave-money/wave-money.d.ts +111 -0
- package/dist/esm/wave-money/wave-money.js +246 -0
- package/dist/esm/yoma-mmqr/index.d.ts +1 -0
- package/dist/esm/yoma-mmqr/index.js +1 -0
- package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +105 -0
- package/dist/esm/yoma-mmqr/yoma-mmqr.js +229 -0
- package/package.json +151 -4
- package/skills/node-myanmar-payments/SKILL.md +109 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type AmountInput } from '../core/amount.js';
|
|
2
|
+
import { CallbackRequest, PaymentCallback, PaymentStatusResult } from '../core/callback.js';
|
|
3
|
+
import { type EnvSource } from '../core/env.js';
|
|
4
|
+
import { type GatewayOptions, type RequestOptions } from '../core/http.js';
|
|
5
|
+
import { FormPayment } from '../core/results.js';
|
|
6
|
+
/** The settings of {@link AyaPayConfig}. */
|
|
7
|
+
export interface AyaPayConfigOptions {
|
|
8
|
+
/** The public application key, sent with every request. */
|
|
9
|
+
appKey: string;
|
|
10
|
+
/** The secret that signs requests and verifies callbacks. */
|
|
11
|
+
appSecret: string;
|
|
12
|
+
/** Use the UAT environment (default `true`). */
|
|
13
|
+
sandbox?: boolean | undefined;
|
|
14
|
+
/** Overrides the gateway base URL. */
|
|
15
|
+
baseUrl?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* AYA Payment Gateway (APG) credentials and endpoints. A missing credential throws a
|
|
19
|
+
* `ConfigurationError`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class AyaPayConfig {
|
|
22
|
+
static readonly SANDBOX_URL = "https://uat-pgw.ayainnovation.com";
|
|
23
|
+
static readonly PRODUCTION_URL = "https://pgw.ayainnovation.com";
|
|
24
|
+
readonly appKey: string;
|
|
25
|
+
readonly appSecret: string;
|
|
26
|
+
readonly sandbox: boolean;
|
|
27
|
+
/** The base URL in use. */
|
|
28
|
+
readonly baseUrl: string;
|
|
29
|
+
constructor(options: AyaPayConfigOptions);
|
|
30
|
+
/**
|
|
31
|
+
* Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `AYA_PAY_SANDBOX` and `AYA_PAY_BASE_URL`, falling
|
|
32
|
+
* back to the `AYA_PGW_*` names.
|
|
33
|
+
*/
|
|
34
|
+
static fromEnv(env?: EnvSource): AyaPayConfig;
|
|
35
|
+
}
|
|
36
|
+
/** How the customer pays through the chosen channel. `services()` lists the methods each channel supports. */
|
|
37
|
+
export type AyaPayMethod = 'WEB' | 'QR' | 'NOTI';
|
|
38
|
+
/** The AYA payment methods. */
|
|
39
|
+
export declare const AyaPayMethod: Readonly<{
|
|
40
|
+
/** Pay on a hosted web page (cards, web checkout). */
|
|
41
|
+
readonly Web: "WEB";
|
|
42
|
+
/** Scan a QR with the wallet app. */
|
|
43
|
+
readonly Qr: "QR";
|
|
44
|
+
/** Approve a push notification in the wallet app. */
|
|
45
|
+
readonly Noti: "NOTI";
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* An AYA Payment Gateway order. AYA only accepts MMK (currency code 104) and documents no decimals,
|
|
49
|
+
* so amounts are whole kyat.
|
|
50
|
+
*/
|
|
51
|
+
export interface AyaPayPaymentData {
|
|
52
|
+
/** Your unique order id (`merchOrderId`), 6 to 40 characters. */
|
|
53
|
+
orderId: string;
|
|
54
|
+
/** The amount in whole kyat, e.g. `Amount.kyat(1000)` or `1000`. */
|
|
55
|
+
amount: AmountInput;
|
|
56
|
+
/** The channel key from `services()`, e.g. `aya_pay`, `kbz_pay`, `visa`. */
|
|
57
|
+
channel: string;
|
|
58
|
+
/** How the customer pays through that channel. */
|
|
59
|
+
method: AyaPayMethod;
|
|
60
|
+
/** Where AYA sends the customer afterwards. Defaults to the URL registered with AYA. */
|
|
61
|
+
returnUrl?: string | undefined;
|
|
62
|
+
/** Shown to the customer. */
|
|
63
|
+
description?: string | undefined;
|
|
64
|
+
/** Up to five of your own reference values, echoed back in the callback. */
|
|
65
|
+
userRefs?: readonly string[] | undefined;
|
|
66
|
+
}
|
|
67
|
+
/** A payment channel enabled for your merchant account. */
|
|
68
|
+
export declare class AyaPayService {
|
|
69
|
+
/** The display name, e.g. `AYA Pay`. */
|
|
70
|
+
readonly name: string;
|
|
71
|
+
/** The `channel` value to send when initiating, e.g. `aya_pay` or `visa`. */
|
|
72
|
+
readonly key: string;
|
|
73
|
+
/** The channel's logo. */
|
|
74
|
+
readonly imageUrl: string | undefined;
|
|
75
|
+
/** The methods this channel supports. */
|
|
76
|
+
readonly methods: readonly AyaPayMethod[];
|
|
77
|
+
/** Methods the gateway listed that this package does not know yet. */
|
|
78
|
+
readonly unknownMethods: readonly string[];
|
|
79
|
+
constructor(init: {
|
|
80
|
+
name: string;
|
|
81
|
+
key: string;
|
|
82
|
+
imageUrl?: string | undefined;
|
|
83
|
+
methods: readonly AyaPayMethod[];
|
|
84
|
+
unknownMethods?: readonly string[];
|
|
85
|
+
});
|
|
86
|
+
/** Whether the channel supports `method`. */
|
|
87
|
+
supports(method: AyaPayMethod): boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The AYA Payment Gateway (APG): one hosted checkout for AYA Pay, other wallets and cards, with
|
|
91
|
+
* channel listing, status enquiry and verified callbacks.
|
|
92
|
+
*/
|
|
93
|
+
export declare class AyaPay {
|
|
94
|
+
/** The configuration in use. */
|
|
95
|
+
readonly config: AyaPayConfig;
|
|
96
|
+
private readonly transport;
|
|
97
|
+
constructor(config: AyaPayConfig | AyaPayConfigOptions, options?: GatewayOptions);
|
|
98
|
+
/** A gateway configured from the `AYA_PAY_*` (or `AYA_PGW_*`) environment variables. */
|
|
99
|
+
static fromEnv(env?: EnvSource, options?: GatewayOptions): AyaPay;
|
|
100
|
+
/** Checks the order against AYA's documented rules; throws an `InvalidPaymentDataError`. */
|
|
101
|
+
static validate(data: AyaPayPaymentData): void;
|
|
102
|
+
/** Lists the payment channels enabled for your merchant account. */
|
|
103
|
+
services(options?: RequestOptions): Promise<AyaPayService[]>;
|
|
104
|
+
/**
|
|
105
|
+
* Signs the order. The customer's browser must POST the returned form to AYA's hosted checkout;
|
|
106
|
+
* `toHtml()` renders a page that does it. Makes no network call.
|
|
107
|
+
*/
|
|
108
|
+
initiate(data: AyaPayPaymentData): FormPayment;
|
|
109
|
+
/** Asks AYA for the current state of an order (enquiry). */
|
|
110
|
+
status(orderId: string, options?: RequestOptions): Promise<PaymentStatusResult>;
|
|
111
|
+
/** Verifies AYA's backend callback. */
|
|
112
|
+
handleCallback(request: CallbackRequest): PaymentCallback;
|
|
113
|
+
/**
|
|
114
|
+
* Verifies the signed query string AYA adds when it sends the customer back to your return URL.
|
|
115
|
+
* Use it to show the right page; fulfill orders from the backend callback.
|
|
116
|
+
*/
|
|
117
|
+
verifyRedirect(request: CallbackRequest): PaymentCallback;
|
|
118
|
+
/** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
|
|
119
|
+
private verifiedPayload;
|
|
120
|
+
private checksum;
|
|
121
|
+
private post;
|
|
122
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {} from '../core/amount.js';
|
|
2
|
+
import { CallbackRequest, losslessInput, losslessQueryInput, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
|
|
3
|
+
import { decodeBase64, hmacSha256Hex, safeEqual } from '../core/crypto.js';
|
|
4
|
+
import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
|
|
5
|
+
import { ApiError, SignatureVerificationError } from '../core/errors.js';
|
|
6
|
+
import { httpClientFrom, Transport, } from '../core/http.js';
|
|
7
|
+
import { isLosslessObject, parseJsonObject, toPlainObject, } from '../core/json.js';
|
|
8
|
+
import { FormPayment } from '../core/results.js';
|
|
9
|
+
import { resolveStatus } from '../core/status.js';
|
|
10
|
+
import { toAmount, Validator } from '../core/validate.js';
|
|
11
|
+
import { get, object, optional, scalarString, trimmed } from '../core/values.js';
|
|
12
|
+
/**
|
|
13
|
+
* AYA Payment Gateway (APG) credentials and endpoints. A missing credential throws a
|
|
14
|
+
* `ConfigurationError`.
|
|
15
|
+
*/
|
|
16
|
+
export class AyaPayConfig {
|
|
17
|
+
static SANDBOX_URL = 'https://uat-pgw.ayainnovation.com';
|
|
18
|
+
static PRODUCTION_URL = 'https://pgw.ayainnovation.com';
|
|
19
|
+
appKey;
|
|
20
|
+
appSecret;
|
|
21
|
+
sandbox;
|
|
22
|
+
/** The base URL in use. */
|
|
23
|
+
baseUrl;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.appKey = requireSetting('aya_pay', 'app_key', options.appKey);
|
|
26
|
+
this.appSecret = requireSetting('aya_pay', 'app_secret', options.appSecret);
|
|
27
|
+
this.sandbox = options.sandbox ?? true;
|
|
28
|
+
this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
|
|
29
|
+
(this.sandbox ? AyaPayConfig.SANDBOX_URL : AyaPayConfig.PRODUCTION_URL));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `AYA_PAY_SANDBOX` and `AYA_PAY_BASE_URL`, falling
|
|
33
|
+
* back to the `AYA_PGW_*` names.
|
|
34
|
+
*/
|
|
35
|
+
static fromEnv(env = defaultEnv()) {
|
|
36
|
+
return new AyaPayConfig({
|
|
37
|
+
appKey: envFirst(env, 'AYA_PAY_APP_KEY', 'AYA_PGW_APP_KEY'),
|
|
38
|
+
appSecret: envFirst(env, 'AYA_PAY_APP_SECRET', 'AYA_PGW_APP_SECRET'),
|
|
39
|
+
sandbox: envSandbox(env, 'AYA_PAY_SANDBOX'),
|
|
40
|
+
baseUrl: envFirst(env, 'AYA_PAY_BASE_URL', 'AYA_PGW_BASE_URL'),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The AYA payment methods. */
|
|
45
|
+
export const AyaPayMethod = Object.freeze({
|
|
46
|
+
/** Pay on a hosted web page (cards, web checkout). */
|
|
47
|
+
Web: 'WEB',
|
|
48
|
+
/** Scan a QR with the wallet app. */
|
|
49
|
+
Qr: 'QR',
|
|
50
|
+
/** Approve a push notification in the wallet app. */
|
|
51
|
+
Noti: 'NOTI',
|
|
52
|
+
});
|
|
53
|
+
const METHODS = ['WEB', 'QR', 'NOTI'];
|
|
54
|
+
/** A payment channel enabled for your merchant account. */
|
|
55
|
+
export class AyaPayService {
|
|
56
|
+
/** The display name, e.g. `AYA Pay`. */
|
|
57
|
+
name;
|
|
58
|
+
/** The `channel` value to send when initiating, e.g. `aya_pay` or `visa`. */
|
|
59
|
+
key;
|
|
60
|
+
/** The channel's logo. */
|
|
61
|
+
imageUrl;
|
|
62
|
+
/** The methods this channel supports. */
|
|
63
|
+
methods;
|
|
64
|
+
/** Methods the gateway listed that this package does not know yet. */
|
|
65
|
+
unknownMethods;
|
|
66
|
+
constructor(init) {
|
|
67
|
+
this.name = init.name;
|
|
68
|
+
this.key = init.key;
|
|
69
|
+
this.imageUrl = init.imageUrl;
|
|
70
|
+
this.methods = Object.freeze([...init.methods]);
|
|
71
|
+
this.unknownMethods = Object.freeze([...(init.unknownMethods ?? [])]);
|
|
72
|
+
}
|
|
73
|
+
/** Whether the channel supports `method`. */
|
|
74
|
+
supports(method) {
|
|
75
|
+
return this.methods.includes(method);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const CURRENCY_MMK = '104';
|
|
79
|
+
const STATUSES = {
|
|
80
|
+
'00': 'successful',
|
|
81
|
+
'01': 'pending',
|
|
82
|
+
'02': 'failed',
|
|
83
|
+
'03': 'failed',
|
|
84
|
+
'04': 'expired',
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The documented field order of the decoded callback / enquiry payload. AYA leaves out fields that
|
|
88
|
+
* do not apply (e.g. card fields for wallet payments) and signs only the ones present. The payload
|
|
89
|
+
* spells `currencyCode` as `currenyCode`.
|
|
90
|
+
*/
|
|
91
|
+
const PAYLOAD_FIELDS = [
|
|
92
|
+
'merchOrderId',
|
|
93
|
+
'tranId',
|
|
94
|
+
'amount',
|
|
95
|
+
'currencyCode',
|
|
96
|
+
'statusCode',
|
|
97
|
+
'paymentCardNumber',
|
|
98
|
+
'paymentMobileNumber',
|
|
99
|
+
'cardTypeName',
|
|
100
|
+
'cardExpiryDate',
|
|
101
|
+
'nameOnCard',
|
|
102
|
+
'approvalCode',
|
|
103
|
+
'tranRef',
|
|
104
|
+
'userRef1',
|
|
105
|
+
'userRef2',
|
|
106
|
+
'userRef3',
|
|
107
|
+
'userRef4',
|
|
108
|
+
'userRef5',
|
|
109
|
+
'description',
|
|
110
|
+
'dateTime',
|
|
111
|
+
];
|
|
112
|
+
/**
|
|
113
|
+
* The AYA Payment Gateway (APG): one hosted checkout for AYA Pay, other wallets and cards, with
|
|
114
|
+
* channel listing, status enquiry and verified callbacks.
|
|
115
|
+
*/
|
|
116
|
+
export class AyaPay {
|
|
117
|
+
/** The configuration in use. */
|
|
118
|
+
config;
|
|
119
|
+
transport;
|
|
120
|
+
constructor(config, options = {}) {
|
|
121
|
+
this.config = config instanceof AyaPayConfig ? config : new AyaPayConfig(config);
|
|
122
|
+
this.transport = new Transport(httpClientFrom(options));
|
|
123
|
+
}
|
|
124
|
+
/** A gateway configured from the `AYA_PAY_*` (or `AYA_PGW_*`) environment variables. */
|
|
125
|
+
static fromEnv(env = defaultEnv(), options = {}) {
|
|
126
|
+
return new AyaPay(AyaPayConfig.fromEnv(env), options);
|
|
127
|
+
}
|
|
128
|
+
/** Checks the order against AYA's documented rules; throws an `InvalidPaymentDataError`. */
|
|
129
|
+
static validate(data) {
|
|
130
|
+
const userRefs = data.userRefs;
|
|
131
|
+
new Validator()
|
|
132
|
+
.required('orderId', data.orderId)
|
|
133
|
+
.length('orderId', data.orderId, 6, 40)
|
|
134
|
+
.amount('amount', data.amount, { gateway: 'AYA Payment Gateway', maxDecimals: 0 })
|
|
135
|
+
.required('channel', data.channel)
|
|
136
|
+
.when(!METHODS.includes(data.method), 'method', 'The method field must be one of WEB, QR or NOTI.')
|
|
137
|
+
.string('returnUrl', data.returnUrl)
|
|
138
|
+
.url('returnUrl', data.returnUrl)
|
|
139
|
+
.string('description', data.description)
|
|
140
|
+
.when(userRefs !== undefined &&
|
|
141
|
+
(!Array.isArray(userRefs) || userRefs.some((ref) => typeof ref !== 'string')), 'userRefs', 'The userRefs field must be a list of strings.')
|
|
142
|
+
.when(Array.isArray(userRefs) && userRefs.length > 5, 'userRefs', 'The userRefs field must not have more than 5 items.')
|
|
143
|
+
.validate();
|
|
144
|
+
}
|
|
145
|
+
/** Lists the payment channels enabled for your merchant account. */
|
|
146
|
+
async services(options = {}) {
|
|
147
|
+
const timestamp = unixTime();
|
|
148
|
+
const body = await this.post('services', {
|
|
149
|
+
appKey: this.config.appKey,
|
|
150
|
+
timestamp,
|
|
151
|
+
checkSum: this.checksum([this.config.appKey, this.config.appSecret, String(timestamp)]),
|
|
152
|
+
}, options.signal);
|
|
153
|
+
const list = Array.isArray(body.data) ? body.data : [];
|
|
154
|
+
const services = [];
|
|
155
|
+
for (const entry of list) {
|
|
156
|
+
if (!isLosslessObject(entry) || get(entry, 'key') === '') {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const methods = [];
|
|
160
|
+
const unknownMethods = [];
|
|
161
|
+
for (const method of Array.isArray(entry.methods) ? entry.methods : []) {
|
|
162
|
+
const name = scalarString(method) ?? '';
|
|
163
|
+
if (METHODS.includes(name)) {
|
|
164
|
+
methods.push(name);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
unknownMethods.push(name);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
services.push(new AyaPayService({
|
|
171
|
+
name: get(entry, 'name') || get(entry, 'key'),
|
|
172
|
+
key: get(entry, 'key'),
|
|
173
|
+
imageUrl: optional(entry, 'image_url'),
|
|
174
|
+
methods,
|
|
175
|
+
unknownMethods,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
return services;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Signs the order. The customer's browser must POST the returned form to AYA's hosted checkout;
|
|
182
|
+
* `toHtml()` renders a page that does it. Makes no network call.
|
|
183
|
+
*/
|
|
184
|
+
initiate(data) {
|
|
185
|
+
AyaPay.validate(data);
|
|
186
|
+
const refs = [...(data.userRefs ?? []), '', '', '', '', ''].slice(0, 5);
|
|
187
|
+
const fields = [
|
|
188
|
+
{ name: 'merchOrderId', value: data.orderId },
|
|
189
|
+
{ name: 'amount', value: String(toAmount(data.amount)) },
|
|
190
|
+
{ name: 'appKey', value: this.config.appKey },
|
|
191
|
+
{ name: 'timestamp', value: String(unixTime()) },
|
|
192
|
+
{ name: 'userRef1', value: refs[0] },
|
|
193
|
+
{ name: 'userRef2', value: refs[1] },
|
|
194
|
+
{ name: 'userRef3', value: refs[2] },
|
|
195
|
+
{ name: 'userRef4', value: refs[3] },
|
|
196
|
+
{ name: 'userRef5', value: refs[4] },
|
|
197
|
+
{ name: 'description', value: data.description ?? '' },
|
|
198
|
+
{ name: 'currencyCode', value: CURRENCY_MMK },
|
|
199
|
+
{ name: 'channel', value: data.channel },
|
|
200
|
+
{ name: 'method', value: data.method },
|
|
201
|
+
{ name: 'overrideFrontendRedirectUrl', value: data.returnUrl ?? '' },
|
|
202
|
+
];
|
|
203
|
+
fields.push({ name: 'checkSum', value: this.checksum(fields.map((field) => field.value)) });
|
|
204
|
+
return new FormPayment({
|
|
205
|
+
orderId: data.orderId,
|
|
206
|
+
action: `${this.config.baseUrl}/v1/payment/request`,
|
|
207
|
+
fields,
|
|
208
|
+
enctype: 'multipart/form-data',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** Asks AYA for the current state of an order (enquiry). */
|
|
212
|
+
async status(orderId, options = {}) {
|
|
213
|
+
const timestamp = unixTime();
|
|
214
|
+
const body = await this.post('enquiry', {
|
|
215
|
+
merchOrderId: orderId,
|
|
216
|
+
appKey: this.config.appKey,
|
|
217
|
+
timestamp,
|
|
218
|
+
checkSum: this.checksum([orderId, String(timestamp), this.config.appKey]),
|
|
219
|
+
}, options.signal);
|
|
220
|
+
const payload = this.verifiedPayload(object(body, 'data') ?? {}, 'enquiry response');
|
|
221
|
+
const statusCode = trimmed(payload, 'statusCode');
|
|
222
|
+
return new PaymentStatusResult({
|
|
223
|
+
orderId: optional(payload, 'merchOrderId') || orderId,
|
|
224
|
+
status: resolveStatus(STATUSES, statusCode),
|
|
225
|
+
gatewayStatus: statusCode,
|
|
226
|
+
gatewayReference: optional(payload, 'tranId'),
|
|
227
|
+
amount: optional(payload, 'amount'),
|
|
228
|
+
raw: toPlainObject(payload),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** Verifies AYA's backend callback. */
|
|
232
|
+
handleCallback(request) {
|
|
233
|
+
return toCallback(this.verifiedPayload(losslessInput(request), 'callback'));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Verifies the signed query string AYA adds when it sends the customer back to your return URL.
|
|
237
|
+
* Use it to show the right page; fulfill orders from the backend callback.
|
|
238
|
+
*/
|
|
239
|
+
verifyRedirect(request) {
|
|
240
|
+
return toCallback(this.verifiedPayload(losslessQueryInput(request), 'redirect'));
|
|
241
|
+
}
|
|
242
|
+
/** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
|
|
243
|
+
verifiedPayload(input, context) {
|
|
244
|
+
const fail = () => new SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, toPlainObject(input));
|
|
245
|
+
const json = decodeBase64(get(input, 'payload'));
|
|
246
|
+
const payload = json === undefined ? undefined : parseJsonObject(json);
|
|
247
|
+
if (payload === undefined) {
|
|
248
|
+
throw fail();
|
|
249
|
+
}
|
|
250
|
+
const parts = [];
|
|
251
|
+
for (const field of PAYLOAD_FIELDS) {
|
|
252
|
+
const key = field === 'currencyCode' && 'currenyCode' in payload ? 'currenyCode' : field;
|
|
253
|
+
if (Object.prototype.hasOwnProperty.call(payload, key)) {
|
|
254
|
+
parts.push(scalarString(payload[key]) ?? '');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!safeEqual(this.checksum(parts), get(input, 'checkSum').toLowerCase())) {
|
|
258
|
+
throw fail();
|
|
259
|
+
}
|
|
260
|
+
return payload;
|
|
261
|
+
}
|
|
262
|
+
checksum(parts) {
|
|
263
|
+
return hmacSha256Hex(this.config.appSecret, parts.join(':'));
|
|
264
|
+
}
|
|
265
|
+
async post(endpoint, data, signal) {
|
|
266
|
+
const response = await this.transport.postJson(`${this.config.baseUrl}/v1/payment/${endpoint}`, data, {}, signal);
|
|
267
|
+
const body = response.json();
|
|
268
|
+
const status = get(body, 'status');
|
|
269
|
+
if (!response.successful() || status !== '00') {
|
|
270
|
+
const message = optional(body, 'message');
|
|
271
|
+
throw new ApiError(status
|
|
272
|
+
? `AYA Pay ${endpoint} failed: [${status}] ${message ?? ''}`.trimEnd()
|
|
273
|
+
: `AYA Pay ${endpoint} failed with HTTP ${response.status}.`, {
|
|
274
|
+
gatewayCode: status || undefined,
|
|
275
|
+
gatewayMessage: message,
|
|
276
|
+
httpStatus: response.status,
|
|
277
|
+
raw: toPlainObject(body),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return body;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function toCallback(payload) {
|
|
284
|
+
const statusCode = trimmed(payload, 'statusCode');
|
|
285
|
+
return new PaymentCallback({
|
|
286
|
+
orderId: get(payload, 'merchOrderId'),
|
|
287
|
+
status: resolveStatus(STATUSES, statusCode),
|
|
288
|
+
gatewayStatus: statusCode,
|
|
289
|
+
gatewayReference: optional(payload, 'tranId'),
|
|
290
|
+
amount: optional(payload, 'amount'),
|
|
291
|
+
raw: toPlainObject(payload),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function unixTime() {
|
|
295
|
+
return Math.floor(Date.now() / 1000);
|
|
296
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AyaPay, AyaPayConfig, AyaPayMethod, AyaPayService, type AyaPayConfigOptions, type AyaPayPaymentData, } from './aya-pay.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AyaPay, AyaPayConfig, AyaPayMethod, AyaPayService, } from './aya-pay.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An exact, non-negative money amount, kept as decimal text so it is never rounded through a
|
|
3
|
+
* floating-point number. Build one with {@link Amount.kyat} for whole amounts or
|
|
4
|
+
* {@link Amount.parse} for decimal ones; each gateway then checks it against that gateway's
|
|
5
|
+
* documented rules (Wave Money, AYA and Yoma MMQR only accept whole kyat, KBZ Pay up to 2 decimal
|
|
6
|
+
* places, CyberSource any).
|
|
7
|
+
*
|
|
8
|
+
* Amounts are immutable. `JSON.stringify` writes them as strings, e.g. `"1000.50"`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Amount {
|
|
11
|
+
private readonly value;
|
|
12
|
+
private constructor();
|
|
13
|
+
/**
|
|
14
|
+
* A whole amount, e.g. `Amount.kyat(1000)` for 1000 MMK. Works for whole units of any currency.
|
|
15
|
+
*
|
|
16
|
+
* Takes a safe integer `number` or a `bigint`. Fractions, negative values, `NaN` and integers
|
|
17
|
+
* beyond `Number.MAX_SAFE_INTEGER` (pass a `bigint` instead) throw an
|
|
18
|
+
* {@link InvalidPaymentDataError} for the `amount` field.
|
|
19
|
+
*/
|
|
20
|
+
static kyat(amount: number | bigint): Amount;
|
|
21
|
+
/**
|
|
22
|
+
* A decimal amount written as plain digits, e.g. `Amount.parse('1000.50')`. Signs, exponents,
|
|
23
|
+
* spaces and thousands separators are rejected with an {@link InvalidPaymentDataError}. Leading
|
|
24
|
+
* zeros of the whole part are removed; the fractional digits are kept exactly as given.
|
|
25
|
+
*/
|
|
26
|
+
static parse(amount: string): Amount;
|
|
27
|
+
/**
|
|
28
|
+
* Returns `amount` when it is already an `Amount`, or {@link Amount.kyat} of a whole number.
|
|
29
|
+
*/
|
|
30
|
+
static from(amount: Amount | number | bigint): Amount;
|
|
31
|
+
/** Whether `value` is an `Amount`. */
|
|
32
|
+
static isAmount(value: unknown): value is Amount;
|
|
33
|
+
/** The amount exactly as it is sent to the gateway, e.g. `"1000.50"`. */
|
|
34
|
+
toString(): string;
|
|
35
|
+
/** The amount as a JSON string, e.g. `"1000.50"`, so no consumer parses it as a float. */
|
|
36
|
+
toJSON(): string;
|
|
37
|
+
/** The number of fractional digits, e.g. `2` for `1000.50`. */
|
|
38
|
+
decimalPlaces(): number;
|
|
39
|
+
/** The digits before the decimal point, e.g. `"1000"` for `1000.50`. */
|
|
40
|
+
wholePart(): string;
|
|
41
|
+
/** Whether the amount equals zero, e.g. `0` or `0.00`. */
|
|
42
|
+
isZero(): boolean;
|
|
43
|
+
/** Whether the amount is greater than zero. */
|
|
44
|
+
isPositive(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether both amounts have the same value, ignoring trailing fractional zeros
|
|
47
|
+
* (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
|
|
48
|
+
* that was not sent never matches.
|
|
49
|
+
*/
|
|
50
|
+
equals(other: Amount | string | null | undefined): boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What payment data accepts as an amount: an {@link Amount} or a whole number of kyat
|
|
54
|
+
* (a safe integer `number` or a `bigint`). Decimal amounts must be an `Amount`, e.g.
|
|
55
|
+
* `Amount.parse('1000.50')`.
|
|
56
|
+
*/
|
|
57
|
+
export type AmountInput = Amount | number | bigint;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { InvalidPaymentDataError } from './errors.js';
|
|
2
|
+
const PATTERN = /^\d+(\.\d+)?$/;
|
|
3
|
+
/**
|
|
4
|
+
* An exact, non-negative money amount, kept as decimal text so it is never rounded through a
|
|
5
|
+
* floating-point number. Build one with {@link Amount.kyat} for whole amounts or
|
|
6
|
+
* {@link Amount.parse} for decimal ones; each gateway then checks it against that gateway's
|
|
7
|
+
* documented rules (Wave Money, AYA and Yoma MMQR only accept whole kyat, KBZ Pay up to 2 decimal
|
|
8
|
+
* places, CyberSource any).
|
|
9
|
+
*
|
|
10
|
+
* Amounts are immutable. `JSON.stringify` writes them as strings, e.g. `"1000.50"`.
|
|
11
|
+
*/
|
|
12
|
+
export class Amount {
|
|
13
|
+
value;
|
|
14
|
+
constructor(value) {
|
|
15
|
+
this.value = value;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A whole amount, e.g. `Amount.kyat(1000)` for 1000 MMK. Works for whole units of any currency.
|
|
19
|
+
*
|
|
20
|
+
* Takes a safe integer `number` or a `bigint`. Fractions, negative values, `NaN` and integers
|
|
21
|
+
* beyond `Number.MAX_SAFE_INTEGER` (pass a `bigint` instead) throw an
|
|
22
|
+
* {@link InvalidPaymentDataError} for the `amount` field.
|
|
23
|
+
*/
|
|
24
|
+
static kyat(amount) {
|
|
25
|
+
if (typeof amount === 'bigint') {
|
|
26
|
+
if (amount < 0n) {
|
|
27
|
+
throw invalid('The amount field must not be negative.');
|
|
28
|
+
}
|
|
29
|
+
return new Amount(amount.toString());
|
|
30
|
+
}
|
|
31
|
+
if (typeof amount !== 'number' || !Number.isSafeInteger(amount)) {
|
|
32
|
+
throw invalid(`The amount field must be a whole number of kyat; use Amount.parse('10.50') for decimal amounts, got ${String(amount)}.`);
|
|
33
|
+
}
|
|
34
|
+
if (amount < 0) {
|
|
35
|
+
throw invalid('The amount field must not be negative.');
|
|
36
|
+
}
|
|
37
|
+
return new Amount(String(amount));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A decimal amount written as plain digits, e.g. `Amount.parse('1000.50')`. Signs, exponents,
|
|
41
|
+
* spaces and thousands separators are rejected with an {@link InvalidPaymentDataError}. Leading
|
|
42
|
+
* zeros of the whole part are removed; the fractional digits are kept exactly as given.
|
|
43
|
+
*/
|
|
44
|
+
static parse(amount) {
|
|
45
|
+
if (typeof amount !== 'string' || !PATTERN.test(amount)) {
|
|
46
|
+
throw invalid(`The amount field must be a number such as 1000 or 1000.50, got ${JSON.stringify(amount)}.`);
|
|
47
|
+
}
|
|
48
|
+
const [whole = '', fraction] = amount.split('.');
|
|
49
|
+
const trimmedWhole = whole.replace(/^0+/, '') || '0';
|
|
50
|
+
return new Amount(fraction === undefined ? trimmedWhole : `${trimmedWhole}.${fraction}`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns `amount` when it is already an `Amount`, or {@link Amount.kyat} of a whole number.
|
|
54
|
+
*/
|
|
55
|
+
static from(amount) {
|
|
56
|
+
return amount instanceof Amount ? amount : Amount.kyat(amount);
|
|
57
|
+
}
|
|
58
|
+
/** Whether `value` is an `Amount`. */
|
|
59
|
+
static isAmount(value) {
|
|
60
|
+
return value instanceof Amount;
|
|
61
|
+
}
|
|
62
|
+
/** The amount exactly as it is sent to the gateway, e.g. `"1000.50"`. */
|
|
63
|
+
toString() {
|
|
64
|
+
return this.value;
|
|
65
|
+
}
|
|
66
|
+
/** The amount as a JSON string, e.g. `"1000.50"`, so no consumer parses it as a float. */
|
|
67
|
+
toJSON() {
|
|
68
|
+
return this.value;
|
|
69
|
+
}
|
|
70
|
+
/** The number of fractional digits, e.g. `2` for `1000.50`. */
|
|
71
|
+
decimalPlaces() {
|
|
72
|
+
const dot = this.value.indexOf('.');
|
|
73
|
+
return dot === -1 ? 0 : this.value.length - dot - 1;
|
|
74
|
+
}
|
|
75
|
+
/** The digits before the decimal point, e.g. `"1000"` for `1000.50`. */
|
|
76
|
+
wholePart() {
|
|
77
|
+
return this.value.split('.')[0];
|
|
78
|
+
}
|
|
79
|
+
/** Whether the amount equals zero, e.g. `0` or `0.00`. */
|
|
80
|
+
isZero() {
|
|
81
|
+
return /^[0.]+$/.test(this.value);
|
|
82
|
+
}
|
|
83
|
+
/** Whether the amount is greater than zero. */
|
|
84
|
+
isPositive() {
|
|
85
|
+
return !this.isZero();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether both amounts have the same value, ignoring trailing fractional zeros
|
|
89
|
+
* (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
|
|
90
|
+
* that was not sent never matches.
|
|
91
|
+
*/
|
|
92
|
+
equals(other) {
|
|
93
|
+
if (other === undefined || other === null) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const normalize = (value) => value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value;
|
|
97
|
+
const text = other instanceof Amount ? other.value : other;
|
|
98
|
+
return normalize(this.value) === normalize(text);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function invalid(message) {
|
|
102
|
+
return new InvalidPaymentDataError({ amount: message });
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
/**
|
|
3
|
+
* Stores access tokens between calls (used by Yoma MMQR). Implement it on top of Redis or similar
|
|
4
|
+
* to share tokens between processes; every method may return a promise.
|
|
5
|
+
*/
|
|
6
|
+
export interface TokenCache {
|
|
7
|
+
/** The cached value, or `undefined` when it is missing or expired. */
|
|
8
|
+
get(key: string): MaybePromise<string | undefined | null>;
|
|
9
|
+
/** Stores `value` for `ttlSeconds`; `0` or less never expires. */
|
|
10
|
+
set(key: string, value: string, ttlSeconds: number): MaybePromise<unknown>;
|
|
11
|
+
/** Removes `key`. */
|
|
12
|
+
delete(key: string): MaybePromise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The default {@link TokenCache}: a `Map` that lives as long as the process. Share one gateway (or
|
|
16
|
+
* one cache) across requests so the token is reused.
|
|
17
|
+
*/
|
|
18
|
+
export declare class MemoryTokenCache implements TokenCache {
|
|
19
|
+
private readonly items;
|
|
20
|
+
get(key: string): string | undefined;
|
|
21
|
+
set(key: string, value: string, ttlSeconds: number): void;
|
|
22
|
+
delete(key: string): void;
|
|
23
|
+
/** Removes every entry. */
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default {@link TokenCache}: a `Map` that lives as long as the process. Share one gateway (or
|
|
3
|
+
* one cache) across requests so the token is reused.
|
|
4
|
+
*/
|
|
5
|
+
export class MemoryTokenCache {
|
|
6
|
+
items = new Map();
|
|
7
|
+
get(key) {
|
|
8
|
+
const item = this.items.get(key);
|
|
9
|
+
if (item === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (item.expiresAt !== undefined && Date.now() >= item.expiresAt) {
|
|
13
|
+
this.items.delete(key);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return item.value;
|
|
17
|
+
}
|
|
18
|
+
set(key, value, ttlSeconds) {
|
|
19
|
+
this.items.set(key, {
|
|
20
|
+
value,
|
|
21
|
+
expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : undefined,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
delete(key) {
|
|
25
|
+
this.items.delete(key);
|
|
26
|
+
}
|
|
27
|
+
/** Removes every entry. */
|
|
28
|
+
clear() {
|
|
29
|
+
this.items.clear();
|
|
30
|
+
}
|
|
31
|
+
}
|