@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@laranex/myanmar-payments` (node-myanmar-payments) will be documented in this file.
4
+
5
+ ## v4.0.0 - Unreleased
6
+
7
+ Initial release. The version number matches the other Laranex Myanmar payments packages (`php-myanmar-payments`, `laravel-myanmar-payments`, `go-myanmar-payments`), and the gateways, flows, validation, signing, status maps, results and errors are a port of them, sharing their test vectors.
8
+
9
+ ### Added
10
+ - Gateways: KBZ Pay (`pwa`, `qr`, `app`, `status`), Wave Money (`initiate`), AYA Payment Gateway (`services`, `initiate`, `status`, `verifyRedirect`), Yoma MMQR (`initiate`, `renewQr`, `status`, `forgetToken`) and CyberSource Secure Acceptance (`initiate`); every gateway validates its payment data (`KbzPay.validate()` and so on) and verifies callbacks with `handleCallback`.
11
+ - Subpath exports per gateway (`@laranex/myanmar-payments/kbz-pay`, `/wave-money`, `/aya-pay`, `/yoma-mmqr`, `/cyber-source`) next to the root export, as ES modules and CommonJS with type declarations.
12
+ - Exact `Amount` type (`Amount.kyat()`, `Amount.parse()`) kept as decimal text, so amounts never pass through a float; payment data also takes a whole-number `number` or `bigint` and rejects fractions. Decimals are accepted only where the gateway documents them (KBZ Pay up to 2 places, CyberSource any); every gateway except CyberSource is MMK only.
13
+ - A config class per gateway with `fromEnv(process.env)`, reading the same `*_SANDBOX`, credential and URL variables as the PHP, Laravel and Go packages; a missing credential throws `ConfigurationError`. `MyanmarPayments` builds every gateway lazily from one config object or the environment.
14
+ - Result classes: `RedirectPayment`, `FormPayment` (with `toHtml()`), `QrPayment` (with `qrImageDataUri()`) and `AppPayment`; `PaymentCallback` (with a gateway-independent `PaymentStatus` and the `Acknowledgement` each gateway expects, sent with `send(res)` or `toResponse()`) and `PaymentStatusResult`.
15
+ - `CallbackRequest.fromNodeRequest()` (node:http, Express, Fastify, Koa), `CallbackRequest.fromWebRequest()` (Fetch API servers such as Next.js, Hono and Bun), `CallbackRequest.from()` and `CallbackRequest.fromJson()`. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text.
16
+ - Errors: `PaymentError` and its subclasses `InvalidPaymentDataError` (per-field `errors`), `ApiError` (`gatewayCode`, `gatewayMessage`, `httpStatus`, `raw`, `cause`), `SignatureVerificationError` and `ConfigurationError`.
17
+ - No runtime dependencies: the global `fetch` (or an injected `fetch` / `HttpClient`, 30 second default timeout, `AbortSignal` per call) and `node:crypto`; an injectable `TokenCache` with an in-memory default for Yoma's access token.
18
+ - Callback, return and cancel URLs only need to be valid absolute http or https URLs; there is no HTTPS-only rule (gateways may still require HTTPS in production).
19
+ - Wave Money's sandbox is `https://preprodpayments.wavemoney.io:8107`, with checkout at `https://preprodpayments.wavemoney.io/authenticate`.
20
+ - Agent skill in `skills/node-myanmar-payments` so coding agents use the package correctly; install it with `npx skills add laranex/node-myanmar-payments`.
21
+ - Requires Node.js 20 or higher.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nay Thu Khant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # Node Myanmar Payments
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@laranex/myanmar-payments.svg?style=flat-square)](https://www.npmjs.com/package/@laranex/myanmar-payments)
4
+ [![Tests](https://img.shields.io/github/actions/workflow/status/laranex/node-myanmar-payments/tests.yml?label=tests&style=flat-square)](https://github.com/laranex/node-myanmar-payments/actions/workflows/tests.yml)
5
+ [![License](https://img.shields.io/github/license/laranex/node-myanmar-payments.svg?style=flat-square)](LICENSE.md)
6
+
7
+ A Node.js and TypeScript SDK for Myanmar payment gateways (KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR and CyberSource), with typed payment data, verified callbacks and a gateway-independent payment status. Built for humans and AI agents.
8
+
9
+ ## Documentation
10
+
11
+ Full documentation, including installation, usage and the AI agent skill, lives at **[laranex.vercel.app/node-myanmar-payments](https://laranex.vercel.app/node-myanmar-payments)**.
@@ -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,301 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AyaPay = exports.AyaPayService = exports.AyaPayMethod = exports.AyaPayConfig = void 0;
4
+ const callback_js_1 = require("../core/callback.js");
5
+ const crypto_js_1 = require("../core/crypto.js");
6
+ const env_js_1 = require("../core/env.js");
7
+ const errors_js_1 = require("../core/errors.js");
8
+ const http_js_1 = require("../core/http.js");
9
+ const json_js_1 = require("../core/json.js");
10
+ const results_js_1 = require("../core/results.js");
11
+ const status_js_1 = require("../core/status.js");
12
+ const validate_js_1 = require("../core/validate.js");
13
+ const values_js_1 = require("../core/values.js");
14
+ /**
15
+ * AYA Payment Gateway (APG) credentials and endpoints. A missing credential throws a
16
+ * `ConfigurationError`.
17
+ */
18
+ class AyaPayConfig {
19
+ static SANDBOX_URL = 'https://uat-pgw.ayainnovation.com';
20
+ static PRODUCTION_URL = 'https://pgw.ayainnovation.com';
21
+ appKey;
22
+ appSecret;
23
+ sandbox;
24
+ /** The base URL in use. */
25
+ baseUrl;
26
+ constructor(options) {
27
+ this.appKey = (0, env_js_1.requireSetting)('aya_pay', 'app_key', options.appKey);
28
+ this.appSecret = (0, env_js_1.requireSetting)('aya_pay', 'app_secret', options.appSecret);
29
+ this.sandbox = options.sandbox ?? true;
30
+ this.baseUrl = (0, env_js_1.trimUrl)((0, env_js_1.optionalSetting)(options.baseUrl) ??
31
+ (this.sandbox ? AyaPayConfig.SANDBOX_URL : AyaPayConfig.PRODUCTION_URL));
32
+ }
33
+ /**
34
+ * Reads `AYA_PAY_APP_KEY`, `AYA_PAY_APP_SECRET`, `AYA_PAY_SANDBOX` and `AYA_PAY_BASE_URL`, falling
35
+ * back to the `AYA_PGW_*` names.
36
+ */
37
+ static fromEnv(env = (0, env_js_1.defaultEnv)()) {
38
+ return new AyaPayConfig({
39
+ appKey: (0, env_js_1.envFirst)(env, 'AYA_PAY_APP_KEY', 'AYA_PGW_APP_KEY'),
40
+ appSecret: (0, env_js_1.envFirst)(env, 'AYA_PAY_APP_SECRET', 'AYA_PGW_APP_SECRET'),
41
+ sandbox: (0, env_js_1.envSandbox)(env, 'AYA_PAY_SANDBOX'),
42
+ baseUrl: (0, env_js_1.envFirst)(env, 'AYA_PAY_BASE_URL', 'AYA_PGW_BASE_URL'),
43
+ });
44
+ }
45
+ }
46
+ exports.AyaPayConfig = AyaPayConfig;
47
+ /** The AYA payment methods. */
48
+ exports.AyaPayMethod = Object.freeze({
49
+ /** Pay on a hosted web page (cards, web checkout). */
50
+ Web: 'WEB',
51
+ /** Scan a QR with the wallet app. */
52
+ Qr: 'QR',
53
+ /** Approve a push notification in the wallet app. */
54
+ Noti: 'NOTI',
55
+ });
56
+ const METHODS = ['WEB', 'QR', 'NOTI'];
57
+ /** A payment channel enabled for your merchant account. */
58
+ class AyaPayService {
59
+ /** The display name, e.g. `AYA Pay`. */
60
+ name;
61
+ /** The `channel` value to send when initiating, e.g. `aya_pay` or `visa`. */
62
+ key;
63
+ /** The channel's logo. */
64
+ imageUrl;
65
+ /** The methods this channel supports. */
66
+ methods;
67
+ /** Methods the gateway listed that this package does not know yet. */
68
+ unknownMethods;
69
+ constructor(init) {
70
+ this.name = init.name;
71
+ this.key = init.key;
72
+ this.imageUrl = init.imageUrl;
73
+ this.methods = Object.freeze([...init.methods]);
74
+ this.unknownMethods = Object.freeze([...(init.unknownMethods ?? [])]);
75
+ }
76
+ /** Whether the channel supports `method`. */
77
+ supports(method) {
78
+ return this.methods.includes(method);
79
+ }
80
+ }
81
+ exports.AyaPayService = AyaPayService;
82
+ const CURRENCY_MMK = '104';
83
+ const STATUSES = {
84
+ '00': 'successful',
85
+ '01': 'pending',
86
+ '02': 'failed',
87
+ '03': 'failed',
88
+ '04': 'expired',
89
+ };
90
+ /**
91
+ * The documented field order of the decoded callback / enquiry payload. AYA leaves out fields that
92
+ * do not apply (e.g. card fields for wallet payments) and signs only the ones present. The payload
93
+ * spells `currencyCode` as `currenyCode`.
94
+ */
95
+ const PAYLOAD_FIELDS = [
96
+ 'merchOrderId',
97
+ 'tranId',
98
+ 'amount',
99
+ 'currencyCode',
100
+ 'statusCode',
101
+ 'paymentCardNumber',
102
+ 'paymentMobileNumber',
103
+ 'cardTypeName',
104
+ 'cardExpiryDate',
105
+ 'nameOnCard',
106
+ 'approvalCode',
107
+ 'tranRef',
108
+ 'userRef1',
109
+ 'userRef2',
110
+ 'userRef3',
111
+ 'userRef4',
112
+ 'userRef5',
113
+ 'description',
114
+ 'dateTime',
115
+ ];
116
+ /**
117
+ * The AYA Payment Gateway (APG): one hosted checkout for AYA Pay, other wallets and cards, with
118
+ * channel listing, status enquiry and verified callbacks.
119
+ */
120
+ class AyaPay {
121
+ /** The configuration in use. */
122
+ config;
123
+ transport;
124
+ constructor(config, options = {}) {
125
+ this.config = config instanceof AyaPayConfig ? config : new AyaPayConfig(config);
126
+ this.transport = new http_js_1.Transport((0, http_js_1.httpClientFrom)(options));
127
+ }
128
+ /** A gateway configured from the `AYA_PAY_*` (or `AYA_PGW_*`) environment variables. */
129
+ static fromEnv(env = (0, env_js_1.defaultEnv)(), options = {}) {
130
+ return new AyaPay(AyaPayConfig.fromEnv(env), options);
131
+ }
132
+ /** Checks the order against AYA's documented rules; throws an `InvalidPaymentDataError`. */
133
+ static validate(data) {
134
+ const userRefs = data.userRefs;
135
+ new validate_js_1.Validator()
136
+ .required('orderId', data.orderId)
137
+ .length('orderId', data.orderId, 6, 40)
138
+ .amount('amount', data.amount, { gateway: 'AYA Payment Gateway', maxDecimals: 0 })
139
+ .required('channel', data.channel)
140
+ .when(!METHODS.includes(data.method), 'method', 'The method field must be one of WEB, QR or NOTI.')
141
+ .string('returnUrl', data.returnUrl)
142
+ .url('returnUrl', data.returnUrl)
143
+ .string('description', data.description)
144
+ .when(userRefs !== undefined &&
145
+ (!Array.isArray(userRefs) || userRefs.some((ref) => typeof ref !== 'string')), 'userRefs', 'The userRefs field must be a list of strings.')
146
+ .when(Array.isArray(userRefs) && userRefs.length > 5, 'userRefs', 'The userRefs field must not have more than 5 items.')
147
+ .validate();
148
+ }
149
+ /** Lists the payment channels enabled for your merchant account. */
150
+ async services(options = {}) {
151
+ const timestamp = unixTime();
152
+ const body = await this.post('services', {
153
+ appKey: this.config.appKey,
154
+ timestamp,
155
+ checkSum: this.checksum([this.config.appKey, this.config.appSecret, String(timestamp)]),
156
+ }, options.signal);
157
+ const list = Array.isArray(body.data) ? body.data : [];
158
+ const services = [];
159
+ for (const entry of list) {
160
+ if (!(0, json_js_1.isLosslessObject)(entry) || (0, values_js_1.get)(entry, 'key') === '') {
161
+ continue;
162
+ }
163
+ const methods = [];
164
+ const unknownMethods = [];
165
+ for (const method of Array.isArray(entry.methods) ? entry.methods : []) {
166
+ const name = (0, values_js_1.scalarString)(method) ?? '';
167
+ if (METHODS.includes(name)) {
168
+ methods.push(name);
169
+ }
170
+ else {
171
+ unknownMethods.push(name);
172
+ }
173
+ }
174
+ services.push(new AyaPayService({
175
+ name: (0, values_js_1.get)(entry, 'name') || (0, values_js_1.get)(entry, 'key'),
176
+ key: (0, values_js_1.get)(entry, 'key'),
177
+ imageUrl: (0, values_js_1.optional)(entry, 'image_url'),
178
+ methods,
179
+ unknownMethods,
180
+ }));
181
+ }
182
+ return services;
183
+ }
184
+ /**
185
+ * Signs the order. The customer's browser must POST the returned form to AYA's hosted checkout;
186
+ * `toHtml()` renders a page that does it. Makes no network call.
187
+ */
188
+ initiate(data) {
189
+ AyaPay.validate(data);
190
+ const refs = [...(data.userRefs ?? []), '', '', '', '', ''].slice(0, 5);
191
+ const fields = [
192
+ { name: 'merchOrderId', value: data.orderId },
193
+ { name: 'amount', value: String((0, validate_js_1.toAmount)(data.amount)) },
194
+ { name: 'appKey', value: this.config.appKey },
195
+ { name: 'timestamp', value: String(unixTime()) },
196
+ { name: 'userRef1', value: refs[0] },
197
+ { name: 'userRef2', value: refs[1] },
198
+ { name: 'userRef3', value: refs[2] },
199
+ { name: 'userRef4', value: refs[3] },
200
+ { name: 'userRef5', value: refs[4] },
201
+ { name: 'description', value: data.description ?? '' },
202
+ { name: 'currencyCode', value: CURRENCY_MMK },
203
+ { name: 'channel', value: data.channel },
204
+ { name: 'method', value: data.method },
205
+ { name: 'overrideFrontendRedirectUrl', value: data.returnUrl ?? '' },
206
+ ];
207
+ fields.push({ name: 'checkSum', value: this.checksum(fields.map((field) => field.value)) });
208
+ return new results_js_1.FormPayment({
209
+ orderId: data.orderId,
210
+ action: `${this.config.baseUrl}/v1/payment/request`,
211
+ fields,
212
+ enctype: 'multipart/form-data',
213
+ });
214
+ }
215
+ /** Asks AYA for the current state of an order (enquiry). */
216
+ async status(orderId, options = {}) {
217
+ const timestamp = unixTime();
218
+ const body = await this.post('enquiry', {
219
+ merchOrderId: orderId,
220
+ appKey: this.config.appKey,
221
+ timestamp,
222
+ checkSum: this.checksum([orderId, String(timestamp), this.config.appKey]),
223
+ }, options.signal);
224
+ const payload = this.verifiedPayload((0, values_js_1.object)(body, 'data') ?? {}, 'enquiry response');
225
+ const statusCode = (0, values_js_1.trimmed)(payload, 'statusCode');
226
+ return new callback_js_1.PaymentStatusResult({
227
+ orderId: (0, values_js_1.optional)(payload, 'merchOrderId') || orderId,
228
+ status: (0, status_js_1.resolveStatus)(STATUSES, statusCode),
229
+ gatewayStatus: statusCode,
230
+ gatewayReference: (0, values_js_1.optional)(payload, 'tranId'),
231
+ amount: (0, values_js_1.optional)(payload, 'amount'),
232
+ raw: (0, json_js_1.toPlainObject)(payload),
233
+ });
234
+ }
235
+ /** Verifies AYA's backend callback. */
236
+ handleCallback(request) {
237
+ return toCallback(this.verifiedPayload((0, callback_js_1.losslessInput)(request), 'callback'));
238
+ }
239
+ /**
240
+ * Verifies the signed query string AYA adds when it sends the customer back to your return URL.
241
+ * Use it to show the right page; fulfill orders from the backend callback.
242
+ */
243
+ verifyRedirect(request) {
244
+ return toCallback(this.verifiedPayload((0, callback_js_1.losslessQueryInput)(request), 'redirect'));
245
+ }
246
+ /** Decodes a `payload` + `checkSum` pair and returns the payload if the checksum matches. */
247
+ verifiedPayload(input, context) {
248
+ const fail = () => new errors_js_1.SignatureVerificationError(`AYA Pay ${context} checksum verification failed.`, (0, json_js_1.toPlainObject)(input));
249
+ const json = (0, crypto_js_1.decodeBase64)((0, values_js_1.get)(input, 'payload'));
250
+ const payload = json === undefined ? undefined : (0, json_js_1.parseJsonObject)(json);
251
+ if (payload === undefined) {
252
+ throw fail();
253
+ }
254
+ const parts = [];
255
+ for (const field of PAYLOAD_FIELDS) {
256
+ const key = field === 'currencyCode' && 'currenyCode' in payload ? 'currenyCode' : field;
257
+ if (Object.prototype.hasOwnProperty.call(payload, key)) {
258
+ parts.push((0, values_js_1.scalarString)(payload[key]) ?? '');
259
+ }
260
+ }
261
+ if (!(0, crypto_js_1.safeEqual)(this.checksum(parts), (0, values_js_1.get)(input, 'checkSum').toLowerCase())) {
262
+ throw fail();
263
+ }
264
+ return payload;
265
+ }
266
+ checksum(parts) {
267
+ return (0, crypto_js_1.hmacSha256Hex)(this.config.appSecret, parts.join(':'));
268
+ }
269
+ async post(endpoint, data, signal) {
270
+ const response = await this.transport.postJson(`${this.config.baseUrl}/v1/payment/${endpoint}`, data, {}, signal);
271
+ const body = response.json();
272
+ const status = (0, values_js_1.get)(body, 'status');
273
+ if (!response.successful() || status !== '00') {
274
+ const message = (0, values_js_1.optional)(body, 'message');
275
+ throw new errors_js_1.ApiError(status
276
+ ? `AYA Pay ${endpoint} failed: [${status}] ${message ?? ''}`.trimEnd()
277
+ : `AYA Pay ${endpoint} failed with HTTP ${response.status}.`, {
278
+ gatewayCode: status || undefined,
279
+ gatewayMessage: message,
280
+ httpStatus: response.status,
281
+ raw: (0, json_js_1.toPlainObject)(body),
282
+ });
283
+ }
284
+ return body;
285
+ }
286
+ }
287
+ exports.AyaPay = AyaPay;
288
+ function toCallback(payload) {
289
+ const statusCode = (0, values_js_1.trimmed)(payload, 'statusCode');
290
+ return new callback_js_1.PaymentCallback({
291
+ orderId: (0, values_js_1.get)(payload, 'merchOrderId'),
292
+ status: (0, status_js_1.resolveStatus)(STATUSES, statusCode),
293
+ gatewayStatus: statusCode,
294
+ gatewayReference: (0, values_js_1.optional)(payload, 'tranId'),
295
+ amount: (0, values_js_1.optional)(payload, 'amount'),
296
+ raw: (0, json_js_1.toPlainObject)(payload),
297
+ });
298
+ }
299
+ function unixTime() {
300
+ return Math.floor(Date.now() / 1000);
301
+ }
@@ -0,0 +1 @@
1
+ export { AyaPay, AyaPayConfig, AyaPayMethod, AyaPayService, type AyaPayConfigOptions, type AyaPayPaymentData, } from './aya-pay.js';
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AyaPayService = exports.AyaPayMethod = exports.AyaPayConfig = exports.AyaPay = void 0;
4
+ var aya_pay_js_1 = require("./aya-pay.js");
5
+ Object.defineProperty(exports, "AyaPay", { enumerable: true, get: function () { return aya_pay_js_1.AyaPay; } });
6
+ Object.defineProperty(exports, "AyaPayConfig", { enumerable: true, get: function () { return aya_pay_js_1.AyaPayConfig; } });
7
+ Object.defineProperty(exports, "AyaPayMethod", { enumerable: true, get: function () { return aya_pay_js_1.AyaPayMethod; } });
8
+ Object.defineProperty(exports, "AyaPayService", { enumerable: true, get: function () { return aya_pay_js_1.AyaPayService; } });
@@ -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,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Amount = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const PATTERN = /^\d+(\.\d+)?$/;
6
+ /**
7
+ * An exact, non-negative money amount, kept as decimal text so it is never rounded through a
8
+ * floating-point number. Build one with {@link Amount.kyat} for whole amounts or
9
+ * {@link Amount.parse} for decimal ones; each gateway then checks it against that gateway's
10
+ * documented rules (Wave Money, AYA and Yoma MMQR only accept whole kyat, KBZ Pay up to 2 decimal
11
+ * places, CyberSource any).
12
+ *
13
+ * Amounts are immutable. `JSON.stringify` writes them as strings, e.g. `"1000.50"`.
14
+ */
15
+ class Amount {
16
+ value;
17
+ constructor(value) {
18
+ this.value = value;
19
+ }
20
+ /**
21
+ * A whole amount, e.g. `Amount.kyat(1000)` for 1000 MMK. Works for whole units of any currency.
22
+ *
23
+ * Takes a safe integer `number` or a `bigint`. Fractions, negative values, `NaN` and integers
24
+ * beyond `Number.MAX_SAFE_INTEGER` (pass a `bigint` instead) throw an
25
+ * {@link InvalidPaymentDataError} for the `amount` field.
26
+ */
27
+ static kyat(amount) {
28
+ if (typeof amount === 'bigint') {
29
+ if (amount < 0n) {
30
+ throw invalid('The amount field must not be negative.');
31
+ }
32
+ return new Amount(amount.toString());
33
+ }
34
+ if (typeof amount !== 'number' || !Number.isSafeInteger(amount)) {
35
+ throw invalid(`The amount field must be a whole number of kyat; use Amount.parse('10.50') for decimal amounts, got ${String(amount)}.`);
36
+ }
37
+ if (amount < 0) {
38
+ throw invalid('The amount field must not be negative.');
39
+ }
40
+ return new Amount(String(amount));
41
+ }
42
+ /**
43
+ * A decimal amount written as plain digits, e.g. `Amount.parse('1000.50')`. Signs, exponents,
44
+ * spaces and thousands separators are rejected with an {@link InvalidPaymentDataError}. Leading
45
+ * zeros of the whole part are removed; the fractional digits are kept exactly as given.
46
+ */
47
+ static parse(amount) {
48
+ if (typeof amount !== 'string' || !PATTERN.test(amount)) {
49
+ throw invalid(`The amount field must be a number such as 1000 or 1000.50, got ${JSON.stringify(amount)}.`);
50
+ }
51
+ const [whole = '', fraction] = amount.split('.');
52
+ const trimmedWhole = whole.replace(/^0+/, '') || '0';
53
+ return new Amount(fraction === undefined ? trimmedWhole : `${trimmedWhole}.${fraction}`);
54
+ }
55
+ /**
56
+ * Returns `amount` when it is already an `Amount`, or {@link Amount.kyat} of a whole number.
57
+ */
58
+ static from(amount) {
59
+ return amount instanceof Amount ? amount : Amount.kyat(amount);
60
+ }
61
+ /** Whether `value` is an `Amount`. */
62
+ static isAmount(value) {
63
+ return value instanceof Amount;
64
+ }
65
+ /** The amount exactly as it is sent to the gateway, e.g. `"1000.50"`. */
66
+ toString() {
67
+ return this.value;
68
+ }
69
+ /** The amount as a JSON string, e.g. `"1000.50"`, so no consumer parses it as a float. */
70
+ toJSON() {
71
+ return this.value;
72
+ }
73
+ /** The number of fractional digits, e.g. `2` for `1000.50`. */
74
+ decimalPlaces() {
75
+ const dot = this.value.indexOf('.');
76
+ return dot === -1 ? 0 : this.value.length - dot - 1;
77
+ }
78
+ /** The digits before the decimal point, e.g. `"1000"` for `1000.50`. */
79
+ wholePart() {
80
+ return this.value.split('.')[0];
81
+ }
82
+ /** Whether the amount equals zero, e.g. `0` or `0.00`. */
83
+ isZero() {
84
+ return /^[0.]+$/.test(this.value);
85
+ }
86
+ /** Whether the amount is greater than zero. */
87
+ isPositive() {
88
+ return !this.isZero();
89
+ }
90
+ /**
91
+ * Whether both amounts have the same value, ignoring trailing fractional zeros
92
+ * (`1000`, `1000.0` and `1000.00` are equal). `undefined` is never equal, so a gateway amount
93
+ * that was not sent never matches.
94
+ */
95
+ equals(other) {
96
+ if (other === undefined || other === null) {
97
+ return false;
98
+ }
99
+ const normalize = (value) => value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value;
100
+ const text = other instanceof Amount ? other.value : other;
101
+ return normalize(this.value) === normalize(text);
102
+ }
103
+ }
104
+ exports.Amount = Amount;
105
+ function invalid(message) {
106
+ return new errors_js_1.InvalidPaymentDataError({ amount: message });
107
+ }