@sikka/aps 0.0.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.
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/dist/chunk-FLFFXAST.mjs +408 -0
- package/dist/chunk-TGVOU6OE.mjs +409 -0
- package/dist/index.d.mts +2007 -0
- package/dist/index.d.ts +2007 -0
- package/dist/index.js +2822 -0
- package/dist/index.mjs +2741 -0
- package/dist/react/index.d.mts +932 -0
- package/dist/react/index.d.ts +932 -0
- package/dist/react/index.js +1863 -0
- package/dist/react/index.mjs +1829 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2822 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
APS: () => APS,
|
|
34
|
+
APSClient: () => APSClient,
|
|
35
|
+
APSException: () => APSException,
|
|
36
|
+
Commands: () => Commands,
|
|
37
|
+
CurrencyCodes: () => CurrencyCodes,
|
|
38
|
+
CustomPaymentPageModule: () => CustomPaymentPageModule,
|
|
39
|
+
ECIValues: () => ECIValues,
|
|
40
|
+
ErrorCategories: () => ErrorCategories,
|
|
41
|
+
HostedCheckoutModule: () => HostedCheckoutModule,
|
|
42
|
+
LanguageCodes: () => LanguageCodes,
|
|
43
|
+
PaymentLinksModule: () => PaymentLinksModule,
|
|
44
|
+
PaymentMethodCodes: () => PaymentMethodCodes,
|
|
45
|
+
PaymentsModule: () => PaymentsModule,
|
|
46
|
+
RecurringModule: () => RecurringModule,
|
|
47
|
+
ResponseCodes: () => ResponseCodes,
|
|
48
|
+
StatusCodes: () => StatusCodes,
|
|
49
|
+
TestCards: () => TestCards,
|
|
50
|
+
TokenizationModule: () => TokenizationModule,
|
|
51
|
+
WebhooksModule: () => WebhooksModule,
|
|
52
|
+
categorizeError: () => categorizeError,
|
|
53
|
+
checkRetryableError: () => isRetryableError2,
|
|
54
|
+
default: () => index_default,
|
|
55
|
+
formatErrorForLogging: () => formatErrorForLogging,
|
|
56
|
+
getErrorCodesByCategory: () => getErrorCodesByCategory,
|
|
57
|
+
getErrorDetails: () => getErrorDetails,
|
|
58
|
+
getUserFriendlyMessage: () => getUserFriendlyMessage,
|
|
59
|
+
isRetryableError: () => isRetryableError,
|
|
60
|
+
isSuccessCode: () => isSuccessCode,
|
|
61
|
+
isValidAmount: () => isValidAmount,
|
|
62
|
+
isValidCVV: () => isValidCVV,
|
|
63
|
+
isValidCardNumber: () => isValidCardNumber,
|
|
64
|
+
isValidCurrency: () => isValidCurrency,
|
|
65
|
+
isValidCustomerName: () => isValidCustomerName,
|
|
66
|
+
isValidDescription: () => isValidDescription,
|
|
67
|
+
isValidEmail: () => isValidEmail,
|
|
68
|
+
isValidExpiryDate: () => isValidExpiryDate,
|
|
69
|
+
isValidLanguage: () => isValidLanguage,
|
|
70
|
+
isValidMerchantReference: () => isValidMerchantReference,
|
|
71
|
+
isValidPhone: () => isValidPhone,
|
|
72
|
+
isValidReturnUrl: () => isValidReturnUrl,
|
|
73
|
+
isValidSignature: () => isValidSignature,
|
|
74
|
+
isValidTokenName: () => isValidTokenName,
|
|
75
|
+
isValidWebhookPayload: () => isValidWebhookPayload,
|
|
76
|
+
suggestHttpStatus: () => suggestHttpStatus,
|
|
77
|
+
validatePaymentParams: () => validatePaymentParams,
|
|
78
|
+
validators: () => validators
|
|
79
|
+
});
|
|
80
|
+
module.exports = __toCommonJS(index_exports);
|
|
81
|
+
|
|
82
|
+
// src/types.ts
|
|
83
|
+
var APSException = class extends Error {
|
|
84
|
+
constructor(error) {
|
|
85
|
+
super(error.message);
|
|
86
|
+
this.name = "APSException";
|
|
87
|
+
this.code = error.code;
|
|
88
|
+
this.statusCode = error.statusCode;
|
|
89
|
+
this.rawResponse = error.rawResponse;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/utils.ts
|
|
94
|
+
var import_crypto = __toESM(require("crypto"));
|
|
95
|
+
var noopLogger = {
|
|
96
|
+
debug: () => {
|
|
97
|
+
},
|
|
98
|
+
info: () => {
|
|
99
|
+
},
|
|
100
|
+
warn: () => {
|
|
101
|
+
},
|
|
102
|
+
error: () => {
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
function generateHMAC(data, secret) {
|
|
106
|
+
return import_crypto.default.createHmac("sha256", secret).update(data).digest("base64");
|
|
107
|
+
}
|
|
108
|
+
function verifyHMAC(data, signature, secret) {
|
|
109
|
+
const expectedSignature = generateHMAC(data, secret);
|
|
110
|
+
return expectedSignature === signature;
|
|
111
|
+
}
|
|
112
|
+
function buildQueryString(params) {
|
|
113
|
+
return Object.entries(params).filter(([_, value]) => value !== void 0 && value !== null && value !== "").sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => `${key}=${value}`).join("&");
|
|
114
|
+
}
|
|
115
|
+
function getBaseUrl(environment) {
|
|
116
|
+
if (environment === "production") {
|
|
117
|
+
return "https://checkout.payfort.com";
|
|
118
|
+
}
|
|
119
|
+
return "https://sbcheckout.payfort.com";
|
|
120
|
+
}
|
|
121
|
+
function getHostedCheckoutUrl(environment) {
|
|
122
|
+
if (environment === "production") {
|
|
123
|
+
return "https://checkout.payfort.com/FortAPI/paymentPage";
|
|
124
|
+
}
|
|
125
|
+
return "https://sbcheckout.payfort.com/FortAPI/paymentPage";
|
|
126
|
+
}
|
|
127
|
+
function getApiUrl(environment) {
|
|
128
|
+
if (environment === "production") {
|
|
129
|
+
return "https://paymentservices.payfort.com";
|
|
130
|
+
}
|
|
131
|
+
return "https://sbpaymentservices.payfort.com";
|
|
132
|
+
}
|
|
133
|
+
function generateOrderId() {
|
|
134
|
+
return `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
135
|
+
}
|
|
136
|
+
function sleep(ms) {
|
|
137
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
|
+
}
|
|
139
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
140
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
141
|
+
var RateLimiter = class {
|
|
142
|
+
constructor(maxRequests = 100, windowMs = 6e4) {
|
|
143
|
+
this.requests = [];
|
|
144
|
+
this.maxRequests = maxRequests;
|
|
145
|
+
this.windowMs = windowMs;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if a request can be made
|
|
149
|
+
*/
|
|
150
|
+
canMakeRequest() {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
this.requests = this.requests.filter((time) => now - time < this.windowMs);
|
|
153
|
+
return this.requests.length < this.maxRequests;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Record a request
|
|
157
|
+
*/
|
|
158
|
+
recordRequest() {
|
|
159
|
+
this.requests.push(Date.now());
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get time until next request is allowed (in ms)
|
|
163
|
+
*/
|
|
164
|
+
getTimeUntilNextRequest() {
|
|
165
|
+
if (this.canMakeRequest()) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
const oldestRequest = Math.min(...this.requests);
|
|
169
|
+
return this.windowMs - (Date.now() - oldestRequest);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Wait until a request can be made
|
|
173
|
+
*/
|
|
174
|
+
async waitForSlot() {
|
|
175
|
+
const waitTime = this.getTimeUntilNextRequest();
|
|
176
|
+
if (waitTime > 0) {
|
|
177
|
+
await sleep(waitTime);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var globalRateLimiter = null;
|
|
182
|
+
function getRateLimiter(maxRequests, windowMs) {
|
|
183
|
+
if (!globalRateLimiter) {
|
|
184
|
+
globalRateLimiter = new RateLimiter(maxRequests, windowMs);
|
|
185
|
+
}
|
|
186
|
+
return globalRateLimiter;
|
|
187
|
+
}
|
|
188
|
+
async function makeRequest(url, body, options = {}) {
|
|
189
|
+
const {
|
|
190
|
+
timeout = DEFAULT_TIMEOUT,
|
|
191
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
192
|
+
retryDelayMs = 1e3,
|
|
193
|
+
logger = noopLogger,
|
|
194
|
+
idempotencyKey
|
|
195
|
+
} = options;
|
|
196
|
+
const rateLimiter = getRateLimiter();
|
|
197
|
+
if (!rateLimiter.canMakeRequest()) {
|
|
198
|
+
logger.warn?.("Rate limit reached, waiting for slot...");
|
|
199
|
+
await rateLimiter.waitForSlot();
|
|
200
|
+
}
|
|
201
|
+
let lastError = null;
|
|
202
|
+
const startTime = Date.now();
|
|
203
|
+
const headers = {
|
|
204
|
+
"Content-Type": "application/json"
|
|
205
|
+
};
|
|
206
|
+
if (idempotencyKey) {
|
|
207
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
208
|
+
}
|
|
209
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
212
|
+
try {
|
|
213
|
+
logger.debug?.(`APS Request (attempt ${attempt + 1}/${maxRetries + 1}):`, { url, body });
|
|
214
|
+
rateLimiter.recordRequest();
|
|
215
|
+
const response = await fetch(url, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers,
|
|
218
|
+
body: JSON.stringify(body),
|
|
219
|
+
signal: controller.signal
|
|
220
|
+
});
|
|
221
|
+
clearTimeout(timeoutId);
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
if (response.status === 429) {
|
|
224
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
225
|
+
const waitTime = retryAfter ? parseInt(retryAfter) * 1e3 : retryDelayMs * Math.pow(2, attempt);
|
|
226
|
+
logger.warn?.(`Rate limited (429), waiting ${waitTime}ms before retry...`);
|
|
227
|
+
await sleep(waitTime);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
231
|
+
const delay = retryDelayMs * Math.pow(2, attempt);
|
|
232
|
+
logger.warn?.(`Server error (${response.status}), retrying in ${delay}ms...`);
|
|
233
|
+
await sleep(delay);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
237
|
+
}
|
|
238
|
+
const data = await response.json();
|
|
239
|
+
const duration = Date.now() - startTime;
|
|
240
|
+
logger.info?.(`APS Response (${duration}ms):`, {
|
|
241
|
+
responseCode: data.response_code,
|
|
242
|
+
status: data.status
|
|
243
|
+
});
|
|
244
|
+
return data;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
clearTimeout(timeoutId);
|
|
247
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
248
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
249
|
+
logger.error?.(`Request timed out after ${timeout}ms`);
|
|
250
|
+
throw new Error(`Request timed out after ${timeout}ms`);
|
|
251
|
+
}
|
|
252
|
+
logger.error?.(`Request failed (attempt ${attempt + 1}):`, lastError.message);
|
|
253
|
+
if (attempt < maxRetries) {
|
|
254
|
+
const delay = retryDelayMs * Math.pow(2, attempt);
|
|
255
|
+
await sleep(delay);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
logger.error?.("Request failed after all retries:", lastError?.message);
|
|
261
|
+
throw lastError || new Error("Request failed after retries");
|
|
262
|
+
}
|
|
263
|
+
function validateRequiredFields(fields, required) {
|
|
264
|
+
const missing = required.filter((field) => {
|
|
265
|
+
const value = fields[field];
|
|
266
|
+
return value === void 0 || value === null || value === "";
|
|
267
|
+
});
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
throw new Error(`Missing required fields: ${missing.join(", ")}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function validateEmail(email) {
|
|
273
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
274
|
+
return emailRegex.test(email);
|
|
275
|
+
}
|
|
276
|
+
function validateCardNumber(cardNumber) {
|
|
277
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
278
|
+
if (cleaned.length < 13 || cleaned.length > 19) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
let sum = 0;
|
|
282
|
+
let shouldDouble = false;
|
|
283
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
284
|
+
let digit = parseInt(cleaned.charAt(i), 10);
|
|
285
|
+
if (shouldDouble) {
|
|
286
|
+
digit *= 2;
|
|
287
|
+
if (digit > 9) {
|
|
288
|
+
digit -= 9;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
sum += digit;
|
|
292
|
+
shouldDouble = !shouldDouble;
|
|
293
|
+
}
|
|
294
|
+
return sum % 10 === 0;
|
|
295
|
+
}
|
|
296
|
+
function validateExpiryDate(month, year) {
|
|
297
|
+
const expiryMonth = parseInt(month, 10);
|
|
298
|
+
const expiryYear = parseInt(year.length === 2 ? `20${year}` : year, 10);
|
|
299
|
+
if (expiryMonth < 1 || expiryMonth > 12) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const now = /* @__PURE__ */ new Date();
|
|
303
|
+
const currentYear = now.getFullYear();
|
|
304
|
+
const currentMonth = now.getMonth() + 1;
|
|
305
|
+
if (expiryYear < currentYear) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (expiryYear === currentYear && expiryMonth < currentMonth) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
function validateCVV(cvv) {
|
|
314
|
+
return /^\d{3,4}$/.test(cvv);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/modules/payment-links.ts
|
|
318
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
319
|
+
var PaymentLinksModule = class {
|
|
320
|
+
constructor(config, options) {
|
|
321
|
+
this.config = config;
|
|
322
|
+
this.options = options;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Create a new payment link using APS Payment Links API
|
|
326
|
+
*/
|
|
327
|
+
async create(options) {
|
|
328
|
+
try {
|
|
329
|
+
validateRequiredFields(options, ["order"]);
|
|
330
|
+
validateRequiredFields(options.order, ["amount", "currency"]);
|
|
331
|
+
if (options.order.customer?.email) {
|
|
332
|
+
if (!validateEmail(options.order.customer.email)) {
|
|
333
|
+
throw new Error("Invalid customer email format");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const orderId = options.order.id || `order_${Date.now()}`;
|
|
337
|
+
const amount = options.order.amount;
|
|
338
|
+
const currency = options.order.currency || this.config.currency;
|
|
339
|
+
const requestBody = {
|
|
340
|
+
service_command: "PAYMENT_LINK",
|
|
341
|
+
access_code: this.config.accessCode,
|
|
342
|
+
merchant_identifier: this.config.merchantId,
|
|
343
|
+
merchant_reference: orderId,
|
|
344
|
+
amount: amount.toString(),
|
|
345
|
+
currency,
|
|
346
|
+
language: this.config.language || "en",
|
|
347
|
+
customer_email: options.order.customer?.email || "customer@example.com",
|
|
348
|
+
notification_type: "NONE"
|
|
349
|
+
};
|
|
350
|
+
if (options.order.description) {
|
|
351
|
+
requestBody.order_description = options.order.description;
|
|
352
|
+
}
|
|
353
|
+
if (options.order.customer?.name) {
|
|
354
|
+
requestBody.customer_name = options.order.customer.name;
|
|
355
|
+
}
|
|
356
|
+
if (options.order.customer?.phone) {
|
|
357
|
+
requestBody.customer_phone = options.order.customer.phone;
|
|
358
|
+
}
|
|
359
|
+
if (options.recurring) {
|
|
360
|
+
requestBody.link_command = "AUTHORIZATION";
|
|
361
|
+
} else {
|
|
362
|
+
requestBody.link_command = "PURCHASE";
|
|
363
|
+
}
|
|
364
|
+
if (options.allowedPaymentMethods && options.allowedPaymentMethods.length > 0) {
|
|
365
|
+
requestBody.payment_option = options.allowedPaymentMethods[0];
|
|
366
|
+
}
|
|
367
|
+
if (options.tokenValidityHours && options.tokenValidityHours > 0) {
|
|
368
|
+
const expiryDate = /* @__PURE__ */ new Date();
|
|
369
|
+
expiryDate.setHours(expiryDate.getHours() + options.tokenValidityHours);
|
|
370
|
+
const year = expiryDate.getFullYear();
|
|
371
|
+
const month = String(expiryDate.getMonth() + 1).padStart(2, "0");
|
|
372
|
+
const day = String(expiryDate.getDate()).padStart(2, "0");
|
|
373
|
+
const hours = String(expiryDate.getHours()).padStart(2, "0");
|
|
374
|
+
const minutes = String(expiryDate.getMinutes()).padStart(2, "0");
|
|
375
|
+
const seconds = String(expiryDate.getSeconds()).padStart(2, "0");
|
|
376
|
+
requestBody.request_expiry_date = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+00:00`;
|
|
377
|
+
}
|
|
378
|
+
requestBody.signature = this.generateSignature(requestBody);
|
|
379
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
380
|
+
const response = await this.makeApiRequest(`${apiUrl}/FortAPI/paymentApi`, requestBody);
|
|
381
|
+
const isSuccess = response.status?.toString() === "20" || response.response_code?.toString() === "00000" || response.response_code?.toString() === "48000" || response.response_message?.toLowerCase() === "success";
|
|
382
|
+
if (!isSuccess) {
|
|
383
|
+
throw new APSException({
|
|
384
|
+
code: response.response_code?.toString() || "PAYMENT_LINK_ERROR",
|
|
385
|
+
message: response.response_message || "Failed to create payment link",
|
|
386
|
+
rawResponse: response
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
url: response.payment_link,
|
|
391
|
+
linkId: response.payment_link_id,
|
|
392
|
+
orderId,
|
|
393
|
+
expiresAt: requestBody.request_expiry_date ? new Date(requestBody.request_expiry_date) : void 0,
|
|
394
|
+
rawResponse: response
|
|
395
|
+
};
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof APSException) {
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
const apsError = {
|
|
401
|
+
code: "PAYMENT_LINK_ERROR",
|
|
402
|
+
message: error instanceof Error ? error.message : "Failed to create payment link",
|
|
403
|
+
rawResponse: error
|
|
404
|
+
};
|
|
405
|
+
throw new APSException(apsError);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Generate signature for APS API requests
|
|
410
|
+
* Following official APS documentation:
|
|
411
|
+
* 1. Sort all parameters alphabetically
|
|
412
|
+
* 2. Concatenate as param_name=param_value
|
|
413
|
+
* 3. Wrap with SHA phrase at beginning and end
|
|
414
|
+
* 4. Hash with SHA-256
|
|
415
|
+
*/
|
|
416
|
+
generateSignature(params) {
|
|
417
|
+
const sortedKeys = Object.keys(params).sort();
|
|
418
|
+
const concatenated = sortedKeys.map((key) => `${key}=${params[key]}`).join("");
|
|
419
|
+
const signatureString = `${this.config.requestSecret}${concatenated}${this.config.requestSecret}`;
|
|
420
|
+
return import_crypto2.default.createHash("sha256").update(signatureString).digest("hex").toUpperCase();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Make API request to APS with timeout and retry support
|
|
424
|
+
*/
|
|
425
|
+
async makeApiRequest(url, body, idempotencyKey) {
|
|
426
|
+
return makeRequest(url, body, {
|
|
427
|
+
timeout: this.options.timeout,
|
|
428
|
+
maxRetries: this.options.maxRetries,
|
|
429
|
+
logger: this.options.logger,
|
|
430
|
+
idempotencyKey
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Generate a payment link URL (alias for create)
|
|
435
|
+
*/
|
|
436
|
+
async generateUrl(options) {
|
|
437
|
+
const response = await this.create(options);
|
|
438
|
+
return response.url;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/modules/hosted-checkout.ts
|
|
443
|
+
var import_crypto3 = __toESM(require("crypto"));
|
|
444
|
+
var HostedCheckoutModule = class {
|
|
445
|
+
constructor(config, _options) {
|
|
446
|
+
this.config = config;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Create a hosted checkout session
|
|
450
|
+
*/
|
|
451
|
+
async create(checkoutOptions) {
|
|
452
|
+
try {
|
|
453
|
+
validateRequiredFields(checkoutOptions, ["order"]);
|
|
454
|
+
validateRequiredFields(checkoutOptions.order, ["amount", "currency"]);
|
|
455
|
+
if (checkoutOptions.order.customer?.email) {
|
|
456
|
+
if (!validateEmail(checkoutOptions.order.customer.email)) {
|
|
457
|
+
throw new Error("Invalid customer email format");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const orderId = checkoutOptions.order.id || generateOrderId();
|
|
461
|
+
const amount = checkoutOptions.order.amount;
|
|
462
|
+
const currency = checkoutOptions.order.currency || this.config.currency;
|
|
463
|
+
const params = {
|
|
464
|
+
command: "PURCHASE",
|
|
465
|
+
access_code: this.config.accessCode,
|
|
466
|
+
merchant_identifier: this.config.merchantId,
|
|
467
|
+
merchant_reference: orderId,
|
|
468
|
+
amount: amount.toString(),
|
|
469
|
+
currency,
|
|
470
|
+
language: this.config.language || "en",
|
|
471
|
+
customer_email: checkoutOptions.order.customer?.email || "customer@example.com"
|
|
472
|
+
// Required by APS
|
|
473
|
+
};
|
|
474
|
+
if (checkoutOptions.order.description) {
|
|
475
|
+
params.order_description = checkoutOptions.order.description;
|
|
476
|
+
}
|
|
477
|
+
if (checkoutOptions.order.customer?.email) {
|
|
478
|
+
params.customer_email = checkoutOptions.order.customer.email;
|
|
479
|
+
}
|
|
480
|
+
if (checkoutOptions.order.customer?.name) {
|
|
481
|
+
params.customer_name = checkoutOptions.order.customer.name;
|
|
482
|
+
}
|
|
483
|
+
if (checkoutOptions.order.customer?.phone) {
|
|
484
|
+
params.customer_phone = checkoutOptions.order.customer.phone;
|
|
485
|
+
}
|
|
486
|
+
if (checkoutOptions.order.customer?.billingAddress) {
|
|
487
|
+
const addr = checkoutOptions.order.customer.billingAddress;
|
|
488
|
+
if (addr.street) params.billing_address = addr.street;
|
|
489
|
+
if (addr.city) params.billing_city = addr.city;
|
|
490
|
+
if (addr.state) params.billing_state = addr.state;
|
|
491
|
+
if (addr.country) params.billing_country = addr.country;
|
|
492
|
+
if (addr.postalCode) params.billing_postal_code = addr.postalCode;
|
|
493
|
+
}
|
|
494
|
+
if (checkoutOptions.successUrl) {
|
|
495
|
+
params.return_url = checkoutOptions.successUrl;
|
|
496
|
+
} else if (checkoutOptions.failureUrl) {
|
|
497
|
+
params.return_url = checkoutOptions.failureUrl;
|
|
498
|
+
} else if (checkoutOptions.cancelUrl) {
|
|
499
|
+
params.return_url = checkoutOptions.cancelUrl;
|
|
500
|
+
}
|
|
501
|
+
if (checkoutOptions.allowedPaymentMethods && checkoutOptions.allowedPaymentMethods.length > 0) {
|
|
502
|
+
params.payment_option = String(checkoutOptions.allowedPaymentMethods[0]);
|
|
503
|
+
}
|
|
504
|
+
if (checkoutOptions.hideShipping) {
|
|
505
|
+
params.hide_shipping = "YES";
|
|
506
|
+
}
|
|
507
|
+
if (checkoutOptions.metadata) {
|
|
508
|
+
for (const [key, value] of Object.entries(checkoutOptions.metadata)) {
|
|
509
|
+
params[`merchant_param_${key}`] = value;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const sortedKeys = Object.keys(params).sort();
|
|
513
|
+
const concatenated = sortedKeys.map((key) => `${key}=${params[key]}`).join("");
|
|
514
|
+
const signatureString = `${this.config.requestSecret}${concatenated}${this.config.requestSecret}`;
|
|
515
|
+
params.signature = import_crypto3.default.createHash("sha256").update(signatureString).digest("hex");
|
|
516
|
+
const checkoutUrl = getHostedCheckoutUrl(this.config.environment);
|
|
517
|
+
const status = "pending";
|
|
518
|
+
return {
|
|
519
|
+
transactionId: orderId,
|
|
520
|
+
orderId,
|
|
521
|
+
status,
|
|
522
|
+
amount,
|
|
523
|
+
currency,
|
|
524
|
+
redirectForm: {
|
|
525
|
+
url: checkoutUrl,
|
|
526
|
+
method: "POST",
|
|
527
|
+
params
|
|
528
|
+
},
|
|
529
|
+
rawResponse: params
|
|
530
|
+
};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
const apsError = {
|
|
533
|
+
code: "HOSTED_CHECKOUT_ERROR",
|
|
534
|
+
message: error instanceof Error ? error.message : "Failed to create hosted checkout",
|
|
535
|
+
rawResponse: error
|
|
536
|
+
};
|
|
537
|
+
throw new APSException(apsError);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Get the redirect URL for hosted checkout
|
|
542
|
+
*/
|
|
543
|
+
async getRedirectUrl(options) {
|
|
544
|
+
const response = await this.create(options);
|
|
545
|
+
if (response.redirectForm) {
|
|
546
|
+
return response.redirectForm.url;
|
|
547
|
+
}
|
|
548
|
+
throw new APSException({
|
|
549
|
+
code: "NO_REDIRECT_URL",
|
|
550
|
+
message: "No redirect URL available"
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/modules/custom-payment-page.ts
|
|
556
|
+
var import_crypto4 = __toESM(require("crypto"));
|
|
557
|
+
var CustomPaymentPageModule = class {
|
|
558
|
+
constructor(config, options) {
|
|
559
|
+
this.config = config;
|
|
560
|
+
this.options = options;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Get tokenization form data
|
|
564
|
+
* This form should be submitted from the frontend to APS
|
|
565
|
+
*/
|
|
566
|
+
getTokenizationForm(options) {
|
|
567
|
+
const merchantRef = options.merchantReference || `token_${Date.now()}`;
|
|
568
|
+
const params = {
|
|
569
|
+
service_command: "TOKENIZATION",
|
|
570
|
+
language: this.config.language || "en",
|
|
571
|
+
merchant_identifier: this.config.merchantId,
|
|
572
|
+
access_code: this.config.accessCode,
|
|
573
|
+
return_url: options.returnUrl,
|
|
574
|
+
merchant_reference: merchantRef
|
|
575
|
+
};
|
|
576
|
+
if (options.cardNumber) {
|
|
577
|
+
params.card_number = options.cardNumber;
|
|
578
|
+
}
|
|
579
|
+
if (options.expiryDate) {
|
|
580
|
+
params.expiry_date = options.expiryDate;
|
|
581
|
+
}
|
|
582
|
+
if (options.cardSecurityCode) {
|
|
583
|
+
params.card_security_code = options.cardSecurityCode;
|
|
584
|
+
}
|
|
585
|
+
if (options.cardHolderName) {
|
|
586
|
+
params.card_holder_name = options.cardHolderName;
|
|
587
|
+
}
|
|
588
|
+
const sortedKeys = Object.keys(params).sort();
|
|
589
|
+
const concatenated = sortedKeys.map((key) => `${key}=${params[key]}`).join("");
|
|
590
|
+
const signatureString = `${this.config.requestSecret}${concatenated}${this.config.requestSecret}`;
|
|
591
|
+
params.signature = import_crypto4.default.createHash("sha256").update(signatureString).digest("hex");
|
|
592
|
+
const url = getHostedCheckoutUrl(this.config.environment);
|
|
593
|
+
return {
|
|
594
|
+
url,
|
|
595
|
+
method: "POST",
|
|
596
|
+
params
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Get create token form data (for saving cards without charging)
|
|
601
|
+
*/
|
|
602
|
+
getCreateTokenForm(options) {
|
|
603
|
+
const merchantRef = options.merchantReference || `createtoken_${Date.now()}`;
|
|
604
|
+
const params = {
|
|
605
|
+
service_command: "CREATE_TOKEN",
|
|
606
|
+
language: this.config.language || "en",
|
|
607
|
+
merchant_identifier: this.config.merchantId,
|
|
608
|
+
access_code: this.config.accessCode,
|
|
609
|
+
return_url: options.returnUrl,
|
|
610
|
+
merchant_reference: merchantRef
|
|
611
|
+
};
|
|
612
|
+
if (options.cardNumber) {
|
|
613
|
+
params.card_number = options.cardNumber;
|
|
614
|
+
}
|
|
615
|
+
if (options.expiryDate) {
|
|
616
|
+
params.expiry_date = options.expiryDate;
|
|
617
|
+
}
|
|
618
|
+
if (options.cardHolderName) {
|
|
619
|
+
params.card_holder_name = options.cardHolderName;
|
|
620
|
+
}
|
|
621
|
+
const sortedKeys = Object.keys(params).sort();
|
|
622
|
+
const concatenated = sortedKeys.map((key) => `${key}=${params[key]}`).join("");
|
|
623
|
+
const signatureString = `${this.config.requestSecret}${concatenated}${this.config.requestSecret}`;
|
|
624
|
+
params.signature = import_crypto4.default.createHash("sha256").update(signatureString).digest("hex");
|
|
625
|
+
const url = getHostedCheckoutUrl(this.config.environment);
|
|
626
|
+
return {
|
|
627
|
+
url,
|
|
628
|
+
method: "POST",
|
|
629
|
+
params
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Charge using a token received from tokenization
|
|
634
|
+
* This is a server-to-server call
|
|
635
|
+
*/
|
|
636
|
+
async chargeWithToken(options) {
|
|
637
|
+
try {
|
|
638
|
+
validateRequiredFields(options, ["order", "tokenName", "customerEmail"]);
|
|
639
|
+
validateRequiredFields(options.order, ["amount", "currency"]);
|
|
640
|
+
if (!validateEmail(options.customerEmail)) {
|
|
641
|
+
throw new Error("Invalid customer email format");
|
|
642
|
+
}
|
|
643
|
+
const orderId = options.order.id || generateOrderId();
|
|
644
|
+
const amount = options.order.amount;
|
|
645
|
+
const currency = options.order.currency;
|
|
646
|
+
const requestBody = {
|
|
647
|
+
command: "PURCHASE",
|
|
648
|
+
access_code: this.config.accessCode,
|
|
649
|
+
merchant_identifier: this.config.merchantId,
|
|
650
|
+
merchant_reference: orderId,
|
|
651
|
+
amount: amount.toString(),
|
|
652
|
+
currency,
|
|
653
|
+
language: this.config.language || "en",
|
|
654
|
+
customer_email: options.customerEmail,
|
|
655
|
+
token_name: options.tokenName
|
|
656
|
+
};
|
|
657
|
+
if (options.order.description) {
|
|
658
|
+
requestBody.order_description = options.order.description;
|
|
659
|
+
}
|
|
660
|
+
if (options.customerName) {
|
|
661
|
+
requestBody.customer_name = options.customerName;
|
|
662
|
+
}
|
|
663
|
+
if (options.customerPhone) {
|
|
664
|
+
requestBody.customer_phone = options.customerPhone;
|
|
665
|
+
}
|
|
666
|
+
if (options.returnUrl) {
|
|
667
|
+
requestBody.return_url = options.returnUrl;
|
|
668
|
+
}
|
|
669
|
+
const sortedKeys = Object.keys(requestBody).sort();
|
|
670
|
+
const concatenated = sortedKeys.map((key) => `${key}=${requestBody[key]}`).join("");
|
|
671
|
+
const signatureString = `${this.config.requestSecret}${concatenated}${this.config.requestSecret}`;
|
|
672
|
+
requestBody.signature = import_crypto4.default.createHash("sha256").update(signatureString).digest("hex");
|
|
673
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
674
|
+
const response = await this.makeApiRequest(`${apiUrl}/FortAPI/paymentApi`, requestBody);
|
|
675
|
+
const isSuccess = response.status?.toString() === "20" || response.response_code?.toString() === "00000" || response.response_message?.toLowerCase() === "success";
|
|
676
|
+
if (!isSuccess) {
|
|
677
|
+
if (response["3ds_url"] || response.acs_url) {
|
|
678
|
+
return {
|
|
679
|
+
transactionId: response.fort_id || orderId,
|
|
680
|
+
orderId,
|
|
681
|
+
status: "pending",
|
|
682
|
+
amount,
|
|
683
|
+
currency,
|
|
684
|
+
authenticationUrl: response["3ds_url"] || response.acs_url,
|
|
685
|
+
redirectForm: {
|
|
686
|
+
url: response["3ds_url"] || response.acs_url,
|
|
687
|
+
method: "POST",
|
|
688
|
+
params: response
|
|
689
|
+
},
|
|
690
|
+
message: response.response_message,
|
|
691
|
+
rawResponse: response
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
throw new APSException({
|
|
695
|
+
code: response.response_code?.toString() || "PAYMENT_ERROR",
|
|
696
|
+
message: response.response_message || "Payment failed",
|
|
697
|
+
rawResponse: response
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
transactionId: response.fort_id || orderId,
|
|
702
|
+
orderId,
|
|
703
|
+
status: "captured",
|
|
704
|
+
amount,
|
|
705
|
+
currency,
|
|
706
|
+
paymentMethod: response.payment_option,
|
|
707
|
+
message: response.response_message,
|
|
708
|
+
rawResponse: response
|
|
709
|
+
};
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (error instanceof APSException) {
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
const apsError = {
|
|
715
|
+
code: "CUSTOM_PAYMENT_ERROR",
|
|
716
|
+
message: error instanceof Error ? error.message : "Failed to process payment",
|
|
717
|
+
rawResponse: error
|
|
718
|
+
};
|
|
719
|
+
throw new APSException(apsError);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Alias for chargeWithToken for backwards compatibility
|
|
724
|
+
*/
|
|
725
|
+
async charge(options) {
|
|
726
|
+
if (options.cardToken) {
|
|
727
|
+
return this.chargeWithToken({
|
|
728
|
+
order: options.order,
|
|
729
|
+
tokenName: options.cardToken,
|
|
730
|
+
customerEmail: options.order.customer?.email || "customer@example.com",
|
|
731
|
+
returnUrl: options.returnUrl
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
throw new APSException({
|
|
735
|
+
code: "MISSING_TOKEN",
|
|
736
|
+
message: "Card token is required. Use getTokenizationForm() to collect card details first."
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Make API request to APS with timeout and retry support
|
|
741
|
+
*/
|
|
742
|
+
async makeApiRequest(url, body, idempotencyKey) {
|
|
743
|
+
return makeRequest(url, body, {
|
|
744
|
+
timeout: this.options.timeout,
|
|
745
|
+
maxRetries: this.options.maxRetries,
|
|
746
|
+
logger: this.options.logger,
|
|
747
|
+
idempotencyKey
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// src/modules/tokenization.ts
|
|
753
|
+
var TokenizationModule = class {
|
|
754
|
+
constructor(config, options) {
|
|
755
|
+
this.config = config;
|
|
756
|
+
this.options = options;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Tokenize card details
|
|
760
|
+
*/
|
|
761
|
+
async create(options) {
|
|
762
|
+
try {
|
|
763
|
+
validateRequiredFields(options, ["cardNumber", "expiryMonth", "expiryYear", "cvv"]);
|
|
764
|
+
if (!validateCardNumber(options.cardNumber)) {
|
|
765
|
+
throw new APSException({
|
|
766
|
+
code: "INVALID_CARD",
|
|
767
|
+
message: "Invalid card number format",
|
|
768
|
+
rawResponse: {}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (!validateExpiryDate(options.expiryMonth, options.expiryYear)) {
|
|
772
|
+
throw new APSException({
|
|
773
|
+
code: "INVALID_EXPIRY",
|
|
774
|
+
message: "Invalid or expired card",
|
|
775
|
+
rawResponse: {}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (!validateCVV(options.cvv)) {
|
|
779
|
+
throw new APSException({
|
|
780
|
+
code: "INVALID_CVV",
|
|
781
|
+
message: "Invalid CVV format",
|
|
782
|
+
rawResponse: {}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const params = {
|
|
786
|
+
command: "tokenize",
|
|
787
|
+
access_code: this.config.accessCode,
|
|
788
|
+
merchant_id: this.config.merchantId,
|
|
789
|
+
language: this.config.language,
|
|
790
|
+
response_format: "json"
|
|
791
|
+
};
|
|
792
|
+
params.card_number = options.cardNumber;
|
|
793
|
+
params.expiry_month = options.expiryMonth;
|
|
794
|
+
params.expiry_year = options.expiryYear;
|
|
795
|
+
params.cvv = options.cvv;
|
|
796
|
+
if (options.cardholderName) {
|
|
797
|
+
params.card_holder_name = options.cardholderName;
|
|
798
|
+
}
|
|
799
|
+
const signatureString = buildQueryString(params);
|
|
800
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
801
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
802
|
+
const response = await this.makeTokenizeRequest(`${apiUrl}/tokenize`, params);
|
|
803
|
+
if (response.response_code?.toString() !== "00") {
|
|
804
|
+
throw new APSException({
|
|
805
|
+
code: response.response_code?.toString() || "TOKENIZE_ERROR",
|
|
806
|
+
message: response.response_message || "Failed to tokenize card",
|
|
807
|
+
rawResponse: response
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
token: response.card_token,
|
|
812
|
+
last4: options.cardNumber.slice(-4),
|
|
813
|
+
brand: this.detectCardBrand(options.cardNumber),
|
|
814
|
+
expiryMonth: options.expiryMonth,
|
|
815
|
+
expiryYear: options.expiryYear
|
|
816
|
+
};
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (error instanceof APSException) {
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
const apsError = {
|
|
822
|
+
code: "TOKENIZE_ERROR",
|
|
823
|
+
message: error instanceof Error ? error.message : "Failed to tokenize card",
|
|
824
|
+
rawResponse: error
|
|
825
|
+
};
|
|
826
|
+
throw new APSException(apsError);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Verify a token (check if it's still valid)
|
|
831
|
+
*/
|
|
832
|
+
async verify(token) {
|
|
833
|
+
try {
|
|
834
|
+
const params = {
|
|
835
|
+
command: "verify_token",
|
|
836
|
+
access_code: this.config.accessCode,
|
|
837
|
+
merchant_id: this.config.merchantId,
|
|
838
|
+
card_token: token,
|
|
839
|
+
response_format: "json"
|
|
840
|
+
};
|
|
841
|
+
const signatureString = buildQueryString(params);
|
|
842
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
843
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
844
|
+
const response = await this.makeTokenizeRequest(`${apiUrl}/verify_token`, params);
|
|
845
|
+
return response.response_code?.toString() === "00";
|
|
846
|
+
} catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Delete/Invalidate a token
|
|
852
|
+
*/
|
|
853
|
+
async delete(token) {
|
|
854
|
+
try {
|
|
855
|
+
const params = {
|
|
856
|
+
command: "delete_token",
|
|
857
|
+
access_code: this.config.accessCode,
|
|
858
|
+
merchant_id: this.config.merchantId,
|
|
859
|
+
card_token: token,
|
|
860
|
+
response_format: "json"
|
|
861
|
+
};
|
|
862
|
+
const signatureString = buildQueryString(params);
|
|
863
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
864
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
865
|
+
const response = await this.makeTokenizeRequest(`${apiUrl}/delete_token`, params);
|
|
866
|
+
return response.response_code?.toString() === "00";
|
|
867
|
+
} catch {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* List saved cards for a customer
|
|
873
|
+
* @param customerId - Customer identifier (email or user ID)
|
|
874
|
+
*/
|
|
875
|
+
async list(customerId) {
|
|
876
|
+
try {
|
|
877
|
+
const params = {
|
|
878
|
+
command: "get_cards",
|
|
879
|
+
access_code: this.config.accessCode,
|
|
880
|
+
merchant_id: this.config.merchantId,
|
|
881
|
+
customer_email: customerId,
|
|
882
|
+
response_format: "json"
|
|
883
|
+
};
|
|
884
|
+
const signatureString = buildQueryString(params);
|
|
885
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
886
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
887
|
+
const response = await this.makeTokenizeRequest(`${apiUrl}/get_cards`, params);
|
|
888
|
+
const cards = [];
|
|
889
|
+
if (response.cards && Array.isArray(response.cards)) {
|
|
890
|
+
for (const card of response.cards) {
|
|
891
|
+
cards.push({
|
|
892
|
+
token: card.card_token,
|
|
893
|
+
last4: card.last4_digits || "****",
|
|
894
|
+
brand: card.card_brand || this.detectCardBrand(card.card_number || ""),
|
|
895
|
+
expiryMonth: card.expiry_month,
|
|
896
|
+
expiryYear: card.expiry_year,
|
|
897
|
+
cardholderName: card.card_holder_name,
|
|
898
|
+
isDefault: card.is_default === "true" || card.is_default === true
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
customerId,
|
|
904
|
+
cards,
|
|
905
|
+
total: cards.length
|
|
906
|
+
};
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error instanceof APSException) {
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
const apsError = {
|
|
912
|
+
code: "LIST_CARDS_ERROR",
|
|
913
|
+
message: error instanceof Error ? error.message : "Failed to list saved cards",
|
|
914
|
+
rawResponse: error
|
|
915
|
+
};
|
|
916
|
+
throw new APSException(apsError);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Detect card brand from card number
|
|
921
|
+
*/
|
|
922
|
+
detectCardBrand(cardNumber) {
|
|
923
|
+
const number = cardNumber.replace(/\D/g, "");
|
|
924
|
+
if (/^4/.test(number)) return "visa";
|
|
925
|
+
if (/^5[1-5]/.test(number)) return "mastercard";
|
|
926
|
+
if (/^3[47]/.test(number)) return "amex";
|
|
927
|
+
if (/^6(?:011|5)/.test(number)) return "discover";
|
|
928
|
+
if (/^(?:2131|1800|35)/.test(number)) return "jcb";
|
|
929
|
+
if (/^9792/.test(number)) return "troy";
|
|
930
|
+
return "unknown";
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Make tokenization API request with timeout and retry support
|
|
934
|
+
*/
|
|
935
|
+
async makeTokenizeRequest(url, params) {
|
|
936
|
+
const data = await makeRequest(url, params, {
|
|
937
|
+
timeout: this.options.timeout,
|
|
938
|
+
maxRetries: this.options.maxRetries,
|
|
939
|
+
logger: this.options.logger
|
|
940
|
+
});
|
|
941
|
+
if (data.response_code?.toString() !== "00" && data.response_code?.toString() !== "0") {
|
|
942
|
+
throw new APSException({
|
|
943
|
+
code: data.response_code?.toString() || "TOKENIZE_ERROR",
|
|
944
|
+
message: data.response_message || "Tokenization request failed",
|
|
945
|
+
rawResponse: data
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
return data;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/modules/payments.ts
|
|
953
|
+
var PaymentsModule = class {
|
|
954
|
+
constructor(config, options) {
|
|
955
|
+
this.config = config;
|
|
956
|
+
this.options = options;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Capture an authorized payment
|
|
960
|
+
*/
|
|
961
|
+
async capture(options) {
|
|
962
|
+
try {
|
|
963
|
+
validateRequiredFields(options, ["transactionId"]);
|
|
964
|
+
const params = {
|
|
965
|
+
command: "capture",
|
|
966
|
+
access_code: this.config.accessCode,
|
|
967
|
+
merchant_id: this.config.merchantId,
|
|
968
|
+
transaction_id: options.transactionId,
|
|
969
|
+
response_format: "json"
|
|
970
|
+
};
|
|
971
|
+
if (options.amount) {
|
|
972
|
+
params.amount = options.amount.toString();
|
|
973
|
+
}
|
|
974
|
+
const signatureString = buildQueryString(params);
|
|
975
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
976
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
977
|
+
const response = await this.makeApiRequest(`${apiUrl}/capture`, params);
|
|
978
|
+
const status = this.mapResponseStatus(response);
|
|
979
|
+
return {
|
|
980
|
+
captureId: response.transaction_id || options.transactionId,
|
|
981
|
+
transactionId: options.transactionId,
|
|
982
|
+
amount: options.amount || parseInt(response.amount) || 0,
|
|
983
|
+
status,
|
|
984
|
+
message: response.response_message,
|
|
985
|
+
rawResponse: response
|
|
986
|
+
};
|
|
987
|
+
} catch (error) {
|
|
988
|
+
const apsError = {
|
|
989
|
+
code: "CAPTURE_ERROR",
|
|
990
|
+
message: error instanceof Error ? error.message : "Failed to capture payment",
|
|
991
|
+
rawResponse: error
|
|
992
|
+
};
|
|
993
|
+
throw new APSException(apsError);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Refund a payment (full or partial)
|
|
998
|
+
*/
|
|
999
|
+
async refund(options) {
|
|
1000
|
+
try {
|
|
1001
|
+
validateRequiredFields(options, ["transactionId"]);
|
|
1002
|
+
const params = {
|
|
1003
|
+
command: "refund",
|
|
1004
|
+
access_code: this.config.accessCode,
|
|
1005
|
+
merchant_id: this.config.merchantId,
|
|
1006
|
+
transaction_id: options.transactionId,
|
|
1007
|
+
response_format: "json"
|
|
1008
|
+
};
|
|
1009
|
+
if (options.amount) {
|
|
1010
|
+
params.amount = options.amount.toString();
|
|
1011
|
+
}
|
|
1012
|
+
if (options.reason) {
|
|
1013
|
+
params.refund_reason = options.reason;
|
|
1014
|
+
}
|
|
1015
|
+
const signatureString = buildQueryString(params);
|
|
1016
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
1017
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
1018
|
+
const response = await this.makeApiRequest(`${apiUrl}/refund`, params);
|
|
1019
|
+
const status = this.mapResponseStatus(response);
|
|
1020
|
+
return {
|
|
1021
|
+
refundId: response.transaction_id || `refund_${Date.now()}`,
|
|
1022
|
+
transactionId: options.transactionId,
|
|
1023
|
+
amount: options.amount || parseInt(response.amount) || 0,
|
|
1024
|
+
status,
|
|
1025
|
+
message: response.response_message,
|
|
1026
|
+
rawResponse: response
|
|
1027
|
+
};
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
const apsError = {
|
|
1030
|
+
code: "REFUND_ERROR",
|
|
1031
|
+
message: error instanceof Error ? error.message : "Failed to process refund",
|
|
1032
|
+
rawResponse: error
|
|
1033
|
+
};
|
|
1034
|
+
throw new APSException(apsError);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Void an authorized transaction
|
|
1039
|
+
*/
|
|
1040
|
+
async void(options) {
|
|
1041
|
+
try {
|
|
1042
|
+
validateRequiredFields(options, ["transactionId"]);
|
|
1043
|
+
const params = {
|
|
1044
|
+
command: "void",
|
|
1045
|
+
access_code: this.config.accessCode,
|
|
1046
|
+
merchant_id: this.config.merchantId,
|
|
1047
|
+
transaction_id: options.transactionId,
|
|
1048
|
+
response_format: "json"
|
|
1049
|
+
};
|
|
1050
|
+
if (options.reason) {
|
|
1051
|
+
params.void_reason = options.reason;
|
|
1052
|
+
}
|
|
1053
|
+
const signatureString = buildQueryString(params);
|
|
1054
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
1055
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
1056
|
+
const response = await this.makeApiRequest(`${apiUrl}/void`, params);
|
|
1057
|
+
const status = this.mapResponseStatus(response);
|
|
1058
|
+
return {
|
|
1059
|
+
voidId: response.transaction_id || `void_${Date.now()}`,
|
|
1060
|
+
transactionId: options.transactionId,
|
|
1061
|
+
status,
|
|
1062
|
+
message: response.response_message,
|
|
1063
|
+
rawResponse: response
|
|
1064
|
+
};
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
const apsError = {
|
|
1067
|
+
code: "VOID_ERROR",
|
|
1068
|
+
message: error instanceof Error ? error.message : "Failed to void transaction",
|
|
1069
|
+
rawResponse: error
|
|
1070
|
+
};
|
|
1071
|
+
throw new APSException(apsError);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Query transaction status
|
|
1076
|
+
*/
|
|
1077
|
+
async query(options) {
|
|
1078
|
+
try {
|
|
1079
|
+
if (!options.transactionId && !options.orderId) {
|
|
1080
|
+
throw new Error("Either transactionId or orderId is required");
|
|
1081
|
+
}
|
|
1082
|
+
const params = {
|
|
1083
|
+
command: "query_transaction",
|
|
1084
|
+
access_code: this.config.accessCode,
|
|
1085
|
+
merchant_id: this.config.merchantId,
|
|
1086
|
+
response_format: "json"
|
|
1087
|
+
};
|
|
1088
|
+
if (options.transactionId) {
|
|
1089
|
+
params.transaction_id = options.transactionId;
|
|
1090
|
+
}
|
|
1091
|
+
if (options.orderId) {
|
|
1092
|
+
params.merchant_order_id = options.orderId;
|
|
1093
|
+
}
|
|
1094
|
+
const signatureString = buildQueryString(params);
|
|
1095
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
1096
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
1097
|
+
const response = await this.makeApiRequest(`${apiUrl}/query_transaction`, params);
|
|
1098
|
+
return {
|
|
1099
|
+
transactionId: response.transaction_id,
|
|
1100
|
+
orderId: response.merchant_order_id,
|
|
1101
|
+
amount: parseInt(response.amount) || 0,
|
|
1102
|
+
currency: response.currency,
|
|
1103
|
+
status: this.mapResponseStatus(response),
|
|
1104
|
+
paymentMethod: response.payment_option,
|
|
1105
|
+
createdAt: response.transaction_date,
|
|
1106
|
+
rawResponse: response
|
|
1107
|
+
};
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
const apsError = {
|
|
1110
|
+
code: "QUERY_ERROR",
|
|
1111
|
+
message: error instanceof Error ? error.message : "Failed to query transaction",
|
|
1112
|
+
rawResponse: error
|
|
1113
|
+
};
|
|
1114
|
+
throw new APSException(apsError);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Map APS response code to transaction status
|
|
1119
|
+
*/
|
|
1120
|
+
mapResponseStatus(response) {
|
|
1121
|
+
const code = response.response_code?.toString() || "";
|
|
1122
|
+
const status = response.status?.toUpperCase();
|
|
1123
|
+
if (code === "00" || code === "0") {
|
|
1124
|
+
if (status === "CAPTURED") return "captured";
|
|
1125
|
+
if (status === "AUTHORIZED") return "authorized";
|
|
1126
|
+
return "captured";
|
|
1127
|
+
}
|
|
1128
|
+
if (code === "03" || status === "AUTHORIZED") {
|
|
1129
|
+
return "authorized";
|
|
1130
|
+
}
|
|
1131
|
+
if (status === "PENDING" || status === "3DS") {
|
|
1132
|
+
return "pending";
|
|
1133
|
+
}
|
|
1134
|
+
if (status === "VOIDED" || response.void_status === "true") {
|
|
1135
|
+
return "voided";
|
|
1136
|
+
}
|
|
1137
|
+
if (status === "REFUNDED" || response.refund_status === "true") {
|
|
1138
|
+
return "refunded";
|
|
1139
|
+
}
|
|
1140
|
+
if (status === "CANCELLED" || status === "CANCELED") {
|
|
1141
|
+
return "cancelled";
|
|
1142
|
+
}
|
|
1143
|
+
if (status === "FAILED" || code !== "00") {
|
|
1144
|
+
return "failed";
|
|
1145
|
+
}
|
|
1146
|
+
return "pending";
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Make API request to APS with timeout and retry support
|
|
1150
|
+
*/
|
|
1151
|
+
async makeApiRequest(url, params, idempotencyKey) {
|
|
1152
|
+
const data = await makeRequest(url, params, {
|
|
1153
|
+
timeout: this.options.timeout,
|
|
1154
|
+
maxRetries: this.options.maxRetries,
|
|
1155
|
+
logger: this.options.logger,
|
|
1156
|
+
idempotencyKey
|
|
1157
|
+
});
|
|
1158
|
+
if (data.response_code?.toString() !== "00" && data.response_code?.toString() !== "0") {
|
|
1159
|
+
throw new APSException({
|
|
1160
|
+
code: data.response_code?.toString() || "API_ERROR",
|
|
1161
|
+
message: data.response_message || "API request failed",
|
|
1162
|
+
rawResponse: data
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
return data;
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// src/modules/webhooks.ts
|
|
1170
|
+
var WebhooksModule = class {
|
|
1171
|
+
constructor(config) {
|
|
1172
|
+
this.config = config;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Verify webhook signature
|
|
1176
|
+
*/
|
|
1177
|
+
verifySignature(payload, signature) {
|
|
1178
|
+
return verifyHMAC(payload, signature, this.config.responseSecret);
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Construct a webhook event from raw payload
|
|
1182
|
+
*/
|
|
1183
|
+
constructEvent(payload) {
|
|
1184
|
+
try {
|
|
1185
|
+
const eventType = this.mapEventType(payload);
|
|
1186
|
+
const status = this.mapStatus(payload.status);
|
|
1187
|
+
return {
|
|
1188
|
+
id: payload.transaction_id || `evt_${Date.now()}`,
|
|
1189
|
+
type: eventType,
|
|
1190
|
+
timestamp: payload.transaction_date ? new Date(payload.transaction_date) : /* @__PURE__ */ new Date(),
|
|
1191
|
+
data: {
|
|
1192
|
+
transactionId: payload.transaction_id,
|
|
1193
|
+
orderId: payload.merchant_order_id,
|
|
1194
|
+
amount: parseInt(payload.amount) || 0,
|
|
1195
|
+
currency: payload.currency,
|
|
1196
|
+
status,
|
|
1197
|
+
paymentMethod: payload.payment_option,
|
|
1198
|
+
metadata: payload.metadata
|
|
1199
|
+
},
|
|
1200
|
+
rawPayload: payload
|
|
1201
|
+
};
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
const apsError = {
|
|
1204
|
+
code: "WEBHOOK_PARSE_ERROR",
|
|
1205
|
+
message: error instanceof Error ? error.message : "Failed to parse webhook event",
|
|
1206
|
+
rawResponse: payload
|
|
1207
|
+
};
|
|
1208
|
+
throw new APSException(apsError);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Map APS event to webhook event type
|
|
1213
|
+
*/
|
|
1214
|
+
mapEventType(payload) {
|
|
1215
|
+
const status = payload.status?.toUpperCase();
|
|
1216
|
+
const command = payload.command;
|
|
1217
|
+
if (command === "refund") {
|
|
1218
|
+
if (status === "SUCCESS" || payload.response_code === "00") {
|
|
1219
|
+
return "refund.success";
|
|
1220
|
+
}
|
|
1221
|
+
return "refund.failed";
|
|
1222
|
+
}
|
|
1223
|
+
switch (status) {
|
|
1224
|
+
case "SUCCESS":
|
|
1225
|
+
case "CAPTURED":
|
|
1226
|
+
case "COMPLETED":
|
|
1227
|
+
return "payment.success";
|
|
1228
|
+
case "FAILED":
|
|
1229
|
+
case "DECLINED":
|
|
1230
|
+
return "payment.failed";
|
|
1231
|
+
case "PENDING":
|
|
1232
|
+
case "3DS":
|
|
1233
|
+
return "payment.pending";
|
|
1234
|
+
default:
|
|
1235
|
+
return "payment.pending";
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Map APS status to transaction status
|
|
1240
|
+
*/
|
|
1241
|
+
mapStatus(status) {
|
|
1242
|
+
if (!status) return "pending";
|
|
1243
|
+
const upperStatus = status.toUpperCase();
|
|
1244
|
+
switch (upperStatus) {
|
|
1245
|
+
case "SUCCESS":
|
|
1246
|
+
case "CAPTURED":
|
|
1247
|
+
case "COMPLETED":
|
|
1248
|
+
return "captured";
|
|
1249
|
+
case "AUTHORIZED":
|
|
1250
|
+
return "authorized";
|
|
1251
|
+
case "PENDING":
|
|
1252
|
+
case "3DS":
|
|
1253
|
+
return "pending";
|
|
1254
|
+
case "FAILED":
|
|
1255
|
+
case "DECLINED":
|
|
1256
|
+
return "failed";
|
|
1257
|
+
case "VOIDED":
|
|
1258
|
+
return "voided";
|
|
1259
|
+
case "REFUNDED":
|
|
1260
|
+
return "refunded";
|
|
1261
|
+
case "CANCELLED":
|
|
1262
|
+
case "CANCELED":
|
|
1263
|
+
return "cancelled";
|
|
1264
|
+
default:
|
|
1265
|
+
return "pending";
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Verify and construct event in one step
|
|
1270
|
+
*/
|
|
1271
|
+
safeConstructEvent(payload, signature) {
|
|
1272
|
+
if (!this.verifySignature(payload, signature)) {
|
|
1273
|
+
throw new APSException({
|
|
1274
|
+
code: "INVALID_SIGNATURE",
|
|
1275
|
+
message: "Webhook signature verification failed",
|
|
1276
|
+
statusCode: 401
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
const parsedPayload = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
1280
|
+
return this.constructEvent(parsedPayload);
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// src/modules/recurring.ts
|
|
1285
|
+
var RecurringModule = class {
|
|
1286
|
+
constructor(config, options) {
|
|
1287
|
+
this.config = config;
|
|
1288
|
+
this.options = options;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Process a recurring payment using a saved token
|
|
1292
|
+
*
|
|
1293
|
+
* This method is designed to be called from server-side code,
|
|
1294
|
+
* typically in a cron job or scheduled task for subscription billing.
|
|
1295
|
+
*
|
|
1296
|
+
* @param options - Recurring payment options
|
|
1297
|
+
* @returns Recurring payment response
|
|
1298
|
+
*
|
|
1299
|
+
* @example
|
|
1300
|
+
* ```typescript
|
|
1301
|
+
* // Monthly subscription charge
|
|
1302
|
+
* const payment = await aps.recurring.process({
|
|
1303
|
+
* order: {
|
|
1304
|
+
* id: `sub_${customerId}_${Date.now()}`,
|
|
1305
|
+
* amount: 2999, // $29.99
|
|
1306
|
+
* currency: 'USD',
|
|
1307
|
+
* description: 'Monthly Pro Plan'
|
|
1308
|
+
* },
|
|
1309
|
+
* tokenName: savedCard.token,
|
|
1310
|
+
* agreementId: savedCard.agreementId,
|
|
1311
|
+
* customerEmail: customer.email,
|
|
1312
|
+
* customerName: customer.name
|
|
1313
|
+
* });
|
|
1314
|
+
*
|
|
1315
|
+
* if (payment.status === 'captured') {
|
|
1316
|
+
* await sendInvoice(customer, payment);
|
|
1317
|
+
* } else {
|
|
1318
|
+
* await notifyPaymentFailed(customer, payment);
|
|
1319
|
+
* }
|
|
1320
|
+
* ```
|
|
1321
|
+
*/
|
|
1322
|
+
async process(options) {
|
|
1323
|
+
try {
|
|
1324
|
+
validateRequiredFields(options, ["tokenName", "agreementId", "customerEmail"]);
|
|
1325
|
+
validateRequiredFields(options.order, ["id", "amount", "currency"]);
|
|
1326
|
+
const params = {
|
|
1327
|
+
command: "PURCHASE",
|
|
1328
|
+
access_code: this.config.accessCode,
|
|
1329
|
+
merchant_identifier: this.config.merchantId,
|
|
1330
|
+
merchant_reference: options.order.id,
|
|
1331
|
+
amount: Math.round(options.order.amount).toString(),
|
|
1332
|
+
currency: options.order.currency,
|
|
1333
|
+
eci: "RECURRING",
|
|
1334
|
+
language: this.config.language,
|
|
1335
|
+
customer_email: options.customerEmail,
|
|
1336
|
+
token_name: options.tokenName,
|
|
1337
|
+
agreement_id: options.agreementId,
|
|
1338
|
+
response_format: "json"
|
|
1339
|
+
};
|
|
1340
|
+
if (options.order.description) {
|
|
1341
|
+
params.order_description = options.order.description;
|
|
1342
|
+
}
|
|
1343
|
+
if (options.customerName) {
|
|
1344
|
+
params.customer_name = options.customerName;
|
|
1345
|
+
}
|
|
1346
|
+
if (options.customerPhone) {
|
|
1347
|
+
params.phone_number = options.customerPhone;
|
|
1348
|
+
}
|
|
1349
|
+
if (options.statementDescriptor) {
|
|
1350
|
+
params.statement_descriptor = options.statementDescriptor;
|
|
1351
|
+
}
|
|
1352
|
+
if (options.paymentOption) {
|
|
1353
|
+
params.payment_option = options.paymentOption;
|
|
1354
|
+
}
|
|
1355
|
+
if (options.settlementReference) {
|
|
1356
|
+
params.settlement_reference = options.settlementReference;
|
|
1357
|
+
}
|
|
1358
|
+
const signatureString = buildQueryString(params);
|
|
1359
|
+
params.signature = generateHMAC(signatureString, this.config.requestSecret);
|
|
1360
|
+
this.options.logger?.info?.("Processing recurring payment", {
|
|
1361
|
+
orderId: options.order.id,
|
|
1362
|
+
amount: options.order.amount,
|
|
1363
|
+
currency: options.order.currency,
|
|
1364
|
+
agreementId: options.agreementId
|
|
1365
|
+
});
|
|
1366
|
+
const apiUrl = getApiUrl(this.config.environment);
|
|
1367
|
+
const response = await this.makeApiRequest(`${apiUrl}/paymentApi`, params);
|
|
1368
|
+
const status = this.mapResponseStatus(response);
|
|
1369
|
+
return {
|
|
1370
|
+
transactionId: response.fort_id || response.transaction_id,
|
|
1371
|
+
orderId: options.order.id,
|
|
1372
|
+
agreementId: options.agreementId,
|
|
1373
|
+
amount: options.order.amount,
|
|
1374
|
+
currency: options.order.currency,
|
|
1375
|
+
status,
|
|
1376
|
+
paymentMethod: response.payment_option,
|
|
1377
|
+
authorizationCode: response.authorization_code,
|
|
1378
|
+
message: response.response_message,
|
|
1379
|
+
rawResponse: response
|
|
1380
|
+
};
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
this.options.logger?.error?.("Recurring payment failed", { error });
|
|
1383
|
+
const apsError = {
|
|
1384
|
+
code: "RECURRING_ERROR",
|
|
1385
|
+
message: error instanceof Error ? error.message : "Failed to process recurring payment",
|
|
1386
|
+
rawResponse: error
|
|
1387
|
+
};
|
|
1388
|
+
throw new APSException(apsError);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Process multiple recurring payments in batch
|
|
1393
|
+
*
|
|
1394
|
+
* Useful for processing all due subscriptions at once.
|
|
1395
|
+
* Returns results for all payments, including failures.
|
|
1396
|
+
*
|
|
1397
|
+
* @param payments - Array of recurring payment options
|
|
1398
|
+
* @returns Array of results with success/failure status
|
|
1399
|
+
*
|
|
1400
|
+
* @example
|
|
1401
|
+
* ```typescript
|
|
1402
|
+
* // Process all due subscriptions
|
|
1403
|
+
* const dueSubscriptions = await getDueSubscriptions();
|
|
1404
|
+
*
|
|
1405
|
+
* const results = await aps.recurring.processBatch(
|
|
1406
|
+
* dueSubscriptions.map(sub => ({
|
|
1407
|
+
* order: {
|
|
1408
|
+
* id: `sub_${sub.id}_${Date.now()}`,
|
|
1409
|
+
* amount: sub.amount,
|
|
1410
|
+
* currency: sub.currency,
|
|
1411
|
+
* description: sub.planName
|
|
1412
|
+
* },
|
|
1413
|
+
* tokenName: sub.tokenName,
|
|
1414
|
+
* agreementId: sub.agreementId,
|
|
1415
|
+
* customerEmail: sub.customerEmail
|
|
1416
|
+
* }))
|
|
1417
|
+
* );
|
|
1418
|
+
*
|
|
1419
|
+
* // Handle results
|
|
1420
|
+
* const successful = results.filter(r => r.success);
|
|
1421
|
+
* const failed = results.filter(r => !r.success);
|
|
1422
|
+
*
|
|
1423
|
+
* console.log(`Processed: ${successful.length} succeeded, ${failed.length} failed`);
|
|
1424
|
+
* ```
|
|
1425
|
+
*/
|
|
1426
|
+
async processBatch(payments) {
|
|
1427
|
+
const results = [];
|
|
1428
|
+
for (const payment of payments) {
|
|
1429
|
+
try {
|
|
1430
|
+
const result = await this.process(payment);
|
|
1431
|
+
results.push({
|
|
1432
|
+
success: true,
|
|
1433
|
+
data: result,
|
|
1434
|
+
orderId: payment.order.id
|
|
1435
|
+
});
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
results.push({
|
|
1438
|
+
success: false,
|
|
1439
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1440
|
+
orderId: payment.order.id
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return results;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Map APS response code to transaction status
|
|
1448
|
+
*/
|
|
1449
|
+
mapResponseStatus(response) {
|
|
1450
|
+
const code = response.response_code?.toString() || "";
|
|
1451
|
+
const status = response.status?.toString();
|
|
1452
|
+
if (code === "14000" || code === "00000" || status === "20") {
|
|
1453
|
+
return "captured";
|
|
1454
|
+
}
|
|
1455
|
+
if (status === "3" || status === "PENDING") {
|
|
1456
|
+
return "pending";
|
|
1457
|
+
}
|
|
1458
|
+
return "failed";
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Make API request to APS
|
|
1462
|
+
*/
|
|
1463
|
+
async makeApiRequest(url, params) {
|
|
1464
|
+
const data = await makeRequest(url, params, {
|
|
1465
|
+
timeout: this.options.timeout,
|
|
1466
|
+
maxRetries: this.options.maxRetries,
|
|
1467
|
+
logger: this.options.logger
|
|
1468
|
+
});
|
|
1469
|
+
if (data.response_code?.toString() !== "14000" && data.response_code?.toString() !== "00000" && data.status?.toString() !== "20") {
|
|
1470
|
+
throw new APSException({
|
|
1471
|
+
code: data.response_code?.toString() || "API_ERROR",
|
|
1472
|
+
message: data.response_message || "Recurring payment failed",
|
|
1473
|
+
rawResponse: data
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
return data;
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/client.ts
|
|
1481
|
+
var APSClient = class {
|
|
1482
|
+
/**
|
|
1483
|
+
* Create a new APS client instance
|
|
1484
|
+
*/
|
|
1485
|
+
constructor(config, options = {}) {
|
|
1486
|
+
if (!config.merchantId) {
|
|
1487
|
+
throw new APSException({
|
|
1488
|
+
code: "MISSING_MERCHANT_ID",
|
|
1489
|
+
message: "Merchant ID is required"
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
if (!config.accessCode) {
|
|
1493
|
+
throw new APSException({
|
|
1494
|
+
code: "MISSING_ACCESS_CODE",
|
|
1495
|
+
message: "Access code is required"
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
if (!config.requestSecret) {
|
|
1499
|
+
throw new APSException({
|
|
1500
|
+
code: "MISSING_REQUEST_SECRET",
|
|
1501
|
+
message: "Request secret is required"
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
if (!config.responseSecret) {
|
|
1505
|
+
throw new APSException({
|
|
1506
|
+
code: "MISSING_RESPONSE_SECRET",
|
|
1507
|
+
message: "Response secret is required"
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
this.config = {
|
|
1511
|
+
...config,
|
|
1512
|
+
environment: config.environment ?? "sandbox",
|
|
1513
|
+
currency: config.currency ?? "USD",
|
|
1514
|
+
language: config.language ?? "en"
|
|
1515
|
+
};
|
|
1516
|
+
this.options = {
|
|
1517
|
+
logger: options.logger ?? (this.config.environment === "sandbox" ? noopLogger : noopLogger),
|
|
1518
|
+
timeout: options.timeout ?? 3e4,
|
|
1519
|
+
maxRetries: options.maxRetries ?? 3,
|
|
1520
|
+
enableLogging: options.enableLogging ?? this.config.environment === "sandbox"
|
|
1521
|
+
};
|
|
1522
|
+
this.paymentLinks = new PaymentLinksModule(this.config, this.options);
|
|
1523
|
+
this.hostedCheckout = new HostedCheckoutModule(this.config, this.options);
|
|
1524
|
+
this.customPaymentPage = new CustomPaymentPageModule(this.config, this.options);
|
|
1525
|
+
this.tokens = new TokenizationModule(this.config, this.options);
|
|
1526
|
+
this.payments = new PaymentsModule(this.config, this.options);
|
|
1527
|
+
this.webhooks = new WebhooksModule(this.config);
|
|
1528
|
+
this.recurring = new RecurringModule(this.config, this.options);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Get the base URL for redirects
|
|
1532
|
+
*/
|
|
1533
|
+
getBaseUrl() {
|
|
1534
|
+
return getBaseUrl(this.config.environment);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Get the API URL for server-to-server calls
|
|
1538
|
+
*/
|
|
1539
|
+
getApiUrl() {
|
|
1540
|
+
return getApiUrl(this.config.environment);
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Verify a webhook signature
|
|
1544
|
+
*/
|
|
1545
|
+
verifyWebhookSignature(payload, signature) {
|
|
1546
|
+
return this.webhooks.verifySignature(payload, signature);
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Construct a webhook event from raw payload
|
|
1550
|
+
*/
|
|
1551
|
+
constructWebhookEvent(payload) {
|
|
1552
|
+
return this.webhooks.constructEvent(payload);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Get the logger instance
|
|
1556
|
+
*/
|
|
1557
|
+
getLogger() {
|
|
1558
|
+
return this.options.logger;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Set a custom logger
|
|
1562
|
+
*/
|
|
1563
|
+
setLogger(logger) {
|
|
1564
|
+
this.options.logger = logger;
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// src/constants.ts
|
|
1569
|
+
var TestCards = {
|
|
1570
|
+
/**
|
|
1571
|
+
* Visa test cards
|
|
1572
|
+
*/
|
|
1573
|
+
VISA: {
|
|
1574
|
+
/** Successful payment */
|
|
1575
|
+
SUCCESS: "4111111111111111",
|
|
1576
|
+
/** Declined payment */
|
|
1577
|
+
DECLINED: "4000000000000002",
|
|
1578
|
+
/** Insufficient funds */
|
|
1579
|
+
INSUFFICIENT_FUNDS: "4000000000009995",
|
|
1580
|
+
/** Lost card */
|
|
1581
|
+
LOST_CARD: "4000000000009987",
|
|
1582
|
+
/** Stolen card */
|
|
1583
|
+
STOLEN_CARD: "4000000000009979",
|
|
1584
|
+
/** Expired card */
|
|
1585
|
+
EXPIRED_CARD: "4000000000009961",
|
|
1586
|
+
/** Invalid CVV */
|
|
1587
|
+
INVALID_CVV: "4000000000000127",
|
|
1588
|
+
/** 3D Secure required */
|
|
1589
|
+
REQUIRES_3DS: "4000000000003220"
|
|
1590
|
+
},
|
|
1591
|
+
/**
|
|
1592
|
+
* Mastercard test cards
|
|
1593
|
+
*/
|
|
1594
|
+
MASTERCARD: {
|
|
1595
|
+
/** Successful payment */
|
|
1596
|
+
SUCCESS: "5100000000000008",
|
|
1597
|
+
/** Declined payment */
|
|
1598
|
+
DECLINED: "5400000000000003",
|
|
1599
|
+
/** Insufficient funds */
|
|
1600
|
+
INSUFFICIENT_FUNDS: "5400000000000094",
|
|
1601
|
+
/** 3D Secure required */
|
|
1602
|
+
REQUIRES_3DS: "5400000000000094"
|
|
1603
|
+
},
|
|
1604
|
+
/**
|
|
1605
|
+
* MADA (Saudi Arabia) test cards
|
|
1606
|
+
*/
|
|
1607
|
+
MADA: {
|
|
1608
|
+
/** Successful payment */
|
|
1609
|
+
SUCCESS: "5297410000000002",
|
|
1610
|
+
/** Declined payment */
|
|
1611
|
+
DECLINED: "5297410000000010",
|
|
1612
|
+
/** 3D Secure required */
|
|
1613
|
+
REQUIRES_3DS: "5297410000000028"
|
|
1614
|
+
},
|
|
1615
|
+
/**
|
|
1616
|
+
* American Express test cards
|
|
1617
|
+
*/
|
|
1618
|
+
AMEX: {
|
|
1619
|
+
/** Successful payment */
|
|
1620
|
+
SUCCESS: "371449635398431",
|
|
1621
|
+
/** Declined payment */
|
|
1622
|
+
DECLINED: "378282246310005"
|
|
1623
|
+
},
|
|
1624
|
+
/**
|
|
1625
|
+
* Test card defaults
|
|
1626
|
+
*/
|
|
1627
|
+
DEFAULTS: {
|
|
1628
|
+
/** Default expiry month (MM) */
|
|
1629
|
+
EXPIRY_MONTH: "12",
|
|
1630
|
+
/** Default expiry year (YY) */
|
|
1631
|
+
EXPIRY_YEAR: "25",
|
|
1632
|
+
/** Default CVV */
|
|
1633
|
+
CVV: "123",
|
|
1634
|
+
/** Default cardholder name */
|
|
1635
|
+
CARDHOLDER_NAME: "Test User"
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
var ResponseCodes = {
|
|
1639
|
+
// Success Codes
|
|
1640
|
+
/** Transaction successful */
|
|
1641
|
+
SUCCESS: "00000",
|
|
1642
|
+
/** Payment link created successfully */
|
|
1643
|
+
PAYMENT_LINK_SUCCESS: "48000",
|
|
1644
|
+
/** Recurring payment successful */
|
|
1645
|
+
RECURRING_SUCCESS: "14000",
|
|
1646
|
+
// Authentication & Security
|
|
1647
|
+
/** 3D Secure authentication failed */
|
|
1648
|
+
AUTHENTICATION_FAILED: "10030",
|
|
1649
|
+
/** 3D Secure not enrolled */
|
|
1650
|
+
NOT_ENROLLED_3DS: "10031",
|
|
1651
|
+
/** Signature mismatch */
|
|
1652
|
+
SIGNATURE_MISMATCH: "00008",
|
|
1653
|
+
/** Invalid access code */
|
|
1654
|
+
INVALID_ACCESS_CODE: "00009",
|
|
1655
|
+
/** Invalid merchant identifier */
|
|
1656
|
+
INVALID_MERCHANT_ID: "00010",
|
|
1657
|
+
// Card Errors
|
|
1658
|
+
/** Card expired */
|
|
1659
|
+
CARD_EXPIRED: "10035",
|
|
1660
|
+
/** Invalid card number */
|
|
1661
|
+
INVALID_CARD_NUMBER: "10036",
|
|
1662
|
+
/** Invalid CVV */
|
|
1663
|
+
INVALID_CVV: "10037",
|
|
1664
|
+
/** Card not supported */
|
|
1665
|
+
CARD_NOT_SUPPORTED: "10038",
|
|
1666
|
+
/** Lost card */
|
|
1667
|
+
LOST_CARD: "10039",
|
|
1668
|
+
/** Stolen card */
|
|
1669
|
+
STOLEN_CARD: "10040",
|
|
1670
|
+
/** Restricted card */
|
|
1671
|
+
RESTRICTED_CARD: "10041",
|
|
1672
|
+
/** Card blocked */
|
|
1673
|
+
CARD_BLOCKED: "10042",
|
|
1674
|
+
/** Invalid expiry date */
|
|
1675
|
+
INVALID_EXPIRY: "10043",
|
|
1676
|
+
// Transaction Errors
|
|
1677
|
+
/** Transaction declined */
|
|
1678
|
+
DECLINED: "14001",
|
|
1679
|
+
/** Insufficient funds */
|
|
1680
|
+
INSUFFICIENT_FUNDS: "14002",
|
|
1681
|
+
/** Transaction limit exceeded */
|
|
1682
|
+
LIMIT_EXCEEDED: "14003",
|
|
1683
|
+
/** Invalid transaction */
|
|
1684
|
+
INVALID_TRANSACTION: "14004",
|
|
1685
|
+
/** Duplicate transaction */
|
|
1686
|
+
DUPLICATE_TRANSACTION: "14005",
|
|
1687
|
+
/** Transaction not allowed */
|
|
1688
|
+
TRANSACTION_NOT_ALLOWED: "14006",
|
|
1689
|
+
/** Transaction expired */
|
|
1690
|
+
TRANSACTION_EXPIRED: "14007",
|
|
1691
|
+
/** Transaction already captured */
|
|
1692
|
+
ALREADY_CAPTURED: "14008",
|
|
1693
|
+
/** Transaction already voided */
|
|
1694
|
+
ALREADY_VOIDED: "14009",
|
|
1695
|
+
/** Transaction already refunded */
|
|
1696
|
+
ALREADY_REFUNDED: "14010",
|
|
1697
|
+
// Amount Errors
|
|
1698
|
+
/** Invalid amount */
|
|
1699
|
+
INVALID_AMOUNT: "15001",
|
|
1700
|
+
/** Amount too small */
|
|
1701
|
+
AMOUNT_TOO_SMALL: "15002",
|
|
1702
|
+
/** Amount too large */
|
|
1703
|
+
AMOUNT_TOO_LARGE: "15003",
|
|
1704
|
+
/** Currency not supported */
|
|
1705
|
+
CURRENCY_NOT_SUPPORTED: "15004",
|
|
1706
|
+
// Parameter Errors
|
|
1707
|
+
/** Missing parameter */
|
|
1708
|
+
MISSING_PARAMETER: "00001",
|
|
1709
|
+
/** Invalid parameter format */
|
|
1710
|
+
INVALID_PARAMETER: "00002",
|
|
1711
|
+
/** Invalid command */
|
|
1712
|
+
INVALID_COMMAND: "00003",
|
|
1713
|
+
/** Invalid language */
|
|
1714
|
+
INVALID_LANGUAGE: "00004",
|
|
1715
|
+
/** Invalid currency */
|
|
1716
|
+
INVALID_CURRENCY: "00005",
|
|
1717
|
+
/** Invalid return URL */
|
|
1718
|
+
INVALID_RETURN_URL: "00006",
|
|
1719
|
+
/** Invalid customer email */
|
|
1720
|
+
INVALID_EMAIL: "00007",
|
|
1721
|
+
/** Parameter value not allowed */
|
|
1722
|
+
PARAMETER_VALUE_NOT_ALLOWED: "00033",
|
|
1723
|
+
// Tokenization Errors
|
|
1724
|
+
/** Token not found */
|
|
1725
|
+
TOKEN_NOT_FOUND: "16001",
|
|
1726
|
+
/** Token expired */
|
|
1727
|
+
TOKEN_EXPIRED: "16002",
|
|
1728
|
+
/** Token already used */
|
|
1729
|
+
TOKEN_ALREADY_USED: "16003",
|
|
1730
|
+
/** Invalid token */
|
|
1731
|
+
INVALID_TOKEN: "16004",
|
|
1732
|
+
// System Errors
|
|
1733
|
+
/** System error */
|
|
1734
|
+
SYSTEM_ERROR: "90001",
|
|
1735
|
+
/** Service unavailable */
|
|
1736
|
+
SERVICE_UNAVAILABLE: "90002",
|
|
1737
|
+
/** Timeout */
|
|
1738
|
+
TIMEOUT: "90003",
|
|
1739
|
+
/** Database error */
|
|
1740
|
+
DATABASE_ERROR: "90004",
|
|
1741
|
+
/** Internal error */
|
|
1742
|
+
INTERNAL_ERROR: "90005"
|
|
1743
|
+
};
|
|
1744
|
+
var ErrorCategories = {
|
|
1745
|
+
/** Success - no error */
|
|
1746
|
+
SUCCESS: "success",
|
|
1747
|
+
/** Card-related errors (expired, invalid, blocked) */
|
|
1748
|
+
CARD_ERROR: "card_error",
|
|
1749
|
+
/** Authentication errors (3DS, signature) */
|
|
1750
|
+
AUTHENTICATION_ERROR: "authentication_error",
|
|
1751
|
+
/** Transaction errors (declined, insufficient funds) */
|
|
1752
|
+
TRANSACTION_ERROR: "transaction_error",
|
|
1753
|
+
/** Amount/currency errors */
|
|
1754
|
+
AMOUNT_ERROR: "amount_error",
|
|
1755
|
+
/** Parameter validation errors */
|
|
1756
|
+
VALIDATION_ERROR: "validation_error",
|
|
1757
|
+
/** Tokenization errors */
|
|
1758
|
+
TOKEN_ERROR: "token_error",
|
|
1759
|
+
/** System/technical errors */
|
|
1760
|
+
SYSTEM_ERROR: "system_error",
|
|
1761
|
+
/** Errors that can be retried */
|
|
1762
|
+
RETRYABLE: "retryable",
|
|
1763
|
+
/** Errors that should not be retried */
|
|
1764
|
+
NON_RETRYABLE: "non_retryable"
|
|
1765
|
+
};
|
|
1766
|
+
var ERROR_CODE_TO_CATEGORY = {
|
|
1767
|
+
[ResponseCodes.SUCCESS]: ErrorCategories.SUCCESS,
|
|
1768
|
+
[ResponseCodes.PAYMENT_LINK_SUCCESS]: ErrorCategories.SUCCESS,
|
|
1769
|
+
[ResponseCodes.RECURRING_SUCCESS]: ErrorCategories.SUCCESS,
|
|
1770
|
+
// Card errors
|
|
1771
|
+
[ResponseCodes.CARD_EXPIRED]: ErrorCategories.CARD_ERROR,
|
|
1772
|
+
[ResponseCodes.INVALID_CARD_NUMBER]: ErrorCategories.CARD_ERROR,
|
|
1773
|
+
[ResponseCodes.INVALID_CVV]: ErrorCategories.CARD_ERROR,
|
|
1774
|
+
[ResponseCodes.CARD_NOT_SUPPORTED]: ErrorCategories.CARD_ERROR,
|
|
1775
|
+
[ResponseCodes.LOST_CARD]: ErrorCategories.CARD_ERROR,
|
|
1776
|
+
[ResponseCodes.STOLEN_CARD]: ErrorCategories.CARD_ERROR,
|
|
1777
|
+
[ResponseCodes.RESTRICTED_CARD]: ErrorCategories.CARD_ERROR,
|
|
1778
|
+
[ResponseCodes.CARD_BLOCKED]: ErrorCategories.CARD_ERROR,
|
|
1779
|
+
[ResponseCodes.INVALID_EXPIRY]: ErrorCategories.CARD_ERROR,
|
|
1780
|
+
// Authentication errors
|
|
1781
|
+
[ResponseCodes.AUTHENTICATION_FAILED]: ErrorCategories.AUTHENTICATION_ERROR,
|
|
1782
|
+
[ResponseCodes.NOT_ENROLLED_3DS]: ErrorCategories.AUTHENTICATION_ERROR,
|
|
1783
|
+
[ResponseCodes.SIGNATURE_MISMATCH]: ErrorCategories.AUTHENTICATION_ERROR,
|
|
1784
|
+
[ResponseCodes.INVALID_ACCESS_CODE]: ErrorCategories.AUTHENTICATION_ERROR,
|
|
1785
|
+
[ResponseCodes.INVALID_MERCHANT_ID]: ErrorCategories.AUTHENTICATION_ERROR,
|
|
1786
|
+
// Transaction errors
|
|
1787
|
+
[ResponseCodes.DECLINED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1788
|
+
[ResponseCodes.INSUFFICIENT_FUNDS]: ErrorCategories.TRANSACTION_ERROR,
|
|
1789
|
+
[ResponseCodes.LIMIT_EXCEEDED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1790
|
+
[ResponseCodes.INVALID_TRANSACTION]: ErrorCategories.TRANSACTION_ERROR,
|
|
1791
|
+
[ResponseCodes.DUPLICATE_TRANSACTION]: ErrorCategories.TRANSACTION_ERROR,
|
|
1792
|
+
[ResponseCodes.TRANSACTION_NOT_ALLOWED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1793
|
+
[ResponseCodes.TRANSACTION_EXPIRED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1794
|
+
[ResponseCodes.ALREADY_CAPTURED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1795
|
+
[ResponseCodes.ALREADY_VOIDED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1796
|
+
[ResponseCodes.ALREADY_REFUNDED]: ErrorCategories.TRANSACTION_ERROR,
|
|
1797
|
+
// Amount errors
|
|
1798
|
+
[ResponseCodes.INVALID_AMOUNT]: ErrorCategories.AMOUNT_ERROR,
|
|
1799
|
+
[ResponseCodes.AMOUNT_TOO_SMALL]: ErrorCategories.AMOUNT_ERROR,
|
|
1800
|
+
[ResponseCodes.AMOUNT_TOO_LARGE]: ErrorCategories.AMOUNT_ERROR,
|
|
1801
|
+
[ResponseCodes.CURRENCY_NOT_SUPPORTED]: ErrorCategories.AMOUNT_ERROR,
|
|
1802
|
+
// Validation errors
|
|
1803
|
+
[ResponseCodes.MISSING_PARAMETER]: ErrorCategories.VALIDATION_ERROR,
|
|
1804
|
+
[ResponseCodes.INVALID_PARAMETER]: ErrorCategories.VALIDATION_ERROR,
|
|
1805
|
+
[ResponseCodes.INVALID_COMMAND]: ErrorCategories.VALIDATION_ERROR,
|
|
1806
|
+
[ResponseCodes.INVALID_LANGUAGE]: ErrorCategories.VALIDATION_ERROR,
|
|
1807
|
+
[ResponseCodes.INVALID_CURRENCY]: ErrorCategories.VALIDATION_ERROR,
|
|
1808
|
+
[ResponseCodes.INVALID_RETURN_URL]: ErrorCategories.VALIDATION_ERROR,
|
|
1809
|
+
[ResponseCodes.INVALID_EMAIL]: ErrorCategories.VALIDATION_ERROR,
|
|
1810
|
+
[ResponseCodes.PARAMETER_VALUE_NOT_ALLOWED]: ErrorCategories.VALIDATION_ERROR,
|
|
1811
|
+
// Token errors
|
|
1812
|
+
[ResponseCodes.TOKEN_NOT_FOUND]: ErrorCategories.TOKEN_ERROR,
|
|
1813
|
+
[ResponseCodes.TOKEN_EXPIRED]: ErrorCategories.TOKEN_ERROR,
|
|
1814
|
+
[ResponseCodes.TOKEN_ALREADY_USED]: ErrorCategories.TOKEN_ERROR,
|
|
1815
|
+
[ResponseCodes.INVALID_TOKEN]: ErrorCategories.TOKEN_ERROR,
|
|
1816
|
+
// System errors
|
|
1817
|
+
[ResponseCodes.SYSTEM_ERROR]: ErrorCategories.SYSTEM_ERROR,
|
|
1818
|
+
[ResponseCodes.SERVICE_UNAVAILABLE]: ErrorCategories.SYSTEM_ERROR,
|
|
1819
|
+
[ResponseCodes.TIMEOUT]: ErrorCategories.SYSTEM_ERROR,
|
|
1820
|
+
[ResponseCodes.DATABASE_ERROR]: ErrorCategories.SYSTEM_ERROR,
|
|
1821
|
+
[ResponseCodes.INTERNAL_ERROR]: ErrorCategories.SYSTEM_ERROR
|
|
1822
|
+
};
|
|
1823
|
+
var RETRYABLE_CODES = [
|
|
1824
|
+
ResponseCodes.TIMEOUT,
|
|
1825
|
+
ResponseCodes.SERVICE_UNAVAILABLE,
|
|
1826
|
+
ResponseCodes.SYSTEM_ERROR,
|
|
1827
|
+
ResponseCodes.INTERNAL_ERROR,
|
|
1828
|
+
ResponseCodes.DATABASE_ERROR
|
|
1829
|
+
];
|
|
1830
|
+
function categorizeError(code) {
|
|
1831
|
+
return ERROR_CODE_TO_CATEGORY[code] || ErrorCategories.SYSTEM_ERROR;
|
|
1832
|
+
}
|
|
1833
|
+
function isRetryableError(code) {
|
|
1834
|
+
return RETRYABLE_CODES.includes(code);
|
|
1835
|
+
}
|
|
1836
|
+
var PaymentMethodCodes = {
|
|
1837
|
+
/** Visa */
|
|
1838
|
+
VISA: "VISA",
|
|
1839
|
+
/** Mastercard */
|
|
1840
|
+
MASTERCARD: "MASTERCARD",
|
|
1841
|
+
/** American Express */
|
|
1842
|
+
AMEX: "AMEX",
|
|
1843
|
+
/** MADA (Saudi Arabia) */
|
|
1844
|
+
MADA: "MADA",
|
|
1845
|
+
/** Apple Pay */
|
|
1846
|
+
APPLE_PAY: "APPLE_PAY",
|
|
1847
|
+
/** STC Pay (Saudi Arabia) */
|
|
1848
|
+
STC_PAY: "STC_PAY",
|
|
1849
|
+
/** KNET (Kuwait) */
|
|
1850
|
+
KNET: "KNET",
|
|
1851
|
+
/** NAPS (Qatar) */
|
|
1852
|
+
NAPS: "NAPS",
|
|
1853
|
+
/** Fawry (Egypt) */
|
|
1854
|
+
FAWRY: "FAWRY",
|
|
1855
|
+
/** Meeza (Egypt) */
|
|
1856
|
+
MEEZA: "MEEZA",
|
|
1857
|
+
/** Sadad (Saudi Arabia) */
|
|
1858
|
+
SADAD: "SADAD",
|
|
1859
|
+
/** Aman (Egypt) */
|
|
1860
|
+
AMAN: "AMAN",
|
|
1861
|
+
/** Tabby (BNPL) */
|
|
1862
|
+
TABBY: "TABBY",
|
|
1863
|
+
/** Tamara (BNPL) */
|
|
1864
|
+
TAMARA: "TAMARA",
|
|
1865
|
+
/** valU (BNPL) */
|
|
1866
|
+
VALU: "VALU"
|
|
1867
|
+
};
|
|
1868
|
+
var CurrencyCodes = {
|
|
1869
|
+
/** Saudi Riyal */
|
|
1870
|
+
SAR: "SAR",
|
|
1871
|
+
/** UAE Dirham */
|
|
1872
|
+
AED: "AED",
|
|
1873
|
+
/** Kuwaiti Dinar */
|
|
1874
|
+
KWD: "KWD",
|
|
1875
|
+
/** Qatari Riyal */
|
|
1876
|
+
QAR: "QAR",
|
|
1877
|
+
/** Bahraini Dinar */
|
|
1878
|
+
BHD: "BHD",
|
|
1879
|
+
/** Omani Rial */
|
|
1880
|
+
OMR: "OMR",
|
|
1881
|
+
/** Jordanian Dinar */
|
|
1882
|
+
JOD: "JOD",
|
|
1883
|
+
/** Egyptian Pound */
|
|
1884
|
+
EGP: "EGP",
|
|
1885
|
+
/** US Dollar */
|
|
1886
|
+
USD: "USD",
|
|
1887
|
+
/** Euro */
|
|
1888
|
+
EUR: "EUR",
|
|
1889
|
+
/** British Pound */
|
|
1890
|
+
GBP: "GBP"
|
|
1891
|
+
};
|
|
1892
|
+
var LanguageCodes = {
|
|
1893
|
+
/** English */
|
|
1894
|
+
EN: "en",
|
|
1895
|
+
/** Arabic */
|
|
1896
|
+
AR: "ar"
|
|
1897
|
+
};
|
|
1898
|
+
var Commands = {
|
|
1899
|
+
/** Purchase (direct charge) */
|
|
1900
|
+
PURCHASE: "PURCHASE",
|
|
1901
|
+
/** Authorization (hold funds) */
|
|
1902
|
+
AUTHORIZATION: "AUTHORIZATION",
|
|
1903
|
+
/** Capture authorized funds */
|
|
1904
|
+
CAPTURE: "CAPTURE",
|
|
1905
|
+
/** Refund a transaction */
|
|
1906
|
+
REFUND: "REFUND",
|
|
1907
|
+
/** Void a transaction */
|
|
1908
|
+
VOID: "VOID",
|
|
1909
|
+
/** Query transaction status */
|
|
1910
|
+
QUERY: "QUERY_TRANSACTION",
|
|
1911
|
+
/** Tokenize a card */
|
|
1912
|
+
TOKENIZATION: "TOKENIZATION",
|
|
1913
|
+
/** Create token */
|
|
1914
|
+
CREATE_TOKEN: "CREATE_TOKEN",
|
|
1915
|
+
/** Verify token */
|
|
1916
|
+
VERIFY_TOKEN: "VERIFY_TOKEN",
|
|
1917
|
+
/** Delete token */
|
|
1918
|
+
DELETE_TOKEN: "DELETE_TOKEN",
|
|
1919
|
+
/** Get saved cards */
|
|
1920
|
+
GET_CARDS: "GET_CARDS",
|
|
1921
|
+
/** Payment link */
|
|
1922
|
+
PAYMENT_LINK: "PAYMENT_LINK"
|
|
1923
|
+
};
|
|
1924
|
+
var ECIValues = {
|
|
1925
|
+
/** Secure e-commerce with 3DS */
|
|
1926
|
+
SECURE_3DS: "05",
|
|
1927
|
+
/** Non-3DS e-commerce */
|
|
1928
|
+
NON_3DS: "07",
|
|
1929
|
+
/** Recurring payment */
|
|
1930
|
+
RECURRING: "RECURRING",
|
|
1931
|
+
/** Merchant-initiated */
|
|
1932
|
+
MERCHANT_INITIATED: "07"
|
|
1933
|
+
};
|
|
1934
|
+
var StatusCodes = {
|
|
1935
|
+
/** Success */
|
|
1936
|
+
SUCCESS: "20",
|
|
1937
|
+
/** Pending/3DS required */
|
|
1938
|
+
PENDING: "3",
|
|
1939
|
+
/** Authorized */
|
|
1940
|
+
AUTHORIZED: "02",
|
|
1941
|
+
/** Captured */
|
|
1942
|
+
CAPTURED: "04"
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// src/errors.ts
|
|
1946
|
+
var ERROR_DETAILS_MAP = {
|
|
1947
|
+
// Success codes
|
|
1948
|
+
[ResponseCodes.SUCCESS]: {
|
|
1949
|
+
message: "Transaction completed successfully.",
|
|
1950
|
+
action: "No action needed. Proceed with order fulfillment.",
|
|
1951
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html"
|
|
1952
|
+
},
|
|
1953
|
+
[ResponseCodes.PAYMENT_LINK_SUCCESS]: {
|
|
1954
|
+
message: "Payment link created successfully.",
|
|
1955
|
+
action: "Share the payment link with your customer via email, SMS, or any channel.",
|
|
1956
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html"
|
|
1957
|
+
},
|
|
1958
|
+
[ResponseCodes.RECURRING_SUCCESS]: {
|
|
1959
|
+
message: "Recurring payment processed successfully.",
|
|
1960
|
+
action: "Payment has been charged. Update subscription status in your system.",
|
|
1961
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html"
|
|
1962
|
+
},
|
|
1963
|
+
// Authentication & Security
|
|
1964
|
+
[ResponseCodes.AUTHENTICATION_FAILED]: {
|
|
1965
|
+
message: "3D Secure authentication failed. The customer could not verify their identity.",
|
|
1966
|
+
action: "Ask the customer to check their 3D Secure password or use a different card. Some banks require SMS verification - ensure customer has network coverage.",
|
|
1967
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#_3d_secure",
|
|
1968
|
+
httpStatus: 402
|
|
1969
|
+
},
|
|
1970
|
+
[ResponseCodes.NOT_ENROLLED_3DS]: {
|
|
1971
|
+
message: "Card is not enrolled in 3D Secure.",
|
|
1972
|
+
action: "The card does not support 3D Secure. You can proceed without 3DS or ask customer to use a different card.",
|
|
1973
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#_3d_secure"
|
|
1974
|
+
},
|
|
1975
|
+
[ResponseCodes.SIGNATURE_MISMATCH]: {
|
|
1976
|
+
message: "Request signature is invalid.",
|
|
1977
|
+
action: "Check that your request_secret is correct and that parameters are being signed in the correct order. Use the SDK's built-in signing methods.",
|
|
1978
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#signature_calculation",
|
|
1979
|
+
httpStatus: 401
|
|
1980
|
+
},
|
|
1981
|
+
[ResponseCodes.INVALID_ACCESS_CODE]: {
|
|
1982
|
+
message: "Invalid access code.",
|
|
1983
|
+
action: "Verify your APS_ACCESS_CODE environment variable matches the value in your APS dashboard under Integration Settings.",
|
|
1984
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#credentials",
|
|
1985
|
+
httpStatus: 401
|
|
1986
|
+
},
|
|
1987
|
+
[ResponseCodes.INVALID_MERCHANT_ID]: {
|
|
1988
|
+
message: "Invalid merchant identifier.",
|
|
1989
|
+
action: "Verify your APS_MERCHANT_ID environment variable matches your APS dashboard. Ensure you're using the correct environment (sandbox vs production).",
|
|
1990
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#credentials",
|
|
1991
|
+
httpStatus: 401
|
|
1992
|
+
},
|
|
1993
|
+
// Card Errors
|
|
1994
|
+
[ResponseCodes.CARD_EXPIRED]: {
|
|
1995
|
+
message: "The card has expired.",
|
|
1996
|
+
action: "Ask the customer to check the expiry date on their card and enter it correctly, or use a different card.",
|
|
1997
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
1998
|
+
httpStatus: 402
|
|
1999
|
+
},
|
|
2000
|
+
[ResponseCodes.INVALID_CARD_NUMBER]: {
|
|
2001
|
+
message: "The card number is invalid.",
|
|
2002
|
+
action: "Ask the customer to check their card number and try again. Ensure no spaces or dashes are included.",
|
|
2003
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2004
|
+
httpStatus: 402
|
|
2005
|
+
},
|
|
2006
|
+
[ResponseCodes.INVALID_CVV]: {
|
|
2007
|
+
message: "The CVV code is invalid.",
|
|
2008
|
+
action: "Ask the customer to check the CVV on the back of their card (3 digits for Visa/MC, 4 for Amex) and enter it correctly.",
|
|
2009
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2010
|
+
httpStatus: 402
|
|
2011
|
+
},
|
|
2012
|
+
[ResponseCodes.CARD_NOT_SUPPORTED]: {
|
|
2013
|
+
message: "This card type is not supported.",
|
|
2014
|
+
action: "The customer is using a card type not accepted by your merchant account. Ask them to use Visa, Mastercard, or MADA.",
|
|
2015
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#payment_methods",
|
|
2016
|
+
httpStatus: 402
|
|
2017
|
+
},
|
|
2018
|
+
[ResponseCodes.LOST_CARD]: {
|
|
2019
|
+
message: "The card has been reported lost.",
|
|
2020
|
+
action: "Do not retry. Ask the customer to use a different card and contact their bank.",
|
|
2021
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2022
|
+
httpStatus: 402
|
|
2023
|
+
},
|
|
2024
|
+
[ResponseCodes.STOLEN_CARD]: {
|
|
2025
|
+
message: "The card has been reported stolen.",
|
|
2026
|
+
action: "Do not retry. Ask the customer to use a different card and contact their bank immediately.",
|
|
2027
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2028
|
+
httpStatus: 402
|
|
2029
|
+
},
|
|
2030
|
+
[ResponseCodes.RESTRICTED_CARD]: {
|
|
2031
|
+
message: "The card has restrictions that prevent this transaction.",
|
|
2032
|
+
action: "The card may be restricted for online or international use. Ask the customer to contact their bank or use a different card.",
|
|
2033
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2034
|
+
httpStatus: 402
|
|
2035
|
+
},
|
|
2036
|
+
[ResponseCodes.CARD_BLOCKED]: {
|
|
2037
|
+
message: "The card has been blocked by the issuing bank.",
|
|
2038
|
+
action: "Ask the customer to contact their bank to unblock the card or use a different card.",
|
|
2039
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2040
|
+
httpStatus: 402
|
|
2041
|
+
},
|
|
2042
|
+
[ResponseCodes.INVALID_EXPIRY]: {
|
|
2043
|
+
message: "The expiry date is invalid.",
|
|
2044
|
+
action: "Ask the customer to check the expiry date format (MM/YY) and ensure the card has not expired.",
|
|
2045
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#card_errors",
|
|
2046
|
+
httpStatus: 402
|
|
2047
|
+
},
|
|
2048
|
+
// Transaction Errors
|
|
2049
|
+
[ResponseCodes.DECLINED]: {
|
|
2050
|
+
message: "The transaction was declined by the issuing bank.",
|
|
2051
|
+
action: "Ask the customer to contact their bank or use a different card. The bank does not provide specific reasons for declines.",
|
|
2052
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#declined_transactions",
|
|
2053
|
+
httpStatus: 402
|
|
2054
|
+
},
|
|
2055
|
+
[ResponseCodes.INSUFFICIENT_FUNDS]: {
|
|
2056
|
+
message: "The card has insufficient funds.",
|
|
2057
|
+
action: "Ask the customer to use a different card or payment method, or reduce the transaction amount.",
|
|
2058
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#transaction_errors",
|
|
2059
|
+
httpStatus: 402
|
|
2060
|
+
},
|
|
2061
|
+
[ResponseCodes.LIMIT_EXCEEDED]: {
|
|
2062
|
+
message: "The transaction amount exceeds the card limit.",
|
|
2063
|
+
action: "The transaction may exceed the customer's daily limit or card limit. Ask them to contact their bank or use a different card.",
|
|
2064
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#transaction_errors",
|
|
2065
|
+
httpStatus: 402
|
|
2066
|
+
},
|
|
2067
|
+
[ResponseCodes.INVALID_TRANSACTION]: {
|
|
2068
|
+
message: "The transaction is invalid.",
|
|
2069
|
+
action: "Check that all required parameters are provided correctly. Ensure the amount is valid and currency is supported.",
|
|
2070
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#transaction_errors",
|
|
2071
|
+
httpStatus: 400
|
|
2072
|
+
},
|
|
2073
|
+
[ResponseCodes.DUPLICATE_TRANSACTION]: {
|
|
2074
|
+
message: "A duplicate transaction was detected.",
|
|
2075
|
+
action: "This transaction appears to be a duplicate. Use a unique merchant_reference for each transaction.",
|
|
2076
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#idempotency",
|
|
2077
|
+
httpStatus: 409
|
|
2078
|
+
},
|
|
2079
|
+
[ResponseCodes.TRANSACTION_NOT_ALLOWED]: {
|
|
2080
|
+
message: "This transaction type is not allowed for this merchant.",
|
|
2081
|
+
action: "Your merchant account may not be configured for this transaction type. Contact APS support to enable the required service.",
|
|
2082
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#merchant_setup",
|
|
2083
|
+
httpStatus: 403
|
|
2084
|
+
},
|
|
2085
|
+
[ResponseCodes.TRANSACTION_EXPIRED]: {
|
|
2086
|
+
message: "The transaction has expired.",
|
|
2087
|
+
action: "The authorization window has expired. Create a new transaction.",
|
|
2088
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#transaction_lifecycle",
|
|
2089
|
+
httpStatus: 410
|
|
2090
|
+
},
|
|
2091
|
+
[ResponseCodes.ALREADY_CAPTURED]: {
|
|
2092
|
+
message: "The transaction has already been captured.",
|
|
2093
|
+
action: "This transaction was already captured. Check your records or query the transaction status.",
|
|
2094
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#capture",
|
|
2095
|
+
httpStatus: 409
|
|
2096
|
+
},
|
|
2097
|
+
[ResponseCodes.ALREADY_VOIDED]: {
|
|
2098
|
+
message: "The transaction has already been voided.",
|
|
2099
|
+
action: "This transaction was already voided. No further action can be taken on it.",
|
|
2100
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#void",
|
|
2101
|
+
httpStatus: 409
|
|
2102
|
+
},
|
|
2103
|
+
[ResponseCodes.ALREADY_REFUNDED]: {
|
|
2104
|
+
message: "The transaction has already been refunded.",
|
|
2105
|
+
action: "This transaction was already refunded. Check the refund status for details.",
|
|
2106
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#refund",
|
|
2107
|
+
httpStatus: 409
|
|
2108
|
+
},
|
|
2109
|
+
// Amount Errors
|
|
2110
|
+
[ResponseCodes.INVALID_AMOUNT]: {
|
|
2111
|
+
message: "The transaction amount is invalid.",
|
|
2112
|
+
action: "Ensure the amount is a positive number in the smallest currency unit (e.g., cents, fils). Amount should not contain decimals.",
|
|
2113
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#amount_format",
|
|
2114
|
+
httpStatus: 400
|
|
2115
|
+
},
|
|
2116
|
+
[ResponseCodes.AMOUNT_TOO_SMALL]: {
|
|
2117
|
+
message: "The transaction amount is below the minimum allowed.",
|
|
2118
|
+
action: "The amount is below the minimum transaction limit. Increase the amount or contact APS for limit adjustments.",
|
|
2119
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#amount_limits",
|
|
2120
|
+
httpStatus: 400
|
|
2121
|
+
},
|
|
2122
|
+
[ResponseCodes.AMOUNT_TOO_LARGE]: {
|
|
2123
|
+
message: "The transaction amount exceeds the maximum allowed.",
|
|
2124
|
+
action: "The amount exceeds your merchant transaction limit. Contact APS support to increase your limit.",
|
|
2125
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#amount_limits",
|
|
2126
|
+
httpStatus: 400
|
|
2127
|
+
},
|
|
2128
|
+
[ResponseCodes.CURRENCY_NOT_SUPPORTED]: {
|
|
2129
|
+
message: "The currency is not supported.",
|
|
2130
|
+
action: "Use a supported currency (SAR, AED, USD, EUR, etc.). Check APS documentation for the full list.",
|
|
2131
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#currencies",
|
|
2132
|
+
httpStatus: 400
|
|
2133
|
+
},
|
|
2134
|
+
// Parameter Errors
|
|
2135
|
+
[ResponseCodes.MISSING_PARAMETER]: {
|
|
2136
|
+
message: "A required parameter is missing.",
|
|
2137
|
+
action: "Check that all required parameters are provided. Common missing fields: merchant_reference, amount, currency, customer_email.",
|
|
2138
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#required_parameters",
|
|
2139
|
+
httpStatus: 400
|
|
2140
|
+
},
|
|
2141
|
+
[ResponseCodes.INVALID_PARAMETER]: {
|
|
2142
|
+
message: "A parameter has an invalid format or value.",
|
|
2143
|
+
action: "Check parameter formats: emails must be valid, amounts must be numeric, dates must be in correct format.",
|
|
2144
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#parameter_formats",
|
|
2145
|
+
httpStatus: 400
|
|
2146
|
+
},
|
|
2147
|
+
[ResponseCodes.INVALID_COMMAND]: {
|
|
2148
|
+
message: "The command is invalid or not recognized.",
|
|
2149
|
+
action: "Use a valid command: PURCHASE, AUTHORIZATION, CAPTURE, REFUND, VOID, TOKENIZATION, etc.",
|
|
2150
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#commands",
|
|
2151
|
+
httpStatus: 400
|
|
2152
|
+
},
|
|
2153
|
+
[ResponseCodes.INVALID_LANGUAGE]: {
|
|
2154
|
+
message: "The language code is invalid.",
|
|
2155
|
+
action: 'Use "en" for English or "ar" for Arabic.',
|
|
2156
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#language_codes",
|
|
2157
|
+
httpStatus: 400
|
|
2158
|
+
},
|
|
2159
|
+
[ResponseCodes.INVALID_CURRENCY]: {
|
|
2160
|
+
message: "The currency code is invalid.",
|
|
2161
|
+
action: "Use a valid 3-letter ISO currency code (SAR, AED, USD, EUR, etc.).",
|
|
2162
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#currencies",
|
|
2163
|
+
httpStatus: 400
|
|
2164
|
+
},
|
|
2165
|
+
[ResponseCodes.INVALID_RETURN_URL]: {
|
|
2166
|
+
message: "The return URL is invalid.",
|
|
2167
|
+
action: "Ensure the return_url is a valid HTTPS URL and properly URL-encoded if needed.",
|
|
2168
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#return_url",
|
|
2169
|
+
httpStatus: 400
|
|
2170
|
+
},
|
|
2171
|
+
[ResponseCodes.INVALID_EMAIL]: {
|
|
2172
|
+
message: "The customer email is invalid.",
|
|
2173
|
+
action: "Ensure the customer_email is a valid email format (e.g., customer@example.com).",
|
|
2174
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#customer_email",
|
|
2175
|
+
httpStatus: 400
|
|
2176
|
+
},
|
|
2177
|
+
[ResponseCodes.PARAMETER_VALUE_NOT_ALLOWED]: {
|
|
2178
|
+
message: "A parameter value is not allowed for your merchant account.",
|
|
2179
|
+
action: "This error often occurs when trying to use tokenization (remember_me) but your merchant account does not have tokenization enabled. Contact APS support to enable tokenization, or set tokenize: false. Other causes: using a payment method not enabled for your account, or an invalid parameter value.",
|
|
2180
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#merchant_setup",
|
|
2181
|
+
httpStatus: 400
|
|
2182
|
+
},
|
|
2183
|
+
// Tokenization Errors
|
|
2184
|
+
[ResponseCodes.TOKEN_NOT_FOUND]: {
|
|
2185
|
+
message: "The card token was not found.",
|
|
2186
|
+
action: "The token may have been deleted or never created. Ask the customer to re-enter their card details.",
|
|
2187
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#tokenization",
|
|
2188
|
+
httpStatus: 404
|
|
2189
|
+
},
|
|
2190
|
+
[ResponseCodes.TOKEN_EXPIRED]: {
|
|
2191
|
+
message: "The card token has expired.",
|
|
2192
|
+
action: "Tokens expire after a period of inactivity. Ask the customer to re-enter their card details.",
|
|
2193
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#token_expiry",
|
|
2194
|
+
httpStatus: 410
|
|
2195
|
+
},
|
|
2196
|
+
[ResponseCodes.TOKEN_ALREADY_USED]: {
|
|
2197
|
+
message: "The token has already been used.",
|
|
2198
|
+
action: "Each token can only be used once for security. Create a new token for this transaction.",
|
|
2199
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#token_usage",
|
|
2200
|
+
httpStatus: 409
|
|
2201
|
+
},
|
|
2202
|
+
[ResponseCodes.INVALID_TOKEN]: {
|
|
2203
|
+
message: "The token is invalid.",
|
|
2204
|
+
action: "Check that the token was properly generated and is being passed correctly.",
|
|
2205
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#tokenization",
|
|
2206
|
+
httpStatus: 400
|
|
2207
|
+
},
|
|
2208
|
+
// System Errors
|
|
2209
|
+
[ResponseCodes.SYSTEM_ERROR]: {
|
|
2210
|
+
message: "A system error occurred.",
|
|
2211
|
+
action: "This is a temporary issue on APS side. Wait a moment and retry the transaction.",
|
|
2212
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#system_errors",
|
|
2213
|
+
httpStatus: 500
|
|
2214
|
+
},
|
|
2215
|
+
[ResponseCodes.SERVICE_UNAVAILABLE]: {
|
|
2216
|
+
message: "The service is temporarily unavailable.",
|
|
2217
|
+
action: "APS is experiencing high load or maintenance. Wait a moment and retry.",
|
|
2218
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#system_errors",
|
|
2219
|
+
httpStatus: 503
|
|
2220
|
+
},
|
|
2221
|
+
[ResponseCodes.TIMEOUT]: {
|
|
2222
|
+
message: "The request timed out.",
|
|
2223
|
+
action: "The request took too long to complete. Check your network connection and retry.",
|
|
2224
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#timeouts",
|
|
2225
|
+
httpStatus: 504
|
|
2226
|
+
},
|
|
2227
|
+
[ResponseCodes.DATABASE_ERROR]: {
|
|
2228
|
+
message: "A database error occurred.",
|
|
2229
|
+
action: "Temporary database issue. Wait a moment and retry the transaction.",
|
|
2230
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#system_errors",
|
|
2231
|
+
httpStatus: 500
|
|
2232
|
+
},
|
|
2233
|
+
[ResponseCodes.INTERNAL_ERROR]: {
|
|
2234
|
+
message: "An internal error occurred.",
|
|
2235
|
+
action: "Unexpected error on APS side. Contact APS support if this persists.",
|
|
2236
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#support",
|
|
2237
|
+
httpStatus: 500
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
function getErrorDetails(code) {
|
|
2241
|
+
const details = ERROR_DETAILS_MAP[code];
|
|
2242
|
+
const category = categorizeError(code);
|
|
2243
|
+
const retryable = isRetryableError(code);
|
|
2244
|
+
if (details) {
|
|
2245
|
+
return {
|
|
2246
|
+
code,
|
|
2247
|
+
message: details.message,
|
|
2248
|
+
action: details.action,
|
|
2249
|
+
category,
|
|
2250
|
+
retryable,
|
|
2251
|
+
documentation: details.documentation,
|
|
2252
|
+
httpStatus: details.httpStatus
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
return {
|
|
2256
|
+
code,
|
|
2257
|
+
message: `An unknown error occurred (code: ${code}).`,
|
|
2258
|
+
action: "Contact APS support with this error code for assistance.",
|
|
2259
|
+
category: ErrorCategories.SYSTEM_ERROR,
|
|
2260
|
+
retryable: false,
|
|
2261
|
+
documentation: "https://paymentservices.amazon.com/docs/EN/index.html#support",
|
|
2262
|
+
httpStatus: 500
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
function isRetryableError2(code) {
|
|
2266
|
+
return isRetryableError(code);
|
|
2267
|
+
}
|
|
2268
|
+
function getUserFriendlyMessage(code, includeAction = true) {
|
|
2269
|
+
const details = getErrorDetails(code);
|
|
2270
|
+
if (includeAction) {
|
|
2271
|
+
return `${details.message} ${details.action}`;
|
|
2272
|
+
}
|
|
2273
|
+
return details.message;
|
|
2274
|
+
}
|
|
2275
|
+
function formatErrorForLogging(code, context) {
|
|
2276
|
+
const details = getErrorDetails(code);
|
|
2277
|
+
const contextStr = context ? ` (${Object.entries(context).map(([k, v]) => `${k}: ${v}`).join(", ")})` : "";
|
|
2278
|
+
return `[APS Error ${code}] ${details.category.toUpperCase()}: ${details.message}${contextStr}`;
|
|
2279
|
+
}
|
|
2280
|
+
function isSuccessCode(code) {
|
|
2281
|
+
return [
|
|
2282
|
+
ResponseCodes.SUCCESS,
|
|
2283
|
+
ResponseCodes.PAYMENT_LINK_SUCCESS,
|
|
2284
|
+
ResponseCodes.RECURRING_SUCCESS
|
|
2285
|
+
].includes(code);
|
|
2286
|
+
}
|
|
2287
|
+
function getErrorCodesByCategory(category) {
|
|
2288
|
+
return Object.entries(ERROR_DETAILS_MAP).filter(([code]) => categorizeError(code) === category).map(([code]) => code);
|
|
2289
|
+
}
|
|
2290
|
+
function suggestHttpStatus(code) {
|
|
2291
|
+
const details = getErrorDetails(code);
|
|
2292
|
+
return details.httpStatus || 400;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// src/validators.ts
|
|
2296
|
+
var import_crypto5 = __toESM(require("crypto"));
|
|
2297
|
+
function isValidMerchantReference(reference) {
|
|
2298
|
+
if (!reference || reference.length === 0) {
|
|
2299
|
+
return {
|
|
2300
|
+
valid: false,
|
|
2301
|
+
error: "Merchant reference is required",
|
|
2302
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
if (reference.length > 40) {
|
|
2306
|
+
return {
|
|
2307
|
+
valid: false,
|
|
2308
|
+
error: "Merchant reference must be 40 characters or less",
|
|
2309
|
+
code: ResponseCodes.INVALID_PARAMETER
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(reference)) {
|
|
2313
|
+
return {
|
|
2314
|
+
valid: false,
|
|
2315
|
+
error: "Merchant reference can only contain letters, numbers, underscores, and hyphens",
|
|
2316
|
+
code: ResponseCodes.INVALID_PARAMETER
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
return { valid: true };
|
|
2320
|
+
}
|
|
2321
|
+
function isValidAmount(amount, _currency) {
|
|
2322
|
+
if (amount === void 0 || amount === null) {
|
|
2323
|
+
return {
|
|
2324
|
+
valid: false,
|
|
2325
|
+
error: "Amount is required",
|
|
2326
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
if (typeof amount !== "number" || isNaN(amount)) {
|
|
2330
|
+
return {
|
|
2331
|
+
valid: false,
|
|
2332
|
+
error: "Amount must be a valid number",
|
|
2333
|
+
code: ResponseCodes.INVALID_AMOUNT
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
if (amount <= 0) {
|
|
2337
|
+
return {
|
|
2338
|
+
valid: false,
|
|
2339
|
+
error: "Amount must be greater than zero",
|
|
2340
|
+
code: ResponseCodes.AMOUNT_TOO_SMALL
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
if (!Number.isInteger(amount)) {
|
|
2344
|
+
return {
|
|
2345
|
+
valid: false,
|
|
2346
|
+
error: "Amount must be a whole number (in smallest currency unit, e.g., cents)",
|
|
2347
|
+
code: ResponseCodes.INVALID_AMOUNT
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
if (amount > 999999999999) {
|
|
2351
|
+
return {
|
|
2352
|
+
valid: false,
|
|
2353
|
+
error: "Amount exceeds maximum allowed value",
|
|
2354
|
+
code: ResponseCodes.AMOUNT_TOO_LARGE
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
return { valid: true };
|
|
2358
|
+
}
|
|
2359
|
+
function isValidCurrency(currency) {
|
|
2360
|
+
if (!currency || currency.length === 0) {
|
|
2361
|
+
return {
|
|
2362
|
+
valid: false,
|
|
2363
|
+
error: "Currency is required",
|
|
2364
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
if (!/^[A-Z]{3}$/.test(currency)) {
|
|
2368
|
+
return {
|
|
2369
|
+
valid: false,
|
|
2370
|
+
error: "Currency must be a valid 3-letter ISO code (e.g., SAR, AED, USD)",
|
|
2371
|
+
code: ResponseCodes.INVALID_CURRENCY
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
const supportedCurrencies = [
|
|
2375
|
+
"SAR",
|
|
2376
|
+
"AED",
|
|
2377
|
+
"KWD",
|
|
2378
|
+
"QAR",
|
|
2379
|
+
"BHD",
|
|
2380
|
+
"OMR",
|
|
2381
|
+
"JOD",
|
|
2382
|
+
"EGP",
|
|
2383
|
+
"USD",
|
|
2384
|
+
"EUR",
|
|
2385
|
+
"GBP",
|
|
2386
|
+
"CAD",
|
|
2387
|
+
"AUD",
|
|
2388
|
+
"CHF",
|
|
2389
|
+
"SEK",
|
|
2390
|
+
"NOK",
|
|
2391
|
+
"DKK",
|
|
2392
|
+
"NZD",
|
|
2393
|
+
"SGD",
|
|
2394
|
+
"HKD",
|
|
2395
|
+
"JPY",
|
|
2396
|
+
"CNY",
|
|
2397
|
+
"INR"
|
|
2398
|
+
];
|
|
2399
|
+
if (!supportedCurrencies.includes(currency)) {
|
|
2400
|
+
return {
|
|
2401
|
+
valid: false,
|
|
2402
|
+
error: `Currency ${currency} may not be supported by APS. Supported: ${supportedCurrencies.join(", ")}`,
|
|
2403
|
+
code: ResponseCodes.CURRENCY_NOT_SUPPORTED
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
return { valid: true };
|
|
2407
|
+
}
|
|
2408
|
+
function isValidEmail(email) {
|
|
2409
|
+
if (!email || email.length === 0) {
|
|
2410
|
+
return {
|
|
2411
|
+
valid: false,
|
|
2412
|
+
error: "Email is required",
|
|
2413
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2417
|
+
if (!emailRegex.test(email)) {
|
|
2418
|
+
return {
|
|
2419
|
+
valid: false,
|
|
2420
|
+
error: "Please enter a valid email address",
|
|
2421
|
+
code: ResponseCodes.INVALID_EMAIL
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
if (email.length > 254) {
|
|
2425
|
+
return {
|
|
2426
|
+
valid: false,
|
|
2427
|
+
error: "Email address is too long",
|
|
2428
|
+
code: ResponseCodes.INVALID_EMAIL
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
return { valid: true };
|
|
2432
|
+
}
|
|
2433
|
+
function isValidCardNumber(cardNumber) {
|
|
2434
|
+
if (!cardNumber || cardNumber.length === 0) {
|
|
2435
|
+
return {
|
|
2436
|
+
valid: false,
|
|
2437
|
+
error: "Card number is required",
|
|
2438
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
const cleaned = cardNumber.replace(/[\s-]/g, "");
|
|
2442
|
+
if (!/^\d+$/.test(cleaned)) {
|
|
2443
|
+
return {
|
|
2444
|
+
valid: false,
|
|
2445
|
+
error: "Card number can only contain digits",
|
|
2446
|
+
code: ResponseCodes.INVALID_CARD_NUMBER
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
if (cleaned.length < 13 || cleaned.length > 19) {
|
|
2450
|
+
return {
|
|
2451
|
+
valid: false,
|
|
2452
|
+
error: "Card number must be between 13 and 19 digits",
|
|
2453
|
+
code: ResponseCodes.INVALID_CARD_NUMBER
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
let sum = 0;
|
|
2457
|
+
let shouldDouble = false;
|
|
2458
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
2459
|
+
let digit = parseInt(cleaned.charAt(i), 10);
|
|
2460
|
+
if (shouldDouble) {
|
|
2461
|
+
digit *= 2;
|
|
2462
|
+
if (digit > 9) {
|
|
2463
|
+
digit -= 9;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
sum += digit;
|
|
2467
|
+
shouldDouble = !shouldDouble;
|
|
2468
|
+
}
|
|
2469
|
+
if (sum % 10 !== 0) {
|
|
2470
|
+
return {
|
|
2471
|
+
valid: false,
|
|
2472
|
+
error: "Card number is invalid (failed Luhn check)",
|
|
2473
|
+
code: ResponseCodes.INVALID_CARD_NUMBER
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
return { valid: true };
|
|
2477
|
+
}
|
|
2478
|
+
function isValidExpiryDate(month, year) {
|
|
2479
|
+
if (!month || !year) {
|
|
2480
|
+
return {
|
|
2481
|
+
valid: false,
|
|
2482
|
+
error: "Expiry month and year are required",
|
|
2483
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
const expiryMonth = parseInt(month, 10);
|
|
2487
|
+
let expiryYear = parseInt(year, 10);
|
|
2488
|
+
if (year.length === 2) {
|
|
2489
|
+
expiryYear = 2e3 + expiryYear;
|
|
2490
|
+
}
|
|
2491
|
+
if (isNaN(expiryMonth) || isNaN(expiryYear)) {
|
|
2492
|
+
return {
|
|
2493
|
+
valid: false,
|
|
2494
|
+
error: "Expiry date must be numeric",
|
|
2495
|
+
code: ResponseCodes.INVALID_EXPIRY
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
if (expiryMonth < 1 || expiryMonth > 12) {
|
|
2499
|
+
return {
|
|
2500
|
+
valid: false,
|
|
2501
|
+
error: "Expiry month must be between 1 and 12",
|
|
2502
|
+
code: ResponseCodes.INVALID_EXPIRY
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
const now = /* @__PURE__ */ new Date();
|
|
2506
|
+
const currentYear = now.getFullYear();
|
|
2507
|
+
const currentMonth = now.getMonth() + 1;
|
|
2508
|
+
if (expiryYear < currentYear) {
|
|
2509
|
+
return {
|
|
2510
|
+
valid: false,
|
|
2511
|
+
error: "Card has expired",
|
|
2512
|
+
code: ResponseCodes.CARD_EXPIRED
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
if (expiryYear === currentYear && expiryMonth < currentMonth) {
|
|
2516
|
+
return {
|
|
2517
|
+
valid: false,
|
|
2518
|
+
error: "Card has expired",
|
|
2519
|
+
code: ResponseCodes.CARD_EXPIRED
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
if (expiryYear > currentYear + 20) {
|
|
2523
|
+
return {
|
|
2524
|
+
valid: false,
|
|
2525
|
+
error: "Expiry date is too far in the future",
|
|
2526
|
+
code: ResponseCodes.INVALID_EXPIRY
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
return { valid: true };
|
|
2530
|
+
}
|
|
2531
|
+
function isValidCVV(cvv, cardType) {
|
|
2532
|
+
if (!cvv || cvv.length === 0) {
|
|
2533
|
+
return {
|
|
2534
|
+
valid: false,
|
|
2535
|
+
error: "CVV is required",
|
|
2536
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
if (!/^\d+$/.test(cvv)) {
|
|
2540
|
+
return {
|
|
2541
|
+
valid: false,
|
|
2542
|
+
error: "CVV must contain only digits",
|
|
2543
|
+
code: ResponseCodes.INVALID_CVV
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
const expectedLength = cardType?.toLowerCase() === "amex" ? 4 : 3;
|
|
2547
|
+
if (cvv.length !== expectedLength) {
|
|
2548
|
+
return {
|
|
2549
|
+
valid: false,
|
|
2550
|
+
error: `CVV must be ${expectedLength} digits${cardType?.toLowerCase() === "amex" ? " for American Express" : ""}`,
|
|
2551
|
+
code: ResponseCodes.INVALID_CVV
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
return { valid: true };
|
|
2555
|
+
}
|
|
2556
|
+
function isValidReturnUrl(url) {
|
|
2557
|
+
if (!url || url.length === 0) {
|
|
2558
|
+
return {
|
|
2559
|
+
valid: false,
|
|
2560
|
+
error: "Return URL is required",
|
|
2561
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
try {
|
|
2565
|
+
const parsed = new URL(url);
|
|
2566
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
2567
|
+
return {
|
|
2568
|
+
valid: false,
|
|
2569
|
+
error: "Return URL must use HTTP or HTTPS protocol",
|
|
2570
|
+
code: ResponseCodes.INVALID_RETURN_URL
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
if (url.length > 2048) {
|
|
2574
|
+
return {
|
|
2575
|
+
valid: false,
|
|
2576
|
+
error: "Return URL is too long (max 2048 characters)",
|
|
2577
|
+
code: ResponseCodes.INVALID_RETURN_URL
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
return { valid: true };
|
|
2581
|
+
} catch {
|
|
2582
|
+
return {
|
|
2583
|
+
valid: false,
|
|
2584
|
+
error: "Return URL must be a valid URL",
|
|
2585
|
+
code: ResponseCodes.INVALID_RETURN_URL
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
function isValidSignature(params, signature, secret) {
|
|
2590
|
+
if (!signature || !secret) {
|
|
2591
|
+
return false;
|
|
2592
|
+
}
|
|
2593
|
+
try {
|
|
2594
|
+
const sortedKeys = Object.keys(params).sort();
|
|
2595
|
+
const concatenated = sortedKeys.filter((key) => key !== "signature").map((key) => `${key}=${params[key]}`).join("");
|
|
2596
|
+
const signatureString = `${secret}${concatenated}${secret}`;
|
|
2597
|
+
const expectedSignature = import_crypto5.default.createHash("sha256").update(signatureString).digest("hex").toUpperCase();
|
|
2598
|
+
return signature.toUpperCase() === expectedSignature;
|
|
2599
|
+
} catch {
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function isValidWebhookPayload(payload, signature, secret) {
|
|
2604
|
+
if (!signature) {
|
|
2605
|
+
return {
|
|
2606
|
+
valid: false,
|
|
2607
|
+
error: "Missing signature header",
|
|
2608
|
+
code: ResponseCodes.SIGNATURE_MISMATCH
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
if (!secret) {
|
|
2612
|
+
return {
|
|
2613
|
+
valid: false,
|
|
2614
|
+
error: "Secret is required for validation",
|
|
2615
|
+
code: ResponseCodes.SIGNATURE_MISMATCH
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
const payloadStr = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
2619
|
+
const expectedSignature = import_crypto5.default.createHmac("sha256", secret).update(payloadStr).digest("base64");
|
|
2620
|
+
if (signature !== expectedSignature) {
|
|
2621
|
+
return {
|
|
2622
|
+
valid: false,
|
|
2623
|
+
error: "Invalid signature",
|
|
2624
|
+
code: ResponseCodes.SIGNATURE_MISMATCH
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
return { valid: true };
|
|
2628
|
+
}
|
|
2629
|
+
function isValidLanguage(language) {
|
|
2630
|
+
if (!language || language.length === 0) {
|
|
2631
|
+
return {
|
|
2632
|
+
valid: false,
|
|
2633
|
+
error: "Language is required",
|
|
2634
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
const supportedLanguages = ["en", "ar"];
|
|
2638
|
+
if (!supportedLanguages.includes(language.toLowerCase())) {
|
|
2639
|
+
return {
|
|
2640
|
+
valid: false,
|
|
2641
|
+
error: `Language must be one of: ${supportedLanguages.join(", ")}`,
|
|
2642
|
+
code: ResponseCodes.INVALID_LANGUAGE
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
return { valid: true };
|
|
2646
|
+
}
|
|
2647
|
+
function isValidTokenName(tokenName) {
|
|
2648
|
+
if (!tokenName || tokenName.length === 0) {
|
|
2649
|
+
return {
|
|
2650
|
+
valid: false,
|
|
2651
|
+
error: "Token name is required",
|
|
2652
|
+
code: ResponseCodes.MISSING_PARAMETER
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
if (tokenName.length < 10 || tokenName.length > 100) {
|
|
2656
|
+
return {
|
|
2657
|
+
valid: false,
|
|
2658
|
+
error: "Token name appears to be invalid (wrong length)",
|
|
2659
|
+
code: ResponseCodes.INVALID_TOKEN
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
return { valid: true };
|
|
2663
|
+
}
|
|
2664
|
+
function isValidPhone(phone) {
|
|
2665
|
+
if (!phone || phone.length === 0) {
|
|
2666
|
+
return { valid: true };
|
|
2667
|
+
}
|
|
2668
|
+
const phoneRegex = /^\+[1-9]\d{7,14}$/;
|
|
2669
|
+
if (!phoneRegex.test(phone.replace(/[\s-]/g, ""))) {
|
|
2670
|
+
return {
|
|
2671
|
+
valid: false,
|
|
2672
|
+
error: "Phone number must be in international format (+1234567890)",
|
|
2673
|
+
code: ResponseCodes.INVALID_PARAMETER
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
return { valid: true };
|
|
2677
|
+
}
|
|
2678
|
+
function isValidDescription(description) {
|
|
2679
|
+
if (!description || description.length === 0) {
|
|
2680
|
+
return { valid: true };
|
|
2681
|
+
}
|
|
2682
|
+
if (description.length > 255) {
|
|
2683
|
+
return {
|
|
2684
|
+
valid: false,
|
|
2685
|
+
error: "Description must be 255 characters or less",
|
|
2686
|
+
code: ResponseCodes.INVALID_PARAMETER
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
return { valid: true };
|
|
2690
|
+
}
|
|
2691
|
+
function isValidCustomerName(name) {
|
|
2692
|
+
if (!name || name.length === 0) {
|
|
2693
|
+
return { valid: true };
|
|
2694
|
+
}
|
|
2695
|
+
if (name.length > 100) {
|
|
2696
|
+
return {
|
|
2697
|
+
valid: false,
|
|
2698
|
+
error: "Customer name must be 100 characters or less",
|
|
2699
|
+
code: ResponseCodes.INVALID_PARAMETER
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
return { valid: true };
|
|
2703
|
+
}
|
|
2704
|
+
function validatePaymentParams(params) {
|
|
2705
|
+
const errors = [];
|
|
2706
|
+
if (params.merchant_reference !== void 0) {
|
|
2707
|
+
const result = isValidMerchantReference(params.merchant_reference);
|
|
2708
|
+
if (!result.valid) {
|
|
2709
|
+
errors.push({ field: "merchant_reference", error: result.error, code: result.code });
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (params.amount !== void 0) {
|
|
2713
|
+
const result = isValidAmount(params.amount, params.currency);
|
|
2714
|
+
if (!result.valid) {
|
|
2715
|
+
errors.push({ field: "amount", error: result.error, code: result.code });
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (params.currency !== void 0) {
|
|
2719
|
+
const result = isValidCurrency(params.currency);
|
|
2720
|
+
if (!result.valid) {
|
|
2721
|
+
errors.push({ field: "currency", error: result.error, code: result.code });
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
if (params.customer_email !== void 0) {
|
|
2725
|
+
const result = isValidEmail(params.customer_email);
|
|
2726
|
+
if (!result.valid) {
|
|
2727
|
+
errors.push({ field: "customer_email", error: result.error, code: result.code });
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
if (params.return_url !== void 0) {
|
|
2731
|
+
const result = isValidReturnUrl(params.return_url);
|
|
2732
|
+
if (!result.valid) {
|
|
2733
|
+
errors.push({ field: "return_url", error: result.error, code: result.code });
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (params.description !== void 0) {
|
|
2737
|
+
const result = isValidDescription(params.description);
|
|
2738
|
+
if (!result.valid) {
|
|
2739
|
+
errors.push({ field: "description", error: result.error, code: result.code });
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
if (params.language !== void 0) {
|
|
2743
|
+
const result = isValidLanguage(params.language);
|
|
2744
|
+
if (!result.valid) {
|
|
2745
|
+
errors.push({ field: "language", error: result.error, code: result.code });
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
valid: errors.length === 0,
|
|
2750
|
+
errors
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
var validators = {
|
|
2754
|
+
isValidMerchantReference,
|
|
2755
|
+
isValidAmount,
|
|
2756
|
+
isValidCurrency,
|
|
2757
|
+
isValidEmail,
|
|
2758
|
+
isValidCardNumber,
|
|
2759
|
+
isValidExpiryDate,
|
|
2760
|
+
isValidCVV,
|
|
2761
|
+
isValidReturnUrl,
|
|
2762
|
+
isValidSignature,
|
|
2763
|
+
isValidWebhookPayload,
|
|
2764
|
+
isValidLanguage,
|
|
2765
|
+
isValidTokenName,
|
|
2766
|
+
isValidPhone,
|
|
2767
|
+
isValidDescription,
|
|
2768
|
+
isValidCustomerName,
|
|
2769
|
+
validatePaymentParams
|
|
2770
|
+
};
|
|
2771
|
+
|
|
2772
|
+
// src/index.ts
|
|
2773
|
+
var index_default = APSClient;
|
|
2774
|
+
var APS = APSClient;
|
|
2775
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2776
|
+
0 && (module.exports = {
|
|
2777
|
+
APS,
|
|
2778
|
+
APSClient,
|
|
2779
|
+
APSException,
|
|
2780
|
+
Commands,
|
|
2781
|
+
CurrencyCodes,
|
|
2782
|
+
CustomPaymentPageModule,
|
|
2783
|
+
ECIValues,
|
|
2784
|
+
ErrorCategories,
|
|
2785
|
+
HostedCheckoutModule,
|
|
2786
|
+
LanguageCodes,
|
|
2787
|
+
PaymentLinksModule,
|
|
2788
|
+
PaymentMethodCodes,
|
|
2789
|
+
PaymentsModule,
|
|
2790
|
+
RecurringModule,
|
|
2791
|
+
ResponseCodes,
|
|
2792
|
+
StatusCodes,
|
|
2793
|
+
TestCards,
|
|
2794
|
+
TokenizationModule,
|
|
2795
|
+
WebhooksModule,
|
|
2796
|
+
categorizeError,
|
|
2797
|
+
checkRetryableError,
|
|
2798
|
+
formatErrorForLogging,
|
|
2799
|
+
getErrorCodesByCategory,
|
|
2800
|
+
getErrorDetails,
|
|
2801
|
+
getUserFriendlyMessage,
|
|
2802
|
+
isRetryableError,
|
|
2803
|
+
isSuccessCode,
|
|
2804
|
+
isValidAmount,
|
|
2805
|
+
isValidCVV,
|
|
2806
|
+
isValidCardNumber,
|
|
2807
|
+
isValidCurrency,
|
|
2808
|
+
isValidCustomerName,
|
|
2809
|
+
isValidDescription,
|
|
2810
|
+
isValidEmail,
|
|
2811
|
+
isValidExpiryDate,
|
|
2812
|
+
isValidLanguage,
|
|
2813
|
+
isValidMerchantReference,
|
|
2814
|
+
isValidPhone,
|
|
2815
|
+
isValidReturnUrl,
|
|
2816
|
+
isValidSignature,
|
|
2817
|
+
isValidTokenName,
|
|
2818
|
+
isValidWebhookPayload,
|
|
2819
|
+
suggestHttpStatus,
|
|
2820
|
+
validatePaymentParams,
|
|
2821
|
+
validators
|
|
2822
|
+
});
|