@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,229 @@
|
|
|
1
|
+
import {} from '../core/amount.js';
|
|
2
|
+
import { MemoryTokenCache } from '../core/cache.js';
|
|
3
|
+
import { CallbackRequest, losslessBody, PaymentCallback, PaymentStatusResult, } from '../core/callback.js';
|
|
4
|
+
import { hmacSha256Hex, safeEqual, sha256Hex } from '../core/crypto.js';
|
|
5
|
+
import { defaultEnv, envFirst, envSandbox, optionalSetting, requireSetting, trimUrl, } from '../core/env.js';
|
|
6
|
+
import { ApiError, SignatureVerificationError } from '../core/errors.js';
|
|
7
|
+
import { httpClientFrom, Transport, } from '../core/http.js';
|
|
8
|
+
import { toPlainObject } from '../core/json.js';
|
|
9
|
+
import { QrPayment } from '../core/results.js';
|
|
10
|
+
import { resolveStatus } from '../core/status.js';
|
|
11
|
+
import { toAmount, Validator } from '../core/validate.js';
|
|
12
|
+
import { get, optional, trimmed } from '../core/values.js';
|
|
13
|
+
/**
|
|
14
|
+
* Yoma MMQR credentials and endpoints. A missing credential throws a `ConfigurationError`.
|
|
15
|
+
*/
|
|
16
|
+
export class YomaMmqrConfig {
|
|
17
|
+
static SANDBOX_URL = 'https://devapi.yomabank.net';
|
|
18
|
+
static PRODUCTION_URL = 'https://paymenthubapi.yomabank.com';
|
|
19
|
+
static DEFAULT_API_VERSION = 'v1rc';
|
|
20
|
+
merchantId;
|
|
21
|
+
clientId;
|
|
22
|
+
clientSecret;
|
|
23
|
+
webhookHashKey;
|
|
24
|
+
webhookSecret;
|
|
25
|
+
sandbox;
|
|
26
|
+
/** The base URL in use. */
|
|
27
|
+
baseUrl;
|
|
28
|
+
/** The API version in use. */
|
|
29
|
+
apiVersion;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.merchantId = requireSetting('yoma_mmqr', 'merchant_id', options.merchantId);
|
|
32
|
+
this.clientId = requireSetting('yoma_mmqr', 'client_id', options.clientId);
|
|
33
|
+
this.clientSecret = requireSetting('yoma_mmqr', 'client_secret', options.clientSecret);
|
|
34
|
+
this.webhookHashKey = requireSetting('yoma_mmqr', 'webhook_hashkey', options.webhookHashKey);
|
|
35
|
+
this.webhookSecret = optionalSetting(options.webhookSecret);
|
|
36
|
+
this.sandbox = options.sandbox ?? true;
|
|
37
|
+
this.baseUrl = trimUrl(optionalSetting(options.baseUrl) ??
|
|
38
|
+
(this.sandbox ? YomaMmqrConfig.SANDBOX_URL : YomaMmqrConfig.PRODUCTION_URL));
|
|
39
|
+
this.apiVersion = optionalSetting(options.apiVersion) ?? YomaMmqrConfig.DEFAULT_API_VERSION;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads `YOMA_MMQR_MERCHANT_ID`, `YOMA_MMQR_CLIENT_ID`, `YOMA_MMQR_CLIENT_SECRET`,
|
|
43
|
+
* `YOMA_MMQR_WEBHOOK_HASHKEY`, `YOMA_MMQR_WEBHOOK_SECRET`, `YOMA_MMQR_SANDBOX`,
|
|
44
|
+
* `YOMA_MMQR_BASE_URL` and `YOMA_MMQR_API_VERSION`.
|
|
45
|
+
*/
|
|
46
|
+
static fromEnv(env = defaultEnv()) {
|
|
47
|
+
return new YomaMmqrConfig({
|
|
48
|
+
merchantId: envFirst(env, 'YOMA_MMQR_MERCHANT_ID'),
|
|
49
|
+
clientId: envFirst(env, 'YOMA_MMQR_CLIENT_ID'),
|
|
50
|
+
clientSecret: envFirst(env, 'YOMA_MMQR_CLIENT_SECRET'),
|
|
51
|
+
webhookHashKey: envFirst(env, 'YOMA_MMQR_WEBHOOK_HASHKEY'),
|
|
52
|
+
webhookSecret: envFirst(env, 'YOMA_MMQR_WEBHOOK_SECRET'),
|
|
53
|
+
sandbox: envSandbox(env, 'YOMA_MMQR_SANDBOX'),
|
|
54
|
+
baseUrl: envFirst(env, 'YOMA_MMQR_BASE_URL'),
|
|
55
|
+
apiVersion: envFirst(env, 'YOMA_MMQR_API_VERSION'),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const STATUSES = {
|
|
60
|
+
SUCCESS: 'successful',
|
|
61
|
+
PENDING: 'pending',
|
|
62
|
+
FAILED: 'failed',
|
|
63
|
+
FAIL: 'failed',
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Yoma Bank MMQR: QR payments, status checks and callbacks. Access tokens are cached in a
|
|
67
|
+
* {@link TokenCache}; share the gateway (or pass a shared cache) so they survive between calls.
|
|
68
|
+
*/
|
|
69
|
+
export class YomaMmqr {
|
|
70
|
+
/** How long a generated QR stays payable, in seconds. */
|
|
71
|
+
static QR_LIFETIME_SECONDS = 120;
|
|
72
|
+
/** The configuration in use. */
|
|
73
|
+
config;
|
|
74
|
+
transport;
|
|
75
|
+
cache;
|
|
76
|
+
pendingToken;
|
|
77
|
+
constructor(config, options = {}) {
|
|
78
|
+
this.config = config instanceof YomaMmqrConfig ? config : new YomaMmqrConfig(config);
|
|
79
|
+
this.transport = new Transport(httpClientFrom(options));
|
|
80
|
+
this.cache = options.tokenCache ?? new MemoryTokenCache();
|
|
81
|
+
}
|
|
82
|
+
/** A gateway configured from the `YOMA_MMQR_*` environment variables. */
|
|
83
|
+
static fromEnv(env = defaultEnv(), options = {}) {
|
|
84
|
+
return new YomaMmqr(YomaMmqrConfig.fromEnv(env), options);
|
|
85
|
+
}
|
|
86
|
+
/** Checks the order against Yoma's documented limits; throws an `InvalidPaymentDataError`. */
|
|
87
|
+
static validate(data) {
|
|
88
|
+
new Validator()
|
|
89
|
+
.required('orderId', data.orderId)
|
|
90
|
+
.max('orderId', data.orderId, 20)
|
|
91
|
+
.amount('amount', data.amount, { gateway: 'Yoma MMQR', maxDecimals: 0 })
|
|
92
|
+
.required('description', data.description)
|
|
93
|
+
.max('description', data.description, 50)
|
|
94
|
+
.validate();
|
|
95
|
+
}
|
|
96
|
+
/** Checks the order out with Yoma and generates its first QR. Call it once per order. */
|
|
97
|
+
async initiate(data, options = {}) {
|
|
98
|
+
YomaMmqr.validate(data);
|
|
99
|
+
const body = await this.call('payment/checkout', {
|
|
100
|
+
merchantId: this.config.merchantId,
|
|
101
|
+
orderNumber: data.orderId,
|
|
102
|
+
amount: String(toAmount(data.amount)),
|
|
103
|
+
description: data.description,
|
|
104
|
+
}, [], options.signal);
|
|
105
|
+
if (body.checkOutStatus !== true) {
|
|
106
|
+
throw new ApiError('Yoma MMQR did not confirm the checkout.', { raw: toPlainObject(body) });
|
|
107
|
+
}
|
|
108
|
+
return this.renewQr(data.orderId, options);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Generates a new QR for an order that is already checked out, e.g. after the previous one
|
|
112
|
+
* expired. The previous QR's reference stops working.
|
|
113
|
+
*/
|
|
114
|
+
async renewQr(orderId, options = {}) {
|
|
115
|
+
const body = await this.call('qr/generate', { merchantId: this.config.merchantId, orderNumber: orderId }, [], options.signal);
|
|
116
|
+
const qrString = get(body, 'qrString');
|
|
117
|
+
const reference = get(body, 'refLabel');
|
|
118
|
+
if (qrString === '' || reference === '') {
|
|
119
|
+
throw new ApiError('Yoma MMQR did not return a QR.', { raw: toPlainObject(body) });
|
|
120
|
+
}
|
|
121
|
+
return new QrPayment({
|
|
122
|
+
orderId,
|
|
123
|
+
qrImage: qrString,
|
|
124
|
+
expiresAt: new Date(Date.now() + YomaMmqr.QR_LIFETIME_SECONDS * 1000),
|
|
125
|
+
reference,
|
|
126
|
+
raw: toPlainObject(body),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Checks a QR's payment status by its reference (`QrPayment.reference`). An expired QR reports `expired`. */
|
|
130
|
+
async status(reference, options = {}) {
|
|
131
|
+
const body = await this.call('payment/check-status', { merchantId: this.config.merchantId, refLabel: reference }, ['QR EXPIRED'], options.signal);
|
|
132
|
+
if (get(body, 'errorCode') === 'QR EXPIRED') {
|
|
133
|
+
return new PaymentStatusResult({
|
|
134
|
+
status: 'expired',
|
|
135
|
+
gatewayStatus: 'QR EXPIRED',
|
|
136
|
+
gatewayReference: reference,
|
|
137
|
+
raw: toPlainObject(body),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const paymentStatus = trimmed(body, 'paymentStatus');
|
|
141
|
+
return new PaymentStatusResult({
|
|
142
|
+
status: resolveStatus(STATUSES, paymentStatus.toUpperCase()),
|
|
143
|
+
gatewayStatus: paymentStatus,
|
|
144
|
+
gatewayReference: get(body, 'refLabel') || reference,
|
|
145
|
+
raw: toPlainObject(body),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Verifies Yoma's payment callback: HMAC-SHA256 of `orderNumber=..&status=..` keyed with the
|
|
150
|
+
* order number followed by the webhook hash key.
|
|
151
|
+
*/
|
|
152
|
+
handleCallback(request) {
|
|
153
|
+
const payload = losslessBody(request);
|
|
154
|
+
if (this.config.webhookSecret !== undefined &&
|
|
155
|
+
!safeEqual(this.config.webhookSecret, request.header('X-Webhook-Secret') ?? '')) {
|
|
156
|
+
throw new SignatureVerificationError('Yoma MMQR callback has a missing or wrong X-Webhook-Secret header.', toPlainObject(payload));
|
|
157
|
+
}
|
|
158
|
+
const orderNumber = get(payload, 'orderNumber');
|
|
159
|
+
const status = trimmed(payload, 'status');
|
|
160
|
+
const expected = hmacSha256Hex(orderNumber + this.config.webhookHashKey, `orderNumber=${orderNumber}&status=${status}`);
|
|
161
|
+
if (orderNumber === '' || !safeEqual(expected, get(payload, 'hashValue').toLowerCase())) {
|
|
162
|
+
throw new SignatureVerificationError('Yoma MMQR callback hash verification failed.', toPlainObject(payload));
|
|
163
|
+
}
|
|
164
|
+
return new PaymentCallback({
|
|
165
|
+
orderId: orderNumber,
|
|
166
|
+
status: resolveStatus(STATUSES, status.toUpperCase()),
|
|
167
|
+
gatewayStatus: status,
|
|
168
|
+
raw: toPlainObject(payload),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Drops the cached access token, e.g. after rotating the client secret. */
|
|
172
|
+
async forgetToken() {
|
|
173
|
+
await this.cache.delete(this.tokenCacheKey());
|
|
174
|
+
}
|
|
175
|
+
async call(path, data, allowedErrors, signal) {
|
|
176
|
+
const url = `${this.config.baseUrl}/payment-gateway/${this.config.apiVersion}/api/${path}`;
|
|
177
|
+
const send = async () => this.transport.postJson(url, data, { Authorization: `Bearer ${await this.token()}` }, signal);
|
|
178
|
+
let response = await send();
|
|
179
|
+
if (response.status === 401) {
|
|
180
|
+
await this.forgetToken();
|
|
181
|
+
response = await send();
|
|
182
|
+
}
|
|
183
|
+
const body = response.json();
|
|
184
|
+
const errorCode = get(body, 'errorCode');
|
|
185
|
+
if (!response.successful() || (errorCode !== '' && !allowedErrors.includes(errorCode))) {
|
|
186
|
+
throw apiError(path, response.status, body, errorCode);
|
|
187
|
+
}
|
|
188
|
+
return body;
|
|
189
|
+
}
|
|
190
|
+
async token() {
|
|
191
|
+
const cached = await this.cache.get(this.tokenCacheKey());
|
|
192
|
+
if (typeof cached === 'string' && cached !== '') {
|
|
193
|
+
return cached;
|
|
194
|
+
}
|
|
195
|
+
// Concurrent calls share one token request; it ignores any one caller's signal so aborting
|
|
196
|
+
// that caller does not fail the others (the client timeout still applies).
|
|
197
|
+
this.pendingToken ??= this.fetchToken().finally(() => {
|
|
198
|
+
this.pendingToken = undefined;
|
|
199
|
+
});
|
|
200
|
+
return this.pendingToken;
|
|
201
|
+
}
|
|
202
|
+
async fetchToken() {
|
|
203
|
+
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
204
|
+
const response = await this.transport.postForm(`${this.config.baseUrl}/token`, { grant_type: 'client_credentials' }, { Authorization: `Basic ${credentials}` });
|
|
205
|
+
const body = response.json();
|
|
206
|
+
const token = get(body, 'access_token');
|
|
207
|
+
if (!response.successful() || token === '') {
|
|
208
|
+
throw apiError('token', response.status, body, get(body, 'error'));
|
|
209
|
+
}
|
|
210
|
+
const expiresIn = Number.parseInt(get(body, 'expires_in'), 10);
|
|
211
|
+
const ttl = Math.max(60, (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) - 60);
|
|
212
|
+
await this.cache.set(this.tokenCacheKey(), token, ttl);
|
|
213
|
+
return token;
|
|
214
|
+
}
|
|
215
|
+
tokenCacheKey() {
|
|
216
|
+
return `node-myanmar-payments.yoma-mmqr.token.${sha256Hex(`${this.config.baseUrl}|${this.config.clientId}`)}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function apiError(endpoint, status, body, errorCode) {
|
|
220
|
+
const message = optional(body, 'errorDescription') || optional(body, 'error_description');
|
|
221
|
+
return new ApiError(errorCode
|
|
222
|
+
? `Yoma MMQR ${endpoint} failed: [${errorCode}] ${message ?? ''}`.trimEnd()
|
|
223
|
+
: `Yoma MMQR ${endpoint} failed with HTTP ${status}.`, {
|
|
224
|
+
gatewayCode: errorCode || undefined,
|
|
225
|
+
gatewayMessage: message || undefined,
|
|
226
|
+
httpStatus: status,
|
|
227
|
+
raw: toPlainObject(body),
|
|
228
|
+
});
|
|
229
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,153 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laranex/myanmar-payments",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "4.0.0-dev.0",
|
|
4
|
+
"description": "Node.js and TypeScript SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR and CyberSource.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"laranex",
|
|
7
|
+
"node-myanmar-payments",
|
|
8
|
+
"myanmar",
|
|
9
|
+
"payments",
|
|
10
|
+
"payment-gateway",
|
|
11
|
+
"kbzpay",
|
|
12
|
+
"wave-money",
|
|
13
|
+
"wavepay",
|
|
14
|
+
"aya-pay",
|
|
15
|
+
"yoma",
|
|
16
|
+
"mmqr",
|
|
17
|
+
"cybersource"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://laranex.vercel.app/node-myanmar-payments",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/laranex/node-myanmar-payments/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/laranex/node-myanmar-payments.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "Nay Thu Khant <naythukhant644@gmail.com>",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/esm/index.d.ts",
|
|
35
|
+
"default": "./dist/esm/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/cjs/index.d.ts",
|
|
39
|
+
"default": "./dist/cjs/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./kbz-pay": {
|
|
43
|
+
"import": {
|
|
44
|
+
"types": "./dist/esm/kbz-pay/index.d.ts",
|
|
45
|
+
"default": "./dist/esm/kbz-pay/index.js"
|
|
46
|
+
},
|
|
47
|
+
"require": {
|
|
48
|
+
"types": "./dist/cjs/kbz-pay/index.d.ts",
|
|
49
|
+
"default": "./dist/cjs/kbz-pay/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"./wave-money": {
|
|
53
|
+
"import": {
|
|
54
|
+
"types": "./dist/esm/wave-money/index.d.ts",
|
|
55
|
+
"default": "./dist/esm/wave-money/index.js"
|
|
56
|
+
},
|
|
57
|
+
"require": {
|
|
58
|
+
"types": "./dist/cjs/wave-money/index.d.ts",
|
|
59
|
+
"default": "./dist/cjs/wave-money/index.js"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"./aya-pay": {
|
|
63
|
+
"import": {
|
|
64
|
+
"types": "./dist/esm/aya-pay/index.d.ts",
|
|
65
|
+
"default": "./dist/esm/aya-pay/index.js"
|
|
66
|
+
},
|
|
67
|
+
"require": {
|
|
68
|
+
"types": "./dist/cjs/aya-pay/index.d.ts",
|
|
69
|
+
"default": "./dist/cjs/aya-pay/index.js"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"./yoma-mmqr": {
|
|
73
|
+
"import": {
|
|
74
|
+
"types": "./dist/esm/yoma-mmqr/index.d.ts",
|
|
75
|
+
"default": "./dist/esm/yoma-mmqr/index.js"
|
|
76
|
+
},
|
|
77
|
+
"require": {
|
|
78
|
+
"types": "./dist/cjs/yoma-mmqr/index.d.ts",
|
|
79
|
+
"default": "./dist/cjs/yoma-mmqr/index.js"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"./cyber-source": {
|
|
83
|
+
"import": {
|
|
84
|
+
"types": "./dist/esm/cyber-source/index.d.ts",
|
|
85
|
+
"default": "./dist/esm/cyber-source/index.js"
|
|
86
|
+
},
|
|
87
|
+
"require": {
|
|
88
|
+
"types": "./dist/cjs/cyber-source/index.d.ts",
|
|
89
|
+
"default": "./dist/cjs/cyber-source/index.js"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"./package.json": "./package.json"
|
|
93
|
+
},
|
|
94
|
+
"main": "./dist/cjs/index.js",
|
|
95
|
+
"module": "./dist/esm/index.js",
|
|
96
|
+
"types": "./dist/cjs/index.d.ts",
|
|
97
|
+
"typesVersions": {
|
|
98
|
+
"*": {
|
|
99
|
+
"kbz-pay": [
|
|
100
|
+
"./dist/cjs/kbz-pay/index.d.ts"
|
|
101
|
+
],
|
|
102
|
+
"wave-money": [
|
|
103
|
+
"./dist/cjs/wave-money/index.d.ts"
|
|
104
|
+
],
|
|
105
|
+
"aya-pay": [
|
|
106
|
+
"./dist/cjs/aya-pay/index.d.ts"
|
|
107
|
+
],
|
|
108
|
+
"yoma-mmqr": [
|
|
109
|
+
"./dist/cjs/yoma-mmqr/index.d.ts"
|
|
110
|
+
],
|
|
111
|
+
"cyber-source": [
|
|
112
|
+
"./dist/cjs/cyber-source/index.d.ts"
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"files": [
|
|
117
|
+
"dist",
|
|
118
|
+
"skills",
|
|
119
|
+
"CHANGELOG.md",
|
|
120
|
+
"LICENSE.md",
|
|
121
|
+
"README.md"
|
|
122
|
+
],
|
|
123
|
+
"scripts": {
|
|
124
|
+
"build": "node scripts/build.mjs",
|
|
125
|
+
"typecheck": "tsc --noEmit",
|
|
126
|
+
"lint": "eslint . && prettier --check .",
|
|
127
|
+
"format": "prettier --write .",
|
|
128
|
+
"test": "vitest run",
|
|
129
|
+
"test:coverage": "vitest run --coverage",
|
|
130
|
+
"check:package": "publint --strict && attw --pack .",
|
|
131
|
+
"prepack": "npm run build",
|
|
132
|
+
"test:dist": "node scripts/smoke.mjs && node scripts/smoke.cjs"
|
|
133
|
+
},
|
|
134
|
+
"devDependencies": {
|
|
135
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
136
|
+
"@eslint/js": "^10.0.1",
|
|
137
|
+
"@types/node": "^20.19.0",
|
|
138
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
139
|
+
"eslint": "^10.12.0",
|
|
140
|
+
"prettier": "^3.9.9",
|
|
141
|
+
"publint": "^0.3.25",
|
|
142
|
+
"typescript": "~5.9.3",
|
|
143
|
+
"typescript-eslint": "^8.71.1",
|
|
144
|
+
"vitest": "^4.1.11"
|
|
145
|
+
},
|
|
146
|
+
"engines": {
|
|
147
|
+
"node": ">=20"
|
|
148
|
+
},
|
|
149
|
+
"publishConfig": {
|
|
150
|
+
"access": "public",
|
|
151
|
+
"provenance": true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: node-myanmar-payments
|
|
3
|
+
description: >
|
|
4
|
+
Integrate Myanmar payment gateways (KBZ Pay, Wave Money, AYA Pay, Yoma MMQR, CyberSource) in a Node.js or TypeScript app with @laranex/myanmar-payments.
|
|
5
|
+
license: MIT
|
|
6
|
+
metadata:
|
|
7
|
+
author: Nay Thu Khant
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Node Myanmar Payments
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
Use this skill when a Node.js or TypeScript app takes payments through KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR or CyberSource. Start payments and verify callbacks with the package's typed API; never build gateway signatures by hand.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @laranex/myanmar-payments
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Node.js 20+ and has no runtime dependencies (global `fetch`, `node:crypto`). It ships ESM and CommonJS with types. Import shared types from the root and each gateway from the root or its subpath: `@laranex/myanmar-payments/kbz-pay`, `/wave-money`, `/aya-pay`, `/yoma-mmqr`, `/cyber-source`.
|
|
23
|
+
|
|
24
|
+
## Configure
|
|
25
|
+
|
|
26
|
+
Every gateway has a config class with `fromEnv(process.env)`, reading `KBZ_PAY_*`, `WAVE_MONEY_*`, `AYA_PAY_*` (or `AYA_PGW_*`), `YOMA_MMQR_*` and `CYBER_SOURCE_*` (the same variables as the PHP, Laravel and Go packages). `sandbox` defaults to `true`; set `*_SANDBOX=false` (or `sandbox: false`) in production.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { MyanmarPayments } from '@laranex/myanmar-payments';
|
|
30
|
+
|
|
31
|
+
const payments = MyanmarPayments.fromEnv(process.env); // create once, share across requests
|
|
32
|
+
const kbz = payments.kbzPay(); // also waveMoney(), ayaPay(), yomaMmqr(), cyberSource()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Or build one gateway: `new KbzPay({ appId, appKey, merchantCode })`, `KbzPay.fromEnv()`, `new KbzPay(KbzPayConfig.fromEnv(process.env))`.
|
|
36
|
+
- Options (second argument): `{ fetch, httpClient, timeoutMs }`; Yoma and the facade also take `tokenCache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
|
|
37
|
+
- A missing credential throws `ConfigurationError` (`gateway`, `key`).
|
|
38
|
+
|
|
39
|
+
## Use
|
|
40
|
+
|
|
41
|
+
### Amounts
|
|
42
|
+
|
|
43
|
+
Amounts are `Amount.kyat(1000)`, `Amount.parse('1000.50')`, or a whole-number `number`/`bigint`; floats like `10.5` are rejected. Only KBZ Pay (up to 2 decimals) and CyberSource accept decimals; Wave, AYA and Yoma take whole kyat. Invalid data throws `InvalidPaymentDataError` with `errors` per field, before any request is sent.
|
|
44
|
+
|
|
45
|
+
### Start a payment
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Amount } from '@laranex/myanmar-payments';
|
|
49
|
+
|
|
50
|
+
const payment = await kbz.pwa({
|
|
51
|
+
orderId: 'ORDER_1',
|
|
52
|
+
amount: Amount.kyat(1000),
|
|
53
|
+
callbackUrl: 'https://shop.test/payments/kbz/callback',
|
|
54
|
+
});
|
|
55
|
+
res.writeHead(302, { Location: payment.url }).end();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `RedirectPayment` (`url`) from `kbz.pwa(data)` and `wave.initiate(data)`. Wave fills `data.merchantReferenceId` when empty; store it.
|
|
59
|
+
- `FormPayment` from `aya.initiate(data)` and `cyberSource.initiate(data)` (synchronous, no network call): send `payment.toHtml()` as an auto-submitting page, or render `action`, `fields` and `enctype` yourself.
|
|
60
|
+
- `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)`.
|
|
61
|
+
- `AppPayment` from `kbz.app(data)`: `res.json(payment)` sends `orderId`, `orderInfo`, `sign`, `signType`.
|
|
62
|
+
- Every network method takes `{ signal }` as its last argument.
|
|
63
|
+
|
|
64
|
+
AYA needs a channel: `await aya.services()` lists `AyaPayService` entries (`key`, `supports(method)`), then `aya.initiate({ orderId: 'ORDER123', amount: 1000, channel: 'kbz_pay', method: AyaPayMethod.Qr })`.
|
|
65
|
+
|
|
66
|
+
### Handle the callback
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { CallbackRequest, SignatureVerificationError } from '@laranex/myanmar-payments';
|
|
70
|
+
|
|
71
|
+
const request = await CallbackRequest.fromNodeRequest(req); // or fromWebRequest(request) for Fetch servers
|
|
72
|
+
try {
|
|
73
|
+
const callback = kbz.handleCallback(request);
|
|
74
|
+
if (callback.isSuccessful()) {
|
|
75
|
+
// compare callback.amount with the order, then fulfill callback.orderId once
|
|
76
|
+
}
|
|
77
|
+
callback.acknowledgement.send(res); // or return callback.acknowledgement.toResponse()
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof SignatureVerificationError) {
|
|
80
|
+
res.statusCode = 400;
|
|
81
|
+
res.end('invalid signature');
|
|
82
|
+
} else throw error;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- Give the callback route the raw body: in Express use `express.raw({ type: '*/*' })` or mount it before `express.json()`.
|
|
87
|
+
- Check AYA's browser return with `aya.verifyRedirect(request)`.
|
|
88
|
+
- For production, store the verified call, acknowledge immediately, then process it once in the background.
|
|
89
|
+
|
|
90
|
+
### Check status and handle errors
|
|
91
|
+
|
|
92
|
+
- `kbz.status(orderId)`, `aya.status(orderId)` and `yoma.status(reference)` return `PaymentStatusResult` with `status` and `isSuccessful()`. Wave Money and CyberSource have no status API.
|
|
93
|
+
- Statuses: `PaymentStatus.Successful`, `Pending`, `Failed`, `Cancelled`, `Expired`, `Unknown`; `PaymentStatus.isFinal(status)`.
|
|
94
|
+
- Gateway failures throw `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause` for network errors and aborts). All errors extend `PaymentError`.
|
|
95
|
+
|
|
96
|
+
## Test your app
|
|
97
|
+
|
|
98
|
+
- Pass a fake `fetch` in the options: `new KbzPay(config, { fetch: async () => new Response('{"Response":{...}}') })`.
|
|
99
|
+
- Replay a stored callback with `CallbackRequest.fromJson(payload, headers)` or `CallbackRequest.from({ body, headers, query })`; it is still signature-checked.
|
|
100
|
+
- To test your own fulfillment code, build `new PaymentCallback({ orderId: 'ORDER_1', status: 'successful', gatewayStatus: 'PAY_SUCCESS' })` yourself.
|
|
101
|
+
|
|
102
|
+
## Avoid
|
|
103
|
+
|
|
104
|
+
- Fulfilling orders from return pages or query strings; fulfill only from a verified callback or a status check.
|
|
105
|
+
- Treating `pending` or `unknown` as paid.
|
|
106
|
+
- Passing floats or `Number(price)` as amounts; use `Amount.parse()`.
|
|
107
|
+
- Parsing the callback body with `express.json()` before the package sees it when you can pass the raw body.
|
|
108
|
+
- Reusing a Wave `merchantReferenceId`, or calling Yoma `initiate()` twice for one order (use `renewQr()`).
|
|
109
|
+
- Opening a KBZ Pay PWA link outside a phone with the KBZ Pay app.
|