@laranex/myanmar-payments 0.0.0-stage → 4.0.0-dev.1

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +10 -2
  4. package/dist/cjs/aya-pay/aya-pay.d.ts +122 -0
  5. package/dist/cjs/aya-pay/aya-pay.js +301 -0
  6. package/dist/cjs/aya-pay/index.d.ts +1 -0
  7. package/dist/cjs/aya-pay/index.js +8 -0
  8. package/dist/cjs/core/amount.d.ts +57 -0
  9. package/dist/cjs/core/amount.js +107 -0
  10. package/dist/cjs/core/cache.d.ts +26 -0
  11. package/dist/cjs/core/cache.js +35 -0
  12. package/dist/cjs/core/callback.d.ts +180 -0
  13. package/dist/cjs/core/callback.js +304 -0
  14. package/dist/cjs/core/crypto.d.ts +19 -0
  15. package/dist/cjs/core/crypto.js +50 -0
  16. package/dist/cjs/core/env.d.ts +21 -0
  17. package/dist/cjs/core/env.js +54 -0
  18. package/dist/cjs/core/errors.d.ts +56 -0
  19. package/dist/cjs/core/errors.js +78 -0
  20. package/dist/cjs/core/http.d.ts +81 -0
  21. package/dist/cjs/core/http.js +128 -0
  22. package/dist/cjs/core/index.d.ts +8 -0
  23. package/dist/cjs/core/index.js +30 -0
  24. package/dist/cjs/core/json.d.ts +44 -0
  25. package/dist/cjs/core/json.js +254 -0
  26. package/dist/cjs/core/results.d.ts +122 -0
  27. package/dist/cjs/core/results.js +147 -0
  28. package/dist/cjs/core/status.d.ts +50 -0
  29. package/dist/cjs/core/status.js +60 -0
  30. package/dist/cjs/core/validate.d.ts +46 -0
  31. package/dist/cjs/core/validate.js +148 -0
  32. package/dist/cjs/core/values.d.ts +16 -0
  33. package/dist/cjs/core/values.js +45 -0
  34. package/dist/cjs/cyber-source/cyber-source.d.ts +95 -0
  35. package/dist/cjs/cyber-source/cyber-source.js +188 -0
  36. package/dist/cjs/cyber-source/index.d.ts +1 -0
  37. package/dist/cjs/cyber-source/index.js +7 -0
  38. package/dist/cjs/index.d.ts +7 -0
  39. package/dist/cjs/index.js +25 -0
  40. package/dist/cjs/kbz-pay/index.d.ts +2 -0
  41. package/dist/cjs/kbz-pay/index.js +8 -0
  42. package/dist/cjs/kbz-pay/kbz-pay.d.ts +102 -0
  43. package/dist/cjs/kbz-pay/kbz-pay.js +266 -0
  44. package/dist/cjs/kbz-pay/signer.d.ts +15 -0
  45. package/dist/cjs/kbz-pay/signer.js +36 -0
  46. package/dist/cjs/myanmar-payments.d.ts +50 -0
  47. package/dist/cjs/myanmar-payments.js +73 -0
  48. package/dist/cjs/package.json +3 -0
  49. package/dist/cjs/wave-money/index.d.ts +1 -0
  50. package/dist/cjs/wave-money/index.js +6 -0
  51. package/dist/cjs/wave-money/wave-money.d.ts +111 -0
  52. package/dist/cjs/wave-money/wave-money.js +251 -0
  53. package/dist/cjs/yoma-mmqr/index.d.ts +1 -0
  54. package/dist/cjs/yoma-mmqr/index.js +6 -0
  55. package/dist/cjs/yoma-mmqr/yoma-mmqr.d.ts +105 -0
  56. package/dist/cjs/yoma-mmqr/yoma-mmqr.js +233 -0
  57. package/dist/esm/aya-pay/aya-pay.d.ts +122 -0
  58. package/dist/esm/aya-pay/aya-pay.js +296 -0
  59. package/dist/esm/aya-pay/index.d.ts +1 -0
  60. package/dist/esm/aya-pay/index.js +1 -0
  61. package/dist/esm/core/amount.d.ts +57 -0
  62. package/dist/esm/core/amount.js +103 -0
  63. package/dist/esm/core/cache.d.ts +26 -0
  64. package/dist/esm/core/cache.js +31 -0
  65. package/dist/esm/core/callback.d.ts +180 -0
  66. package/dist/esm/core/callback.js +294 -0
  67. package/dist/esm/core/crypto.d.ts +19 -0
  68. package/dist/esm/core/crypto.js +41 -0
  69. package/dist/esm/core/env.d.ts +21 -0
  70. package/dist/esm/core/env.js +45 -0
  71. package/dist/esm/core/errors.d.ts +56 -0
  72. package/dist/esm/core/errors.js +70 -0
  73. package/dist/esm/core/http.d.ts +81 -0
  74. package/dist/esm/core/http.js +121 -0
  75. package/dist/esm/core/index.d.ts +8 -0
  76. package/dist/esm/core/index.js +7 -0
  77. package/dist/esm/core/json.d.ts +44 -0
  78. package/dist/esm/core/json.js +244 -0
  79. package/dist/esm/core/results.d.ts +122 -0
  80. package/dist/esm/core/results.js +139 -0
  81. package/dist/esm/core/status.d.ts +50 -0
  82. package/dist/esm/core/status.js +56 -0
  83. package/dist/esm/core/validate.d.ts +46 -0
  84. package/dist/esm/core/validate.js +142 -0
  85. package/dist/esm/core/values.d.ts +16 -0
  86. package/dist/esm/core/values.js +38 -0
  87. package/dist/esm/cyber-source/cyber-source.d.ts +95 -0
  88. package/dist/esm/cyber-source/cyber-source.js +184 -0
  89. package/dist/esm/cyber-source/index.d.ts +1 -0
  90. package/dist/esm/cyber-source/index.js +1 -0
  91. package/dist/esm/index.d.ts +7 -0
  92. package/dist/esm/index.js +7 -0
  93. package/dist/esm/kbz-pay/index.d.ts +2 -0
  94. package/dist/esm/kbz-pay/index.js +2 -0
  95. package/dist/esm/kbz-pay/kbz-pay.d.ts +102 -0
  96. package/dist/esm/kbz-pay/kbz-pay.js +262 -0
  97. package/dist/esm/kbz-pay/signer.d.ts +15 -0
  98. package/dist/esm/kbz-pay/signer.js +32 -0
  99. package/dist/esm/myanmar-payments.d.ts +50 -0
  100. package/dist/esm/myanmar-payments.js +69 -0
  101. package/dist/esm/wave-money/index.d.ts +1 -0
  102. package/dist/esm/wave-money/index.js +1 -0
  103. package/dist/esm/wave-money/wave-money.d.ts +111 -0
  104. package/dist/esm/wave-money/wave-money.js +246 -0
  105. package/dist/esm/yoma-mmqr/index.d.ts +1 -0
  106. package/dist/esm/yoma-mmqr/index.js +1 -0
  107. package/dist/esm/yoma-mmqr/yoma-mmqr.d.ts +105 -0
  108. package/dist/esm/yoma-mmqr/yoma-mmqr.js +229 -0
  109. package/package.json +151 -4
  110. 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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "4.0.0-dev.1",
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.