@keytd/manat 0.0.0-stage → 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,1358 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AZERICARD_MAC_FIELDS: () => AZERICARD_MAC_FIELDS,
24
+ AzericardProvider: () => AzericardProvider,
25
+ EpointProvider: () => EpointProvider,
26
+ GoldenPayProvider: () => GoldenPayProvider,
27
+ KapitalBankProvider: () => KapitalBankProvider,
28
+ Manat: () => Manat,
29
+ ManatError: () => ManatError,
30
+ PayriffProvider: () => PayriffProvider,
31
+ YigimProvider: () => YigimProvider,
32
+ azericard: () => azericard,
33
+ azn: () => azn,
34
+ epoint: () => epoint,
35
+ fromDecimal: () => fromDecimal,
36
+ goldenPay: () => goldenPay,
37
+ isManatError: () => isManatError,
38
+ kapitalBank: () => kapitalBank,
39
+ payriff: () => payriff,
40
+ toDecimalString: () => toDecimalString,
41
+ yigim: () => yigim
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/errors.ts
46
+ var ManatError = class extends Error {
47
+ code;
48
+ provider;
49
+ providerCode;
50
+ providerMessage;
51
+ raw;
52
+ constructor(code2, message, details = {}) {
53
+ super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
54
+ this.name = "ManatError";
55
+ this.code = code2;
56
+ this.provider = details.provider;
57
+ this.providerCode = details.providerCode;
58
+ this.providerMessage = details.providerMessage;
59
+ this.raw = details.raw;
60
+ }
61
+ };
62
+ function isManatError(error, code2) {
63
+ return error instanceof ManatError && (code2 === void 0 || error.code === code2);
64
+ }
65
+
66
+ // src/money.ts
67
+ var MINOR_DIGITS = { AZN: 2, USD: 2, EUR: 2, RUB: 2, TRY: 2, GBP: 2 };
68
+ var NUMERIC_CODES = { AZN: "944", USD: "840", EUR: "978", RUB: "643", TRY: "949", GBP: "826" };
69
+ function assertMoney(money, field = "amount") {
70
+ if (!money || typeof money !== "object") throw new ManatError("invalid_request", `${field} is required`);
71
+ if (!(money.currency in MINOR_DIGITS)) throw new ManatError("invalid_request", `${field}.currency "${money.currency}" is not supported`);
72
+ if (!Number.isSafeInteger(money.amount) || money.amount < 1) {
73
+ throw new ManatError("invalid_request", `${field}.amount must be a positive integer in minor units (2550 = 25.50), got ${money.amount}`);
74
+ }
75
+ }
76
+ function toDecimalString(money) {
77
+ assertMoney(money);
78
+ const digits = MINOR_DIGITS[money.currency];
79
+ const units = money.amount.toString().padStart(digits + 1, "0");
80
+ return `${units.slice(0, -digits)}.${units.slice(-digits)}`;
81
+ }
82
+ function fromDecimal(value, currency) {
83
+ const text = typeof value === "number" ? value.toString() : value.trim();
84
+ const digits = MINOR_DIGITS[currency];
85
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(text);
86
+ if (!match || (match[2]?.length ?? 0) > digits) {
87
+ throw new ManatError("invalid_request", `"${text}" is not a valid ${currency} amount`);
88
+ }
89
+ const fraction = (match[2] ?? "").padEnd(digits, "0");
90
+ return { amount: Number(`${match[1]}${fraction}`), currency };
91
+ }
92
+ function currencyNumericCode(currency) {
93
+ return NUMERIC_CODES[currency];
94
+ }
95
+ function currencyFromNumeric(code2) {
96
+ return Object.keys(NUMERIC_CODES).find((c) => NUMERIC_CODES[c] === code2);
97
+ }
98
+ function azn(value) {
99
+ return fromDecimal(value, "AZN");
100
+ }
101
+
102
+ // src/manat.ts
103
+ var Manat = class {
104
+ constructor(provider) {
105
+ this.provider = provider;
106
+ }
107
+ provider;
108
+ get capabilities() {
109
+ return this.provider.capabilities;
110
+ }
111
+ supports(capability) {
112
+ return this.provider.capabilities.has(capability);
113
+ }
114
+ createPayment(request) {
115
+ if (!request?.orderId || typeof request.orderId !== "string") throw new ManatError("invalid_request", "orderId is required");
116
+ assertMoney(request.amount);
117
+ assertUrl(request.returnUrl, "returnUrl");
118
+ if (request.callbackUrl !== void 0) assertUrl(request.callbackUrl, "callbackUrl");
119
+ return this.provider.createPayment(request);
120
+ }
121
+ verifyCallback(callback) {
122
+ if (!callback || typeof callback.method !== "string") throw new ManatError("invalid_request", "callback.method is required");
123
+ return this.provider.verifyCallback(callback);
124
+ }
125
+ getStatus(input) {
126
+ requirePaymentId(input);
127
+ return this.call("getStatus", this.provider.getStatus, input);
128
+ }
129
+ refund(request) {
130
+ requirePaymentId(request);
131
+ if (request.amount !== void 0) assertMoney(request.amount);
132
+ return this.call("refund", this.provider.refund, request);
133
+ }
134
+ capture(input) {
135
+ requirePaymentId(input);
136
+ if (input.amount !== void 0) assertMoney(input.amount);
137
+ return this.call("capture", this.provider.capture, input);
138
+ }
139
+ cancel(input) {
140
+ requirePaymentId(input);
141
+ return this.call("cancel", this.provider.cancel, input);
142
+ }
143
+ call(name, fn, input) {
144
+ if (!fn || !this.provider.capabilities.has(name)) {
145
+ return Promise.reject(new ManatError("unsupported_operation", `${this.provider.name} does not support ${name}`, { provider: this.provider.name }));
146
+ }
147
+ return fn.call(this.provider, input);
148
+ }
149
+ };
150
+ function requirePaymentId(input) {
151
+ if (!input?.paymentId || typeof input.paymentId !== "string") throw new ManatError("invalid_request", "paymentId is required");
152
+ }
153
+ function assertUrl(value, field) {
154
+ try {
155
+ const url = new URL(String(value));
156
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("scheme");
157
+ } catch {
158
+ throw new ManatError("invalid_request", `${field} must be an absolute http(s) URL`);
159
+ }
160
+ }
161
+
162
+ // src/http.ts
163
+ var HttpClient = class {
164
+ constructor(provider, options = {}) {
165
+ this.provider = provider;
166
+ const impl = options.fetch ?? globalThis.fetch;
167
+ if (!impl) throw new ManatError("invalid_request", "No fetch implementation available; pass options.fetch (Node 18+ has one built in)");
168
+ this.fetchImpl = impl;
169
+ this.timeoutMs = options.timeoutMs ?? 3e4;
170
+ }
171
+ provider;
172
+ fetchImpl;
173
+ timeoutMs;
174
+ async send(request) {
175
+ const headers = { ...request.headers };
176
+ let body;
177
+ if (request.body instanceof URLSearchParams) {
178
+ body = request.body.toString();
179
+ headers["content-type"] ??= "application/x-www-form-urlencoded";
180
+ } else if (typeof request.body === "string") {
181
+ body = request.body;
182
+ } else if (request.body !== void 0) {
183
+ body = JSON.stringify(request.body);
184
+ headers["content-type"] ??= "application/json";
185
+ }
186
+ const controller = new AbortController();
187
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
188
+ try {
189
+ const init = { method: request.method, headers, signal: controller.signal };
190
+ if (body !== void 0) init.body = body;
191
+ const response = await this.fetchImpl(request.url, init);
192
+ return { status: response.status, headers: response.headers, text: await response.text() };
193
+ } catch (error) {
194
+ const reason = controller.signal.aborted ? `timed out after ${this.timeoutMs} ms` : error?.message ?? "request failed";
195
+ throw new ManatError("network_error", `${this.provider}: ${request.method} ${request.url} ${reason}`, { provider: this.provider, cause: error });
196
+ } finally {
197
+ clearTimeout(timer);
198
+ }
199
+ }
200
+ async json(request) {
201
+ const response = await this.send({ ...request, headers: { accept: "application/json", ...request.headers } });
202
+ try {
203
+ return { status: response.status, data: response.text ? JSON.parse(response.text) : null };
204
+ } catch (error) {
205
+ throw new ManatError("network_error", `${this.provider}: expected JSON, got HTTP ${response.status} "${response.text.slice(0, 200)}"`, {
206
+ provider: this.provider,
207
+ raw: response.text,
208
+ cause: error
209
+ });
210
+ }
211
+ }
212
+ };
213
+
214
+ // src/providers/kapitalbank.ts
215
+ var BASE_URLS = {
216
+ test: "https://txpgtst.kapitalbank.az/api",
217
+ production: "https://e-commerce.kapitalbank.az/api"
218
+ };
219
+ var CAPABILITIES = /* @__PURE__ */ new Set(["createPayment", "verifyCallback", "getStatus", "refund", "capture", "cancel"]);
220
+ function mapKapitalStatus(order) {
221
+ switch (order.status) {
222
+ case "Preparing":
223
+ return "pending";
224
+ case "Authorized":
225
+ return "authorized";
226
+ case "PartPaid":
227
+ case "FullyPaid":
228
+ case "Funded":
229
+ case "Closed": {
230
+ const refunded = Number(order.clearedRefundAmount ?? 0);
231
+ if (refunded > 0) return refunded >= Number(order.clearedChargeAmount ?? Infinity) ? "refunded" : "partially_refunded";
232
+ return "paid";
233
+ }
234
+ case "Refunded":
235
+ return "refunded";
236
+ case "Cancelled":
237
+ case "Voided":
238
+ case "Expired":
239
+ return "cancelled";
240
+ case "Rejected":
241
+ case "Refused":
242
+ case "Declined":
243
+ return "failed";
244
+ default:
245
+ throw new ManatError("provider_error", `kapitalbank: unknown order status "${order.status}"`, { provider: "kapitalbank", raw: order });
246
+ }
247
+ }
248
+ var KapitalBankProvider = class {
249
+ name = "kapitalbank";
250
+ capabilities = CAPABILITIES;
251
+ http;
252
+ baseUrl;
253
+ authHeader;
254
+ constructor(config, options = {}) {
255
+ if (!config?.username || !config.password) throw new ManatError("invalid_request", "kapitalbank: username and password are required");
256
+ this.baseUrl = (config.baseUrl ?? BASE_URLS[config.environment ?? "production"]).replace(/\/+$/, "");
257
+ this.authHeader = `Basic ${Buffer.from(`${config.username}:${config.password}`, "utf8").toString("base64")}`;
258
+ this.http = new HttpClient(this.name, options);
259
+ }
260
+ async createPayment(request) {
261
+ const order = {
262
+ typeRid: request.intent === "authorize" ? "Order_DMS" : "Order_SMS",
263
+ amount: toDecimalString(request.amount),
264
+ currency: request.amount.currency,
265
+ language: request.language ?? "az",
266
+ description: request.description ?? request.orderId,
267
+ hppRedirectUrl: request.returnUrl
268
+ };
269
+ const data = await this.call("POST", "/order", { order });
270
+ const created = data.order;
271
+ if (!created?.id || !created.hppUrl || !created.password) {
272
+ throw new ManatError("provider_error", "kapitalbank: order response is missing id, hppUrl or password", { provider: this.name, raw: data });
273
+ }
274
+ const url = new URL(created.hppUrl);
275
+ url.searchParams.set("id", String(created.id));
276
+ url.searchParams.set("password", created.password);
277
+ return {
278
+ paymentId: String(created.id),
279
+ status: created.status ? mapKapitalStatus(created) : "pending",
280
+ next: { type: "redirect", url: url.toString() },
281
+ raw: data
282
+ };
283
+ }
284
+ async verifyCallback(callback) {
285
+ const id = pick(callback.query, "ID") ?? pick(callback.query, "id") ?? pickBody(callback.body, "ID");
286
+ if (!id || !/^\d+$/.test(id)) {
287
+ throw new ManatError("invalid_signature", "kapitalbank: callback has no numeric ID", { provider: this.name, raw: callback });
288
+ }
289
+ const state = await this.getStatus({ paymentId: id });
290
+ const event = { paymentId: state.paymentId, status: state.status, raw: state.raw };
291
+ if (state.amount) event.amount = state.amount;
292
+ return event;
293
+ }
294
+ async getStatus({ paymentId }) {
295
+ const order = await this.fetchOrder(paymentId);
296
+ return this.toState(order);
297
+ }
298
+ async refund(request) {
299
+ const amount = request.amount ?? await this.remainingRefundable(request.paymentId);
300
+ const data = await this.exec(request.paymentId, { phase: "Single", amount: toDecimalString(amount), type: "Refund" });
301
+ const state = await this.getStatus({ paymentId: request.paymentId });
302
+ const result = { paymentId: request.paymentId, status: state.status, raw: { exec: data, order: state.raw } };
303
+ const actionId = data.tran?.match?.tranActionId;
304
+ if (actionId) result.refundId = actionId;
305
+ return result;
306
+ }
307
+ async capture({ paymentId, amount }) {
308
+ const tran = { phase: "Clearing" };
309
+ tran.amount = toDecimalString(amount ?? (await this.getStatus({ paymentId })).amount ?? missingAmount(paymentId));
310
+ await this.exec(paymentId, tran);
311
+ return this.getStatus({ paymentId });
312
+ }
313
+ async cancel({ paymentId }) {
314
+ const current = await this.getStatus({ paymentId });
315
+ const phase = current.status === "authorized" ? "Auth" : "Single";
316
+ await this.exec(paymentId, { phase, voidKind: "Full" });
317
+ return this.getStatus({ paymentId });
318
+ }
319
+ async remainingRefundable(paymentId) {
320
+ const order = await this.fetchOrder(paymentId);
321
+ const currency = toCurrency(order.currency, paymentId);
322
+ const charged = fromDecimal(String(order.clearedChargeAmount ?? order.amount ?? 0), currency).amount;
323
+ const refunded = order.clearedRefundAmount ? fromDecimal(String(order.clearedRefundAmount), currency).amount : 0;
324
+ const remaining = charged - refunded;
325
+ if (remaining <= 0) throw new ManatError("invalid_request", `kapitalbank: order ${paymentId} has nothing left to refund`, { provider: this.name, raw: order });
326
+ return { amount: remaining, currency };
327
+ }
328
+ async fetchOrder(paymentId) {
329
+ assertOrderId(paymentId);
330
+ const data = await this.call("GET", `/order/${paymentId}?tranDetailLevel=2&tokenDetailLevel=2&orderDetailLevel=2`);
331
+ if (!data?.order) throw new ManatError("provider_error", `kapitalbank: no order in response for ${paymentId}`, { provider: this.name, raw: data });
332
+ return { ...data.order, id: data.order.id ?? paymentId };
333
+ }
334
+ toState(order) {
335
+ const state = { paymentId: String(order.id), status: mapKapitalStatus(order), raw: order };
336
+ if (order.amount !== void 0 && order.currency) {
337
+ state.amount = fromDecimal(String(order.amount), toCurrency(order.currency, String(order.id)));
338
+ }
339
+ if (order.clearedRefundAmount && order.currency) {
340
+ state.refundedAmount = fromDecimal(String(order.clearedRefundAmount), toCurrency(order.currency, String(order.id)));
341
+ }
342
+ return state;
343
+ }
344
+ exec(paymentId, tran) {
345
+ assertOrderId(paymentId);
346
+ return this.call("POST", `/order/${paymentId}/exec-tran`, { tran });
347
+ }
348
+ async call(method, path, body) {
349
+ const { status, data } = await this.http.json({
350
+ method,
351
+ url: `${this.baseUrl}${path}`,
352
+ headers: { authorization: this.authHeader },
353
+ body
354
+ });
355
+ if (status >= 400 || data && typeof data === "object" && "errorCode" in data && data.errorCode) {
356
+ const err = data ?? {};
357
+ throw new ManatError("provider_error", `kapitalbank: ${err.errorDescription ?? `HTTP ${status}`}`, {
358
+ provider: this.name,
359
+ ...err.errorCode ? { providerCode: err.errorCode } : {},
360
+ ...err.errorDescription ? { providerMessage: err.errorDescription } : {},
361
+ raw: data
362
+ });
363
+ }
364
+ return data;
365
+ }
366
+ };
367
+ function kapitalBank(config, options) {
368
+ return new KapitalBankProvider(config, options);
369
+ }
370
+ function assertOrderId(paymentId) {
371
+ if (!/^\d+$/.test(paymentId)) throw new ManatError("invalid_request", `kapitalbank: paymentId must be the numeric order id, got "${paymentId}"`);
372
+ }
373
+ function toCurrency(code2, paymentId) {
374
+ if (code2 === "AZN" || code2 === "USD" || code2 === "EUR" || code2 === "RUB" || code2 === "TRY" || code2 === "GBP") return code2;
375
+ throw new ManatError("provider_error", `kapitalbank: unexpected currency "${code2}" on order ${paymentId}`, { provider: "kapitalbank" });
376
+ }
377
+ function missingAmount(paymentId) {
378
+ throw new ManatError("provider_error", `kapitalbank: order ${paymentId} has no amount to capture`, { provider: "kapitalbank" });
379
+ }
380
+ function pick(source, key) {
381
+ const value = source?.[key];
382
+ return Array.isArray(value) ? value[0] : value;
383
+ }
384
+ function pickBody(body, key) {
385
+ if (!body) return void 0;
386
+ if (typeof body === "string") return new URLSearchParams(body).get(key) ?? void 0;
387
+ const value = body[key];
388
+ return typeof value === "string" ? value : void 0;
389
+ }
390
+
391
+ // src/providers/azericard.ts
392
+ var import_node_crypto = require("crypto");
393
+ var GATEWAY_URLS = {
394
+ test: "https://testmpi.3dsecure.az/cgi-bin/cgi_link",
395
+ production: "https://mpi.3dsecure.az/cgi-bin/cgi_link"
396
+ };
397
+ var AZERICARD_MAC_FIELDS = {
398
+ purchase: ["AMOUNT", "CURRENCY", "TERMINAL", "TRTYPE", "TIMESTAMP", "NONCE", "MERCH_URL"],
399
+ completion: ["AMOUNT", "CURRENCY", "TERMINAL", "TRTYPE", "ORDER", "RRN", "INT_REF"],
400
+ reversal: ["AMOUNT", "CURRENCY", "TERMINAL", "TRTYPE", "ORDER", "RRN", "INT_REF"],
401
+ callback: ["AMOUNT", "TERMINAL", "APPROVAL", "RRN", "INT_REF"],
402
+ status: ["ORDER", "TERMINAL", "TRTYPE", "TIMESTAMP", "NONCE"]
403
+ };
404
+ var ACTION_MESSAGES = {
405
+ "0": "Approved",
406
+ "1": "Duplicate transaction",
407
+ "2": "Transaction declined",
408
+ "3": "Transaction processing error",
409
+ "6": "Repeat of a declined transaction",
410
+ "7": "Repeat of a transaction with an authentication error",
411
+ "8": "Repeat of a transaction that terminated without a response"
412
+ };
413
+ var CAPABILITIES2 = /* @__PURE__ */ new Set(["createPayment", "verifyCallback", "getStatus", "refund", "capture", "cancel"]);
414
+ var DAY_MS = 24 * 60 * 60 * 1e3;
415
+ function azericardMacSource(fields, order) {
416
+ return order.map((name) => {
417
+ const value = fields[name] ?? "";
418
+ return value === "" ? "-" : `${Buffer.byteLength(value, "utf8")}${value}`;
419
+ }).join("");
420
+ }
421
+ function azericardTimestamp(date) {
422
+ return date.toISOString().replace(/[-:T]/g, "").slice(0, 14);
423
+ }
424
+ var AzericardProvider = class {
425
+ constructor(config, options = {}) {
426
+ this.config = config;
427
+ if (!config?.terminal) throw new ManatError("invalid_request", "azericard: terminal is required");
428
+ if (!config.merchantName || !config.merchantUrl) throw new ManatError("invalid_request", "azericard: merchantName and merchantUrl are required");
429
+ if (!config.privateKey) throw new ManatError("invalid_request", "azericard: privateKey (PEM) is required");
430
+ this.privateKey = loadKey(() => (0, import_node_crypto.createPrivateKey)(config.privateKey), "privateKey");
431
+ assertRsa2048(this.privateKey, "privateKey");
432
+ this.gatewayKey = config.gatewayPublicKey ? loadKey(() => (0, import_node_crypto.createPublicKey)(config.gatewayPublicKey), "gatewayPublicKey") : void 0;
433
+ this.url = config.gatewayUrl ?? GATEWAY_URLS[config.environment ?? "production"];
434
+ this.macFields = { ...AZERICARD_MAC_FIELDS, ...config.macFields };
435
+ this.clock = config.clock ?? (() => /* @__PURE__ */ new Date());
436
+ this.makeNonce = config.nonce ?? (() => (0, import_node_crypto.randomBytes)(16).toString("hex"));
437
+ this.http = new HttpClient(this.name, options);
438
+ }
439
+ config;
440
+ name = "azericard";
441
+ capabilities = CAPABILITIES2;
442
+ http;
443
+ url;
444
+ privateKey;
445
+ gatewayKey;
446
+ macFields;
447
+ clock;
448
+ makeNonce;
449
+ async createPayment(request) {
450
+ if (!/^\d{6,32}$/.test(request.orderId)) {
451
+ throw new ManatError(
452
+ "invalid_request",
453
+ `azericard: orderId must be 6\u201332 digits (got "${request.orderId}"); its last 6 digits must be unique per terminal per day`
454
+ );
455
+ }
456
+ const fields = compact({
457
+ AMOUNT: toDecimalString(request.amount),
458
+ CURRENCY: request.amount.currency,
459
+ ORDER: request.orderId,
460
+ DESC: truncate(request.description ?? `Order ${request.orderId}`, 50),
461
+ MERCH_NAME: truncate(this.config.merchantName, 50),
462
+ MERCH_URL: this.config.merchantUrl,
463
+ TERMINAL: this.config.terminal,
464
+ EMAIL: this.config.email,
465
+ TRTYPE: request.intent === "authorize" ? "0" : "1",
466
+ COUNTRY: this.config.country,
467
+ MERCH_GMT: this.config.merchantGmt,
468
+ TIMESTAMP: azericardTimestamp(this.clock()),
469
+ NONCE: this.makeNonce(),
470
+ BACKREF: request.callbackUrl ?? request.returnUrl,
471
+ LANG: request.language ?? "az"
472
+ });
473
+ fields.P_SIGN = this.sign(fields, "purchase");
474
+ return {
475
+ paymentId: request.orderId,
476
+ status: "pending",
477
+ next: { type: "form", url: this.url, method: "POST", fields },
478
+ raw: { fields }
479
+ };
480
+ }
481
+ async verifyCallback(callback) {
482
+ if (!this.gatewayKey) {
483
+ throw new ManatError("invalid_request", "azericard: gatewayPublicKey is required to verify callbacks; never accept them unverified");
484
+ }
485
+ const fields = readFields(callback);
486
+ const signature = fields.P_SIGN;
487
+ if (!signature || !/^[0-9a-fA-F]+$/.test(signature) || signature.length % 2 !== 0) {
488
+ throw new ManatError("invalid_signature", "azericard: callback has no valid P_SIGN", { provider: this.name, raw: fields });
489
+ }
490
+ const source = azericardMacSource(fields, this.macFields.callback);
491
+ if (!(0, import_node_crypto.verify)("sha256", Buffer.from(source, "utf8"), this.gatewayKey, Buffer.from(signature, "hex"))) {
492
+ throw new ManatError("invalid_signature", "azericard: callback P_SIGN does not match", { provider: this.name, raw: fields });
493
+ }
494
+ if (fields.TERMINAL !== this.config.terminal) {
495
+ throw new ManatError("invalid_signature", `azericard: callback is for terminal ${fields.TERMINAL}, not ${this.config.terminal}`, { provider: this.name, raw: fields });
496
+ }
497
+ if (!fields.ORDER) throw new ManatError("invalid_signature", "azericard: callback has no ORDER", { provider: this.name, raw: fields });
498
+ const amount = parseAmount(fields.AMOUNT, fields.CURRENCY);
499
+ const approved = fields.ACTION === "0" && !!fields.APPROVAL;
500
+ const event = { paymentId: fields.ORDER, orderId: fields.ORDER, status: approved ? statusForTrtype(fields.TRTYPE) : "failed", raw: fields };
501
+ if (amount) event.amount = amount;
502
+ if (approved && fields.RRN && fields.INT_REF && amount && fields.TIMESTAMP) {
503
+ event.providerRef = encodeRef({ rrn: fields.RRN, intRef: fields.INT_REF, amount, timestamp: fields.TIMESTAMP, trtype: fields.TRTYPE ?? "1" });
504
+ }
505
+ return event;
506
+ }
507
+ /** TRTYPE 90 answers for one transaction type, so the purchase (or pre-auth), completion and reversals are each queried. */
508
+ async getStatus({ paymentId }) {
509
+ if (!/^\d{6,32}$/.test(paymentId)) throw new ManatError("invalid_request", `azericard: paymentId must be the numeric ORDER, got "${paymentId}"`);
510
+ let trtype = "1";
511
+ let data = await this.queryStatus(paymentId, trtype);
512
+ if (data.responseCode === "-24") {
513
+ trtype = "0";
514
+ data = await this.queryStatus(paymentId, trtype);
515
+ }
516
+ let status = mapStatusResponse(data, trtype);
517
+ const amount = parseAmount(data.amount, data.currency);
518
+ const state = { paymentId, status, raw: { [trtype]: data } };
519
+ if (amount) state.amount = amount;
520
+ if ((status === "paid" || status === "authorized") && amount) {
521
+ const related = await this.relatedTransactions(paymentId, trtype === "0" ? ["21", "22", "24"] : ["22", "24"]);
522
+ state.raw = { [trtype]: data, ...related.raw };
523
+ if (status === "authorized" && related.approved.has("21")) status = "paid";
524
+ const refunded = related.approvedAmount("22", amount.currency) + related.approvedAmount("24", amount.currency);
525
+ if (refunded > 0) {
526
+ state.refundedAmount = { amount: refunded, currency: amount.currency };
527
+ if (status === "authorized") status = "cancelled";
528
+ else status = refunded >= amount.amount ? "refunded" : "partially_refunded";
529
+ }
530
+ state.status = status;
531
+ const paidAt = data.tranDate ? bakuTimeToGmt(data.tranDate) : data.timestamp;
532
+ if (data.rrn && data.intRef && paidAt) {
533
+ state.providerRef = encodeRef({ rrn: data.rrn, intRef: data.intRef, amount, timestamp: paidAt, trtype: related.approved.has("21") ? "21" : trtype });
534
+ }
535
+ }
536
+ return state;
537
+ }
538
+ async relatedTransactions(order, types) {
539
+ const raw = {};
540
+ const approved = /* @__PURE__ */ new Map();
541
+ for (const type of types) {
542
+ const data = await this.queryStatus(order, type);
543
+ raw[type] = data;
544
+ if (data.actionCode === "0" && data.responseCode === "00") approved.set(type, data);
545
+ }
546
+ return {
547
+ raw,
548
+ approved,
549
+ approvedAmount(type, currency) {
550
+ const entry = approved.get(type);
551
+ const money = entry ? parseAmount(entry.amount, entry.currency ?? currency) : void 0;
552
+ return money && money.currency === currency ? money.amount : 0;
553
+ }
554
+ };
555
+ }
556
+ async queryStatus(order, tranTrtype) {
557
+ const fields = {
558
+ TRAN_TRTYPE: tranTrtype,
559
+ ORDER: order,
560
+ TERMINAL: this.config.terminal,
561
+ TRTYPE: "90",
562
+ TIMESTAMP: azericardTimestamp(this.clock()),
563
+ NONCE: this.makeNonce()
564
+ };
565
+ fields.P_SIGN = this.sign(fields, "status");
566
+ const { status, data } = await this.http.json({ method: "POST", url: this.url, body: new URLSearchParams(fields) });
567
+ if (status >= 400 || !data || typeof data !== "object") {
568
+ throw new ManatError("provider_error", `azericard: status request failed (HTTP ${status})`, { provider: this.name, raw: data });
569
+ }
570
+ return data;
571
+ }
572
+ async capture(input) {
573
+ const ref = requireRef(input.providerRef);
574
+ const amount = input.amount ?? ref.amount;
575
+ await this.send("completion", "21", input.paymentId, ref, amount);
576
+ return { paymentId: input.paymentId, status: "paid", amount, providerRef: encodeRef({ ...ref, amount, trtype: "21" }), raw: null };
577
+ }
578
+ async cancel(input) {
579
+ const ref = requireRef(input.providerRef);
580
+ await this.send("reversal", "22", input.paymentId, ref, ref.amount);
581
+ return { paymentId: input.paymentId, status: "cancelled", amount: ref.amount, raw: null };
582
+ }
583
+ async refund(request) {
584
+ const ref = requireRef(request.providerRef);
585
+ const amount = request.amount ?? ref.amount;
586
+ if (amount.currency !== ref.amount.currency || amount.amount > ref.amount.amount) {
587
+ throw new ManatError("invalid_request", "azericard: refund amount exceeds the paid amount or uses another currency");
588
+ }
589
+ const paidAt = parseTimestamp(ref.timestamp);
590
+ let trtype = "24";
591
+ if (paidAt && this.clock().getTime() - paidAt.getTime() < DAY_MS) {
592
+ const online = await this.queryStatus(request.paymentId, "22");
593
+ trtype = online.actionCode === "0" && online.responseCode === "00" ? "24" : "22";
594
+ }
595
+ const text = await this.send("reversal", trtype, request.paymentId, ref, amount);
596
+ return { paymentId: request.paymentId, status: amount.amount < ref.amount.amount ? "partially_refunded" : "refunded", raw: { trtype, action: text } };
597
+ }
598
+ async send(message, trtype, order, ref, amount) {
599
+ const fields = {
600
+ AMOUNT: toDecimalString(amount),
601
+ CURRENCY: amount.currency,
602
+ ORDER: order,
603
+ RRN: ref.rrn,
604
+ INT_REF: ref.intRef,
605
+ TERMINAL: this.config.terminal,
606
+ TRTYPE: trtype,
607
+ TIMESTAMP: azericardTimestamp(this.clock()),
608
+ NONCE: this.makeNonce()
609
+ };
610
+ fields.P_SIGN = this.sign(fields, message);
611
+ const response = await this.http.send({ method: "POST", url: this.url, body: new URLSearchParams(fields) });
612
+ const action = response.text.trim();
613
+ if (response.status >= 400) {
614
+ throw new ManatError("provider_error", `azericard: HTTP ${response.status}`, { provider: this.name, raw: response.text });
615
+ }
616
+ if (action !== "0") {
617
+ const message2 = ACTION_MESSAGES[action] ?? `Unexpected response "${action.slice(0, 100)}"`;
618
+ throw new ManatError("provider_error", `azericard: ${message2}`, { provider: this.name, providerCode: action.slice(0, 20), providerMessage: message2, raw: response.text });
619
+ }
620
+ return action;
621
+ }
622
+ sign(fields, message) {
623
+ const source = azericardMacSource(fields, this.macFields[message]);
624
+ return (0, import_node_crypto.sign)("sha256", Buffer.from(source, "utf8"), this.privateKey).toString("hex");
625
+ }
626
+ };
627
+ function azericard(config, options) {
628
+ return new AzericardProvider(config, options);
629
+ }
630
+ function mapStatusResponse(data, tranTrtype) {
631
+ if (data.actionCode === "0" && data.responseCode === "00") return tranTrtype === "0" ? "authorized" : "paid";
632
+ if (data.responseCode === "-40") return "pending";
633
+ return "failed";
634
+ }
635
+ function bakuTimeToGmt(value) {
636
+ const m = /^(\d{4})\.(\d{2})\.(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(value);
637
+ if (!m) return void 0;
638
+ return azericardTimestamp(new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4] - 4, +m[5], +m[6])));
639
+ }
640
+ function statusForTrtype(trtype) {
641
+ switch (trtype) {
642
+ case "0":
643
+ return "authorized";
644
+ case "22":
645
+ case "24":
646
+ return "refunded";
647
+ default:
648
+ return "paid";
649
+ }
650
+ }
651
+ function encodeRef(ref) {
652
+ return ["az1", ref.rrn, ref.intRef, toDecimalString(ref.amount), ref.amount.currency, ref.timestamp, ref.trtype].join("|");
653
+ }
654
+ function requireRef(value) {
655
+ const parts = value?.split("|") ?? [];
656
+ if (parts.length !== 7 || parts[0] !== "az1" || !parts[1] || !parts[2]) {
657
+ throw new ManatError("invalid_request", "azericard: providerRef from the payment callback is required (store event.providerRef with the order)");
658
+ }
659
+ const [, rrn, intRef, amount, currency, timestamp, trtype] = parts;
660
+ return { rrn, intRef, amount: fromDecimal(amount, toCurrency2(currency)), timestamp, trtype };
661
+ }
662
+ function parseAmount(amount, currency) {
663
+ if (!amount || !currency) return void 0;
664
+ const alpha = /^\d{3}$/.test(currency) ? currencyFromNumeric(currency) : currency;
665
+ return alpha ? fromDecimal(amount, toCurrency2(alpha)) : void 0;
666
+ }
667
+ function toCurrency2(code2) {
668
+ if (code2 === "AZN" || code2 === "USD" || code2 === "EUR" || code2 === "RUB" || code2 === "TRY" || code2 === "GBP") return code2;
669
+ throw new ManatError("provider_error", `azericard: unsupported currency "${code2}"`, { provider: "azericard" });
670
+ }
671
+ function parseTimestamp(value) {
672
+ const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(value);
673
+ return m ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])) : void 0;
674
+ }
675
+ function readFields(callback) {
676
+ const out = {};
677
+ const add = (key, value) => {
678
+ const v = Array.isArray(value) ? value[0] : value;
679
+ if (typeof v === "string") out[key.toUpperCase()] = v.trim();
680
+ };
681
+ for (const [k, v] of Object.entries(callback.query ?? {})) add(k, v);
682
+ if (typeof callback.body === "string") new URLSearchParams(callback.body).forEach((v, k) => add(k, v));
683
+ else for (const [k, v] of Object.entries(callback.body ?? {})) add(k, v);
684
+ return out;
685
+ }
686
+ function compact(fields) {
687
+ return Object.fromEntries(Object.entries(fields).filter((entry) => entry[1] !== void 0 && entry[1] !== ""));
688
+ }
689
+ function truncate(value, max) {
690
+ const chars = Array.from(value);
691
+ return chars.length > max ? chars.slice(0, max).join("") : value;
692
+ }
693
+ function loadKey(load, name) {
694
+ try {
695
+ return load();
696
+ } catch (error) {
697
+ throw new ManatError("invalid_request", `azericard: ${name} is not a valid PEM key`, { cause: error });
698
+ }
699
+ }
700
+ function assertRsa2048(key, name) {
701
+ const bits = key.asymmetricKeyDetails?.modulusLength ?? 0;
702
+ if (key.asymmetricKeyType !== "rsa" || bits < 2048) throw new ManatError("invalid_request", `azericard: ${name} must be an RSA key of at least 2048 bits`);
703
+ }
704
+
705
+ // src/providers/payriff.ts
706
+ var DEFAULT_BASE_URL = "https://api.payriff.com";
707
+ var SUCCESS = "00000";
708
+ var CAPABILITIES3 = /* @__PURE__ */ new Set(["createPayment", "verifyCallback", "getStatus", "refund", "capture"]);
709
+ function mapPayriffStatus(status) {
710
+ switch (status) {
711
+ case "CREATED":
712
+ case "PENDING":
713
+ case "ACCEPTED":
714
+ case "IN_REVIEW":
715
+ return "pending";
716
+ case "APPROVED":
717
+ case "PAID":
718
+ case "PARTIAL":
719
+ // pre-auth partially captured
720
+ case "CASH":
721
+ return "paid";
722
+ case "PREAUTH_APPROVED":
723
+ return "authorized";
724
+ case "REFUNDED":
725
+ case "REFUND_IN_PROGRESS":
726
+ return "refunded";
727
+ case "PARTIAL_REFUND":
728
+ return "partially_refunded";
729
+ case "CANCELED":
730
+ case "EXPIRED":
731
+ case "REVERSE":
732
+ case "PREAUTH_EXPIRED":
733
+ return "cancelled";
734
+ case "DECLINED":
735
+ return "failed";
736
+ default:
737
+ throw new ManatError("provider_error", `payriff: unknown paymentStatus "${status}"`, { provider: "payriff" });
738
+ }
739
+ }
740
+ var PayriffProvider = class {
741
+ constructor(config, options = {}) {
742
+ this.config = config;
743
+ if (!config?.secretKey) throw new ManatError("invalid_request", "payriff: secretKey is required");
744
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
745
+ this.http = new HttpClient(this.name, options);
746
+ }
747
+ config;
748
+ name = "payriff";
749
+ capabilities = CAPABILITIES3;
750
+ http;
751
+ baseUrl;
752
+ async createPayment(request) {
753
+ if (request.callbackUrl !== void 0 && !request.callbackUrl.startsWith("https://")) {
754
+ throw new ManatError("invalid_request", "payriff: callbackUrl must be an HTTPS URL");
755
+ }
756
+ const body = {
757
+ amount: Number(toDecimalString(request.amount)),
758
+ currency: request.amount.currency,
759
+ language: (request.language ?? "az").toUpperCase(),
760
+ operation: request.intent === "authorize" ? "PRE_AUTH" : "PURCHASE",
761
+ description: request.description ?? `Order ${request.orderId}`,
762
+ redirectUrl: request.returnUrl
763
+ };
764
+ if (request.callbackUrl) body.callbackUrl = request.callbackUrl;
765
+ if (request.metadata && Object.keys(request.metadata).length) body.metadata = request.metadata;
766
+ const payload = await this.call("POST", "/api/v3/orders", body);
767
+ if (!payload?.orderId || !payload.paymentUrl) {
768
+ throw new ManatError("provider_error", "payriff: order response has no orderId or paymentUrl", { provider: this.name, raw: payload });
769
+ }
770
+ return { paymentId: payload.orderId, status: "pending", next: { type: "redirect", url: payload.paymentUrl }, raw: payload };
771
+ }
772
+ async verifyCallback(callback) {
773
+ const orderId = callbackOrderId(callback);
774
+ if (!orderId) throw new ManatError("invalid_signature", "payriff: callback has no payload.orderId", { provider: this.name, raw: callback.body });
775
+ const state = await this.getStatus({ paymentId: orderId });
776
+ const event = { paymentId: state.paymentId, status: state.status, raw: state.raw };
777
+ if (state.amount) event.amount = state.amount;
778
+ return event;
779
+ }
780
+ async getStatus({ paymentId }) {
781
+ const order = await this.call("GET", `/api/v3/orders/${encodeId(paymentId)}`);
782
+ if (!order?.orderId) throw new ManatError("provider_error", `payriff: no order in response for ${paymentId}`, { provider: this.name, raw: order });
783
+ const state = { paymentId: order.orderId, status: orderStatus(order), raw: order };
784
+ if (order.amount !== void 0 && order.currencyType) state.amount = fromDecimal(String(order.amount), toCurrency3(order.currencyType));
785
+ return state;
786
+ }
787
+ async refund(request) {
788
+ const amount = request.amount ?? (await this.getStatus({ paymentId: request.paymentId })).amount;
789
+ if (!amount) throw new ManatError("provider_error", `payriff: order ${request.paymentId} has no amount to refund`, { provider: this.name });
790
+ const body = { orderId: request.paymentId, refundReason: "Refund", amount: Number(toDecimalString(amount)) };
791
+ const envelope = await this.callEnvelope("POST", "/api/v3/refund", body);
792
+ const state = await this.getStatus({ paymentId: request.paymentId });
793
+ const result = { paymentId: request.paymentId, status: state.status, raw: { refund: envelope, order: state.raw } };
794
+ if (envelope.responseId) result.refundId = envelope.responseId;
795
+ return result;
796
+ }
797
+ async capture({ paymentId, amount }) {
798
+ const body = { orderId: paymentId };
799
+ if (amount) body.amount = Number(toDecimalString(amount));
800
+ await this.call("POST", "/api/v3/complete", body);
801
+ return this.getStatus({ paymentId });
802
+ }
803
+ async call(method, path, body) {
804
+ const envelope = await this.callEnvelope(method, path, body);
805
+ return envelope.payload;
806
+ }
807
+ async callEnvelope(method, path, body) {
808
+ const { status, data } = await this.http.json({
809
+ method,
810
+ url: `${this.baseUrl}${path}`,
811
+ headers: { authorization: this.config.secretKey },
812
+ body
813
+ });
814
+ if (!data || typeof data !== "object" || !("code" in data)) {
815
+ throw new ManatError("provider_error", `payriff: unexpected response (HTTP ${status})`, { provider: this.name, raw: data });
816
+ }
817
+ if (status >= 400 || data.code !== SUCCESS) {
818
+ throw new ManatError("provider_error", `payriff: ${data.message ?? `code ${data.code}`}`, {
819
+ provider: this.name,
820
+ ...data.code ? { providerCode: data.code } : {},
821
+ ...data.message ? { providerMessage: data.message } : {},
822
+ raw: data
823
+ });
824
+ }
825
+ return data;
826
+ }
827
+ };
828
+ function payriff(config, options) {
829
+ return new PayriffProvider(config, options);
830
+ }
831
+ function orderStatus(order) {
832
+ const status = mapPayriffStatus(order.paymentStatus);
833
+ if (status === "refunded" && order.transactions?.some((transaction) => transaction.status === "PARTIAL_REFUND")) return "partially_refunded";
834
+ return status;
835
+ }
836
+ function callbackOrderId(callback) {
837
+ let body = callback.body;
838
+ if (typeof body === "string") {
839
+ try {
840
+ body = JSON.parse(body);
841
+ } catch {
842
+ body = void 0;
843
+ }
844
+ }
845
+ const payload = body && typeof body === "object" ? body.payload : void 0;
846
+ const fromBody = payload && typeof payload.orderId === "string" ? payload.orderId : void 0;
847
+ const query = callback.query?.orderId;
848
+ return fromBody ?? (typeof query === "string" ? query : void 0);
849
+ }
850
+ function encodeId(paymentId) {
851
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(paymentId)) throw new ManatError("invalid_request", `payriff: invalid paymentId "${paymentId}"`);
852
+ return paymentId;
853
+ }
854
+ function toCurrency3(code2) {
855
+ if (code2 === "AZN" || code2 === "USD" || code2 === "EUR" || code2 === "RUB" || code2 === "TRY" || code2 === "GBP") return code2;
856
+ throw new ManatError("provider_error", `payriff: unsupported currency "${code2}"`, { provider: "payriff" });
857
+ }
858
+
859
+ // src/crypto.ts
860
+ var import_node_crypto2 = require("crypto");
861
+ function hash(algorithm, data, encoding = "hex") {
862
+ return (0, import_node_crypto2.createHash)(algorithm).update(data).digest(encoding);
863
+ }
864
+ function safeEqual(actual, expected, options = {}) {
865
+ if (typeof actual !== "string") return false;
866
+ const a = Buffer.from(options.caseInsensitive ? actual.toLowerCase() : actual, "utf8");
867
+ const b = Buffer.from(options.caseInsensitive ? expected.toLowerCase() : expected, "utf8");
868
+ if (a.length !== b.length) return false;
869
+ return (0, import_node_crypto2.timingSafeEqual)(a, b);
870
+ }
871
+
872
+ // src/providers/epoint.ts
873
+ var DEFAULT_BASE_URL2 = "https://epoint.az/api/1";
874
+ var CAPABILITIES4 = /* @__PURE__ */ new Set(["createPayment", "verifyCallback", "getStatus", "refund"]);
875
+ var CURRENCIES = /* @__PURE__ */ new Set(["AZN", "USD", "EUR", "RUB"]);
876
+ function epointSignature(privateKey, data) {
877
+ return hash("sha1", privateKey + data + privateKey, "base64");
878
+ }
879
+ function epointEncode(privateKey, json) {
880
+ const data = Buffer.from(json, "utf8").toString("base64");
881
+ return { data, signature: epointSignature(privateKey, data) };
882
+ }
883
+ function mapEpointStatus(status) {
884
+ switch (status) {
885
+ case "new":
886
+ return "pending";
887
+ case "success":
888
+ return "paid";
889
+ case "returned":
890
+ return "refunded";
891
+ case "failed":
892
+ case "error":
893
+ return "failed";
894
+ default:
895
+ throw new ManatError("provider_error", `epoint: unknown status "${status}"`, { provider: "epoint" });
896
+ }
897
+ }
898
+ var EpointProvider = class {
899
+ constructor(config, options = {}) {
900
+ this.config = config;
901
+ if (!config?.publicKey) throw new ManatError("invalid_request", "epoint: publicKey is required");
902
+ if (!config.privateKey) throw new ManatError("invalid_request", "epoint: privateKey is required");
903
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
904
+ this.http = new HttpClient(this.name, options);
905
+ }
906
+ config;
907
+ name = "epoint";
908
+ capabilities = CAPABILITIES4;
909
+ http;
910
+ baseUrl;
911
+ async createPayment(request) {
912
+ if (request.intent === "authorize") {
913
+ throw new ManatError("unsupported_operation", "epoint: pre-authorisation is not supported (Epoint statuses do not tell a hold from a capture)", { provider: this.name });
914
+ }
915
+ if (!CURRENCIES.has(request.amount.currency)) {
916
+ throw new ManatError("invalid_request", `epoint: currency ${request.amount.currency} is not supported (AZN, USD, EUR, RUB)`);
917
+ }
918
+ if (request.orderId.length > 255) throw new ManatError("invalid_request", "epoint: orderId is longer than 255 characters");
919
+ const payload = {
920
+ public_key: this.config.publicKey,
921
+ amount: toDecimalString(request.amount),
922
+ currency: request.amount.currency,
923
+ language: request.language ?? "az",
924
+ order_id: request.orderId
925
+ };
926
+ if (request.description) payload.description = truncate2(request.description, 1e3);
927
+ payload.success_redirect_url = request.returnUrl;
928
+ payload.error_redirect_url = request.returnUrl;
929
+ const response = await this.call("request", payload);
930
+ if (response.status !== "success" || !response.redirect_url || !response.transaction) {
931
+ throw this.error("payment was not created", response);
932
+ }
933
+ return { paymentId: response.transaction, status: "pending", next: { type: "redirect", url: response.redirect_url }, raw: response };
934
+ }
935
+ async verifyCallback(callback) {
936
+ const fields = formFields(callback.body);
937
+ const data = fields.data;
938
+ const signature = fields.signature;
939
+ if (!data || !signature) {
940
+ throw new ManatError("invalid_signature", "epoint: callback has no data/signature (the browser redirect is not signed; use getStatus)", { provider: this.name, raw: callback.body });
941
+ }
942
+ if (!safeEqual(signature, epointSignature(this.config.privateKey, data))) {
943
+ throw new ManatError("invalid_signature", "epoint: callback signature does not match", { provider: this.name, raw: callback.body });
944
+ }
945
+ const payload = decodeData(data);
946
+ if (!payload?.transaction) throw new ManatError("provider_error", "epoint: signed callback has no transaction", { provider: this.name, raw: payload });
947
+ const event = { paymentId: payload.transaction, status: mapEpointStatus(payload.status), raw: payload };
948
+ if (payload.order_id) event.orderId = String(payload.order_id);
949
+ return event;
950
+ }
951
+ async getStatus({ paymentId }) {
952
+ const response = await this.call("get-status", { public_key: this.config.publicKey, transaction: paymentId });
953
+ if (response.status === "server_error" || !response.transaction) throw this.error("status is not available", response);
954
+ if (response.transaction !== paymentId) throw this.error(`status is for transaction ${response.transaction}, not ${paymentId}`, response);
955
+ return { paymentId, status: mapEpointStatus(response.status), raw: response };
956
+ }
957
+ async refund(request) {
958
+ const payload = {
959
+ public_key: this.config.publicKey,
960
+ language: "az",
961
+ transaction: request.paymentId
962
+ };
963
+ if (request.amount) payload.amount = toDecimalString(request.amount);
964
+ payload.currency = request.amount?.currency ?? "AZN";
965
+ const response = await this.call("reverse", payload);
966
+ if (response.status !== "success") throw this.error("refund failed", response);
967
+ const state = await this.getStatus({ paymentId: request.paymentId });
968
+ const status = state.status === "paid" ? "partially_refunded" : state.status;
969
+ return { paymentId: request.paymentId, status, raw: { refund: response, payment: state.raw } };
970
+ }
971
+ async call(method, payload) {
972
+ const { data, signature } = epointEncode(this.config.privateKey, JSON.stringify(payload));
973
+ const { status, data: body } = await this.http.json({
974
+ method: "POST",
975
+ url: `${this.baseUrl}/${method}`,
976
+ body: new URLSearchParams({ data, signature })
977
+ });
978
+ if (!body || typeof body !== "object") throw new ManatError("provider_error", `epoint: unexpected response (HTTP ${status})`, { provider: this.name, raw: body });
979
+ if (status >= 400) throw this.error(`HTTP ${status}`, body);
980
+ return body;
981
+ }
982
+ error(what, response) {
983
+ return new ManatError("provider_error", `epoint: ${response.message ?? what}`, {
984
+ provider: this.name,
985
+ ...response.code ? { providerCode: String(response.code) } : {},
986
+ ...response.message ? { providerMessage: response.message } : {},
987
+ raw: response
988
+ });
989
+ }
990
+ };
991
+ function epoint(config, options) {
992
+ return new EpointProvider(config, options);
993
+ }
994
+ function formFields(body) {
995
+ if (typeof body === "string") return Object.fromEntries(new URLSearchParams(body));
996
+ if (!body || typeof body !== "object") return {};
997
+ const pick4 = (key) => typeof body[key] === "string" ? body[key] : void 0;
998
+ return { data: pick4("data"), signature: pick4("signature") };
999
+ }
1000
+ function decodeData(data) {
1001
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(data)) throw new ManatError("provider_error", "epoint: callback data is not base64", { provider: "epoint", raw: data });
1002
+ try {
1003
+ const value = JSON.parse(Buffer.from(data, "base64").toString("utf8"));
1004
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1005
+ } catch {
1006
+ throw new ManatError("provider_error", "epoint: callback data is not JSON", { provider: "epoint", raw: data });
1007
+ }
1008
+ }
1009
+ function truncate2(value, max) {
1010
+ const chars = Array.from(value);
1011
+ return chars.length > max ? chars.slice(0, max).join("") : value;
1012
+ }
1013
+
1014
+ // src/providers/goldenpay.ts
1015
+ var BASE_URLS2 = {
1016
+ test: "https://rest-newpgtest.goldenpay.az",
1017
+ production: "https://rest-pg.goldenpay.az"
1018
+ };
1019
+ var CAPABILITIES5 = /* @__PURE__ */ new Set(["createPayment", "verifyCallback", "getStatus", "refund"]);
1020
+ var SUCCESS2 = 1;
1021
+ var REVERSED = 817;
1022
+ var MAX_DESCRIPTION_BYTES = 500;
1023
+ function mapGoldenPayStatus(code2) {
1024
+ switch (code2) {
1025
+ case SUCCESS2:
1026
+ return "paid";
1027
+ case 813:
1028
+ // payment page not opened yet, or waiting for payment
1029
+ case 818:
1030
+ return "pending";
1031
+ case 814:
1032
+ // cancelled by the customer
1033
+ case 819:
1034
+ return "cancelled";
1035
+ case 815:
1036
+ // payment error
1037
+ case 816:
1038
+ return "failed";
1039
+ case REVERSED:
1040
+ return "refunded";
1041
+ default:
1042
+ throw new ManatError("provider_error", `goldenpay: status code ${code2} does not describe a payment`, { provider: "goldenpay" });
1043
+ }
1044
+ }
1045
+ var PAYMENT_CODES = /* @__PURE__ */ new Set([SUCCESS2, 813, 814, 815, 816, REVERSED, 818, 819]);
1046
+ var GoldenPayProvider = class {
1047
+ constructor(config, options = {}) {
1048
+ this.config = config;
1049
+ if (!config?.authKey) throw new ManatError("invalid_request", "goldenpay: authKey is required");
1050
+ if (!config.merchantName) throw new ManatError("invalid_request", "goldenpay: merchantName is required");
1051
+ this.baseUrl = (config.baseUrl ?? BASE_URLS2[config.environment ?? "production"]).replace(/\/+$/, "");
1052
+ this.http = new HttpClient(this.name, options);
1053
+ }
1054
+ config;
1055
+ name = "goldenpay";
1056
+ capabilities = CAPABILITIES5;
1057
+ http;
1058
+ baseUrl;
1059
+ async createPayment(request) {
1060
+ if (request.intent === "authorize") {
1061
+ throw new ManatError("unsupported_operation", "goldenpay: pre-authorisation is not supported", { provider: this.name });
1062
+ }
1063
+ assertAzn(request.amount);
1064
+ const description = truncateBytes(request.description ?? request.orderId, MAX_DESCRIPTION_BYTES);
1065
+ const amount = request.amount.amount;
1066
+ const body = {
1067
+ merchantName: this.config.merchantName,
1068
+ amount,
1069
+ lang: request.language ?? "az",
1070
+ description,
1071
+ redirectUrl: request.returnUrl,
1072
+ hashCode: md5(this.config.authKey + this.config.merchantName + amount + description)
1073
+ };
1074
+ const response = await this.call("POST", "getPaymentKey", body);
1075
+ if (response.status?.code !== SUCCESS2 || !response.paymentKey) throw this.error("payment was not created", response);
1076
+ const url = `${this.baseUrl}/pay/${encodeURIComponent(response.paymentKey)}`;
1077
+ return { paymentId: response.paymentKey, status: "pending", next: { type: "redirect", url }, raw: response };
1078
+ }
1079
+ async verifyCallback(callback) {
1080
+ const key = pick2(callback.query, "payment_key") ?? pick2(callback.query, "paymentKey") ?? pickBody2(callback.body, "payment_key") ?? pickBody2(callback.body, "paymentKey");
1081
+ if (!key || !PAYMENT_KEY.test(key)) {
1082
+ throw new ManatError("invalid_signature", "goldenpay: redirect has no payment_key", { provider: this.name, raw: callback });
1083
+ }
1084
+ const state = await this.getStatus({ paymentId: key });
1085
+ const event = { paymentId: state.paymentId, status: state.status, raw: state.raw };
1086
+ if (state.amount) event.amount = state.amount;
1087
+ return event;
1088
+ }
1089
+ async getStatus({ paymentId }) {
1090
+ assertPaymentKey(paymentId);
1091
+ const query = new URLSearchParams({ payment_key: paymentId, hash_code: md5(this.config.authKey + paymentId) });
1092
+ const response = await this.call("GET", `getPaymentResult?${query}`);
1093
+ const code2 = response.status?.code;
1094
+ if (code2 === void 0 || !PAYMENT_CODES.has(code2)) throw this.error("status is not available", response);
1095
+ if (response.paymentKey && response.paymentKey !== paymentId) throw this.error(`result is for payment ${response.paymentKey}, not ${paymentId}`, response);
1096
+ const state = { paymentId, status: mapGoldenPayStatus(code2), raw: response };
1097
+ if (typeof response.amount === "number" && response.amount > 0) state.amount = { amount: response.amount, currency: "AZN" };
1098
+ return state;
1099
+ }
1100
+ async refund(request) {
1101
+ if (request.amount) assertAzn(request.amount);
1102
+ const payment = await this.getStatus({ paymentId: request.paymentId });
1103
+ const amount = request.amount ?? payment.amount;
1104
+ if (!amount) throw new ManatError("provider_error", `goldenpay: payment ${request.paymentId} has no amount to refund`, { provider: this.name, raw: payment.raw });
1105
+ const hashCode = md5(this.config.authKey + request.paymentId);
1106
+ const response = await this.call("POST", "reversePaymentKey", {
1107
+ paymentKey: request.paymentId,
1108
+ hashCode,
1109
+ amount: amount.amount,
1110
+ secretKey: md5(hashCode + amount.amount)
1111
+ });
1112
+ const code2 = response.status?.code;
1113
+ if (code2 !== REVERSED && code2 !== SUCCESS2) throw this.error("refund failed", response);
1114
+ const partial = payment.amount !== void 0 && amount.amount < payment.amount.amount;
1115
+ return { paymentId: request.paymentId, status: partial ? "partially_refunded" : "refunded", raw: { refund: response, payment: payment.raw } };
1116
+ }
1117
+ async call(method, path, body) {
1118
+ const { status, data } = await this.http.json({ method, url: `${this.baseUrl}/${path}`, body });
1119
+ if (!data || typeof data !== "object" || !data.status) throw new ManatError("provider_error", `goldenpay: unexpected response (HTTP ${status})`, { provider: this.name, raw: data });
1120
+ if (status >= 400) throw this.error(`HTTP ${status}`, data);
1121
+ return data;
1122
+ }
1123
+ error(what, response) {
1124
+ const { code: code2, message } = response.status ?? {};
1125
+ const fromProvider = code2 !== void 0 && code2 !== SUCCESS2;
1126
+ return new ManatError("provider_error", `goldenpay: ${fromProvider && message ? message : what}`, {
1127
+ provider: this.name,
1128
+ ...fromProvider ? { providerCode: String(code2) } : {},
1129
+ ...fromProvider && message ? { providerMessage: message } : {},
1130
+ raw: response
1131
+ });
1132
+ }
1133
+ };
1134
+ function goldenPay(config, options) {
1135
+ return new GoldenPayProvider(config, options);
1136
+ }
1137
+ var PAYMENT_KEY = /^[A-Za-z0-9-]{1,64}$/;
1138
+ function assertPaymentKey(paymentId) {
1139
+ if (!PAYMENT_KEY.test(paymentId)) throw new ManatError("invalid_request", `goldenpay: invalid paymentId "${paymentId}"`);
1140
+ }
1141
+ function assertAzn(money) {
1142
+ if (money?.currency !== "AZN") throw new ManatError("invalid_request", `goldenpay: only AZN is supported, got ${money?.currency}`);
1143
+ }
1144
+ function md5(value) {
1145
+ return hash("md5", value);
1146
+ }
1147
+ function truncateBytes(value, max) {
1148
+ if (Buffer.byteLength(value, "utf8") <= max) return value;
1149
+ let out = "";
1150
+ for (const char of value) {
1151
+ if (Buffer.byteLength(out + char, "utf8") > max) break;
1152
+ out += char;
1153
+ }
1154
+ return out;
1155
+ }
1156
+ function pick2(source, key) {
1157
+ const value = source?.[key];
1158
+ return Array.isArray(value) ? value[0] : value;
1159
+ }
1160
+ function pickBody2(body, key) {
1161
+ if (!body) return void 0;
1162
+ if (typeof body === "string") return new URLSearchParams(body).get(key) ?? void 0;
1163
+ const value = body[key];
1164
+ return typeof value === "string" ? value : void 0;
1165
+ }
1166
+
1167
+ // src/providers/yigim.ts
1168
+ var BASE_URLS3 = {
1169
+ test: "https://sandbox.api.pay.yigim.az",
1170
+ production: "https://api.pay.yigim.az"
1171
+ };
1172
+ var CAPABILITIES6 = /* @__PURE__ */ new Set(["createPayment", "verifyCallback", "getStatus", "refund", "capture", "cancel"]);
1173
+ var OK = 0;
1174
+ var DEFAULT_TEMPLATE = "TPL0001";
1175
+ var REFERENCE = /^[^\s\u0000-\u001f]{1,255}$/;
1176
+ function mapYigimStatus(status) {
1177
+ switch (status) {
1178
+ case "S0":
1179
+ // created, waiting for card data
1180
+ case "S2":
1181
+ // in progress
1182
+ case "S7":
1183
+ // system malfunction
1184
+ case "09":
1185
+ return "pending";
1186
+ case "S1":
1187
+ return "authorized";
1188
+ case "00":
1189
+ return "paid";
1190
+ case "S4":
1191
+ return "cancelled";
1192
+ case "S3":
1193
+ // unknown error
1194
+ case "01":
1195
+ // refer to issuer
1196
+ case "02":
1197
+ // expired card
1198
+ case "03":
1199
+ // invalid amount
1200
+ case "04":
1201
+ // inactive card
1202
+ case "05":
1203
+ // insufficient funds
1204
+ case "06":
1205
+ // suspected fraud
1206
+ case "07":
1207
+ // exceeds withdrawal limit
1208
+ case "08":
1209
+ return "failed";
1210
+ default:
1211
+ throw new ManatError("provider_error", `yigim: unknown payment status "${status}"`, { provider: "yigim" });
1212
+ }
1213
+ }
1214
+ var YigimProvider = class {
1215
+ constructor(config, options = {}) {
1216
+ this.config = config;
1217
+ if (!config?.merchant) throw new ManatError("invalid_request", "yigim: merchant is required");
1218
+ if (!config.apiKey) throw new ManatError("invalid_request", "yigim: apiKey is required");
1219
+ if (!config.biller) throw new ManatError("invalid_request", "yigim: biller is required");
1220
+ this.baseUrl = (config.baseUrl ?? BASE_URLS3[config.environment ?? "production"]).replace(/\/+$/, "");
1221
+ this.http = new HttpClient(this.name, options);
1222
+ }
1223
+ config;
1224
+ name = "yigim";
1225
+ capabilities = CAPABILITIES6;
1226
+ http;
1227
+ baseUrl;
1228
+ async createPayment(request) {
1229
+ assertReference(request.orderId);
1230
+ if (/[&;]/.test(request.returnUrl)) {
1231
+ throw new ManatError("invalid_request", 'yigim: returnUrl may carry at most one query parameter and no ";"');
1232
+ }
1233
+ const params = {
1234
+ reference: request.orderId,
1235
+ type: request.intent === "authorize" ? "DMS" : "SMS",
1236
+ amount: String(request.amount.amount),
1237
+ currency: currencyNumericCode(request.amount.currency),
1238
+ biller: this.config.biller
1239
+ };
1240
+ if (request.description) params.description = request.description;
1241
+ params.template = this.config.template ?? DEFAULT_TEMPLATE;
1242
+ params.language = request.language ?? "az";
1243
+ if (request.callbackUrl) params.callback = request.callbackUrl.includes("?") ? request.callbackUrl : `${request.callbackUrl}?`;
1244
+ params.extra = `back-url=${request.returnUrl};fail-url=${request.returnUrl}`;
1245
+ const reply = await this.call("payment/create", params);
1246
+ if (!reply.url) throw this.error("payment page URL is missing", reply);
1247
+ return { paymentId: request.orderId, status: "pending", next: { type: "redirect", url: reply.url }, raw: reply };
1248
+ }
1249
+ async verifyCallback(callback) {
1250
+ const reference = pick3(callback.query, "reference") ?? pickBody3(callback.body, "reference");
1251
+ if (!reference || !REFERENCE.test(reference)) {
1252
+ throw new ManatError("invalid_signature", "yigim: callback has no reference", { provider: this.name, raw: callback });
1253
+ }
1254
+ const state = await this.getStatus({ paymentId: reference });
1255
+ const event = { paymentId: state.paymentId, orderId: state.paymentId, status: state.status, raw: state.raw };
1256
+ if (state.amount) event.amount = state.amount;
1257
+ return event;
1258
+ }
1259
+ async getStatus({ paymentId }) {
1260
+ assertReference(paymentId);
1261
+ const reply = await this.call("payment/status", { reference: paymentId });
1262
+ if (reply.reference && reply.reference !== paymentId) throw this.error(`status is for payment ${reply.reference}, not ${paymentId}`, reply);
1263
+ const state = { paymentId, status: mapYigimStatus(reply.status), raw: reply };
1264
+ const currency = typeof reply.currency === "string" ? currencyFromNumeric(reply.currency) : void 0;
1265
+ if (reply.amount !== void 0 && reply.amount !== "" && currency) state.amount = fromDecimal(String(reply.amount), currency);
1266
+ return state;
1267
+ }
1268
+ async refund(request) {
1269
+ const reply = await this.call("payment/refund", withAmount(request.paymentId, request.amount));
1270
+ const state = await this.getStatus({ paymentId: request.paymentId });
1271
+ return { paymentId: request.paymentId, status: refundStatus(state, request.amount), raw: { refund: reply, payment: state.raw } };
1272
+ }
1273
+ async capture({ paymentId, amount }) {
1274
+ await this.call("payment/charge", withAmount(paymentId, amount));
1275
+ return this.getStatus({ paymentId });
1276
+ }
1277
+ async cancel({ paymentId }) {
1278
+ await this.call("payment/cancel", { reference: paymentId });
1279
+ return this.getStatus({ paymentId });
1280
+ }
1281
+ async call(command, params) {
1282
+ assertReference(params.reference ?? "");
1283
+ const { status, data } = await this.http.json({
1284
+ method: "GET",
1285
+ url: `${this.baseUrl}/${command}?${new URLSearchParams(params)}`,
1286
+ headers: { "x-merchant": this.config.merchant, "x-api-key": this.config.apiKey, "x-type": "JSON" }
1287
+ });
1288
+ const reply = unwrap(data);
1289
+ if (!reply || code(reply) === void 0) {
1290
+ throw new ManatError("provider_error", `yigim: unexpected response (HTTP ${status})`, { provider: this.name, raw: data });
1291
+ }
1292
+ if (status >= 400 || code(reply) !== OK) throw this.error(`HTTP ${status}`, reply);
1293
+ return reply;
1294
+ }
1295
+ error(what, reply) {
1296
+ const fromProvider = code(reply) !== void 0 && code(reply) !== OK;
1297
+ return new ManatError("provider_error", `yigim: ${fromProvider && reply.message ? reply.message : what}`, {
1298
+ provider: this.name,
1299
+ ...fromProvider ? { providerCode: String(reply.code) } : {},
1300
+ ...fromProvider && reply.message ? { providerMessage: reply.message } : {},
1301
+ raw: reply
1302
+ });
1303
+ }
1304
+ };
1305
+ function yigim(config, options) {
1306
+ return new YigimProvider(config, options);
1307
+ }
1308
+ function code(reply) {
1309
+ if (typeof reply.code === "number") return reply.code;
1310
+ return typeof reply.code === "string" && /^-?\d+$/.test(reply.code) ? Number(reply.code) : void 0;
1311
+ }
1312
+ function unwrap(data) {
1313
+ if (!data || typeof data !== "object") return void 0;
1314
+ const inner = data.response;
1315
+ return inner && typeof inner === "object" ? inner : data;
1316
+ }
1317
+ function withAmount(reference, amount) {
1318
+ return amount ? { reference, amount: String(amount.amount) } : { reference };
1319
+ }
1320
+ function refundStatus(state, amount) {
1321
+ if (state.status !== "paid") return state.status;
1322
+ return amount && state.amount && amount.amount < state.amount.amount ? "partially_refunded" : "refunded";
1323
+ }
1324
+ function assertReference(reference) {
1325
+ if (!REFERENCE.test(reference)) throw new ManatError("invalid_request", `yigim: invalid reference "${reference}"`);
1326
+ }
1327
+ function pick3(source, key) {
1328
+ const value = source?.[key];
1329
+ return Array.isArray(value) ? value[0] : value;
1330
+ }
1331
+ function pickBody3(body, key) {
1332
+ if (!body) return void 0;
1333
+ if (typeof body === "string") return new URLSearchParams(body).get(key) ?? void 0;
1334
+ const value = body[key];
1335
+ return typeof value === "string" ? value : void 0;
1336
+ }
1337
+ // Annotate the CommonJS export names for ESM import in node:
1338
+ 0 && (module.exports = {
1339
+ AZERICARD_MAC_FIELDS,
1340
+ AzericardProvider,
1341
+ EpointProvider,
1342
+ GoldenPayProvider,
1343
+ KapitalBankProvider,
1344
+ Manat,
1345
+ ManatError,
1346
+ PayriffProvider,
1347
+ YigimProvider,
1348
+ azericard,
1349
+ azn,
1350
+ epoint,
1351
+ fromDecimal,
1352
+ goldenPay,
1353
+ isManatError,
1354
+ kapitalBank,
1355
+ payriff,
1356
+ toDecimalString,
1357
+ yigim
1358
+ });