@tapsilat/tapsilat-js 2026.7.7 → 2026.10.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +217 -46
- package/dist/TapsilatSDK.d.ts +158 -40
- package/dist/TapsilatSDK.d.ts.map +1 -1
- package/dist/__mocks__/HttpClient.d.ts +1 -0
- package/dist/__mocks__/HttpClient.d.ts.map +1 -1
- package/dist/errors/TapsilatError.d.ts +12 -1
- package/dist/errors/TapsilatError.d.ts.map +1 -1
- package/dist/http/HttpClient.d.ts +19 -1
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/interceptors.d.ts.map +1 -1
- package/dist/index.cjs +1442 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2029 -133
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1429 -129
- package/dist/index.js.map +1 -1
- package/dist/services/MarketplaceService.d.ts +59 -0
- package/dist/services/MarketplaceService.d.ts.map +1 -0
- package/dist/services/ServiceBase.d.ts +21 -0
- package/dist/services/ServiceBase.d.ts.map +1 -0
- package/dist/services/SubmerchantService.d.ts +31 -0
- package/dist/services/SubmerchantService.d.ts.map +1 -0
- package/dist/services/TokenizationService.d.ts +14 -0
- package/dist/services/TokenizationService.d.ts.map +1 -0
- package/dist/services/VposService.d.ts +26 -0
- package/dist/services/VposService.d.ts.map +1 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/types/elements.d.ts +144 -0
- package/dist/types/elements.d.ts.map +1 -0
- package/dist/types/index.d.ts +90 -44
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/panel.d.ts +839 -0
- package/dist/types/panel.d.ts.map +1 -0
- package/dist/utils/currency.d.ts +25 -0
- package/dist/utils/currency.d.ts.map +1 -0
- package/dist/utils/redact.d.ts +16 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/response.d.ts +12 -0
- package/dist/utils/response.d.ts.map +1 -1
- package/dist/utils/validators.d.ts +12 -0
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/verify.d.ts +31 -8
- package/dist/utils/verify.d.ts.map +1 -1
- package/dist/webhooks/OrderStatus.d.ts +98 -0
- package/dist/webhooks/OrderStatus.d.ts.map +1 -0
- package/dist/webhooks/WebhookEvent.d.ts +58 -0
- package/dist/webhooks/WebhookEvent.d.ts.map +1 -0
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -12,6 +12,18 @@ var crypto = require('crypto');
|
|
|
12
12
|
class TapsilatError extends Error {
|
|
13
13
|
code;
|
|
14
14
|
details;
|
|
15
|
+
/** HTTP status code, when the error came from an HTTP response */
|
|
16
|
+
statusCode;
|
|
17
|
+
/** `code` field from the API response body, as a string */
|
|
18
|
+
apiCode;
|
|
19
|
+
/** `status` field from the API response body */
|
|
20
|
+
apiStatus;
|
|
21
|
+
/** Per-field validation messages from the API response body */
|
|
22
|
+
errors;
|
|
23
|
+
/** Whether the API error says the request can be retried, as Elements errors do */
|
|
24
|
+
retryable;
|
|
25
|
+
/** Raw API response body */
|
|
26
|
+
rawBody;
|
|
15
27
|
/**
|
|
16
28
|
* @summary Creates a new TapsilatError instance
|
|
17
29
|
* @description Initializes base error with message, code and optional details
|
|
@@ -40,7 +52,14 @@ class TapsilatError extends Error {
|
|
|
40
52
|
* @returns New TapsilatError instance with properties from API error
|
|
41
53
|
*/
|
|
42
54
|
static fromAPIError(apiError) {
|
|
43
|
-
|
|
55
|
+
const error = new TapsilatError(apiError.message, apiError.code, apiError.details);
|
|
56
|
+
error.statusCode = apiError.statusCode;
|
|
57
|
+
error.apiCode = apiError.apiCode;
|
|
58
|
+
error.apiStatus = apiError.apiStatus;
|
|
59
|
+
error.errors = apiError.errors;
|
|
60
|
+
error.retryable = apiError.retryable;
|
|
61
|
+
error.rawBody = apiError.rawBody;
|
|
62
|
+
return error;
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
/**
|
|
@@ -51,7 +70,6 @@ class TapsilatError extends Error {
|
|
|
51
70
|
* @extends TapsilatError
|
|
52
71
|
*/
|
|
53
72
|
class TapsilatNetworkError extends TapsilatError {
|
|
54
|
-
statusCode;
|
|
55
73
|
/**
|
|
56
74
|
* @summary Creates a new network error instance
|
|
57
75
|
* @description Initializes network error with message, code, status code and optional details
|
|
@@ -361,7 +379,7 @@ class HttpClient {
|
|
|
361
379
|
let lastError = null;
|
|
362
380
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
363
381
|
try {
|
|
364
|
-
const response = await this.makeRequest(interceptedUrl, interceptedOptions, config?.timeout, config?.responseType);
|
|
382
|
+
const response = await this.makeRequest(interceptedUrl, interceptedOptions, config?.timeout ?? this.config.timeout, config?.responseType);
|
|
365
383
|
// Execute response interceptors
|
|
366
384
|
const interceptedResponse = await this.interceptors.executeResponseInterceptors(response, {
|
|
367
385
|
url: interceptedUrl,
|
|
@@ -437,44 +455,94 @@ class HttpClient {
|
|
|
437
455
|
* @returns Processed API response
|
|
438
456
|
*/
|
|
439
457
|
async processResponse(response, responseType) {
|
|
440
|
-
const contentType = response.headers.get("content-type");
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
458
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
459
|
+
// Binary downloads, such as order exports, are returned as they are
|
|
460
|
+
if (response.ok && (responseType === "blob" || responseType === "arraybuffer")) {
|
|
461
|
+
try {
|
|
462
|
+
const data = responseType === "blob" ? await response.blob() : await response.arrayBuffer();
|
|
463
|
+
return { success: true, status: response.status, data: data };
|
|
446
464
|
}
|
|
447
|
-
|
|
448
|
-
|
|
465
|
+
catch {
|
|
466
|
+
throw new TapsilatError("Failed to read response body", "PARSE_ERROR");
|
|
449
467
|
}
|
|
450
|
-
|
|
451
|
-
|
|
468
|
+
}
|
|
469
|
+
let rawBody;
|
|
470
|
+
try {
|
|
471
|
+
rawBody = await response.text();
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
throw new TapsilatError("Failed to read response body", "PARSE_ERROR");
|
|
475
|
+
}
|
|
476
|
+
const trimmed = rawBody.trim();
|
|
477
|
+
let responseData = undefined;
|
|
478
|
+
if (trimmed !== "") {
|
|
479
|
+
const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
480
|
+
if (responseType === "text" && response.ok) {
|
|
481
|
+
responseData = rawBody;
|
|
452
482
|
}
|
|
453
|
-
else if (
|
|
454
|
-
|
|
483
|
+
else if (contentType.includes("json") || looksLikeJson) {
|
|
484
|
+
try {
|
|
485
|
+
responseData = JSON.parse(trimmed);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
if (response.ok) {
|
|
489
|
+
throw new TapsilatError("Failed to parse response body", "PARSE_ERROR");
|
|
490
|
+
}
|
|
491
|
+
responseData = rawBody;
|
|
492
|
+
}
|
|
455
493
|
}
|
|
456
494
|
else {
|
|
457
|
-
responseData =
|
|
495
|
+
responseData = rawBody;
|
|
458
496
|
}
|
|
459
497
|
}
|
|
460
|
-
catch {
|
|
461
|
-
throw new TapsilatError("Failed to parse response body", "PARSE_ERROR");
|
|
462
|
-
}
|
|
463
498
|
// Handle successful responses
|
|
464
499
|
if (response.ok) {
|
|
500
|
+
// A deployment serving the panel UI answers unknown routes with its HTML page and status 200
|
|
501
|
+
if (contentType.includes("text/html") || trimmed.startsWith("<")) {
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
status: response.status,
|
|
505
|
+
error: {
|
|
506
|
+
message: `Unexpected non-JSON response (Content-Type "${contentType}"); check that the endpoint URL and route are correct`,
|
|
507
|
+
code: "UNEXPECTED_RESPONSE",
|
|
508
|
+
statusCode: response.status,
|
|
509
|
+
rawBody,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
465
513
|
return {
|
|
466
514
|
success: true,
|
|
515
|
+
status: response.status,
|
|
467
516
|
data: responseData,
|
|
468
517
|
};
|
|
469
518
|
}
|
|
470
519
|
// Handle error responses
|
|
471
520
|
const error = this.createErrorFromResponse(response, responseData);
|
|
521
|
+
const body = responseData && typeof responseData === "object"
|
|
522
|
+
? responseData
|
|
523
|
+
: {};
|
|
524
|
+
const errors = body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)
|
|
525
|
+
? body.errors
|
|
526
|
+
: undefined;
|
|
527
|
+
const nested = this.nestedError(body);
|
|
528
|
+
const apiCode = body.code !== undefined && body.code !== null ? body.code : nested?.code;
|
|
472
529
|
return {
|
|
473
530
|
success: false,
|
|
531
|
+
status: response.status,
|
|
474
532
|
error: {
|
|
475
533
|
message: error.message,
|
|
476
534
|
code: error.code,
|
|
477
535
|
details: error.details,
|
|
536
|
+
statusCode: response.status,
|
|
537
|
+
apiCode: apiCode !== undefined && apiCode !== null
|
|
538
|
+
? String(apiCode)
|
|
539
|
+
: undefined,
|
|
540
|
+
apiStatus: body.status !== undefined && body.status !== null
|
|
541
|
+
? String(body.status)
|
|
542
|
+
: undefined,
|
|
543
|
+
errors,
|
|
544
|
+
retryable: typeof nested?.retryable === "boolean" ? nested.retryable : undefined,
|
|
545
|
+
rawBody,
|
|
478
546
|
},
|
|
479
547
|
};
|
|
480
548
|
}
|
|
@@ -531,10 +599,28 @@ class HttpClient {
|
|
|
531
599
|
}
|
|
532
600
|
if (data && typeof data === "object") {
|
|
533
601
|
const obj = data;
|
|
534
|
-
|
|
602
|
+
const nested = this.nestedError(obj);
|
|
603
|
+
return (obj.message || (nested ? nested.message : obj.error) || obj.detail);
|
|
535
604
|
}
|
|
536
605
|
return undefined;
|
|
537
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* @category HTTP Client Internals
|
|
609
|
+
* @summary Returns the error object of a nested error body
|
|
610
|
+
* @description Elements routes answer with {"error": {"code", "message", "retryable"}} instead of top-level fields
|
|
611
|
+
*
|
|
612
|
+
* @param data - The parsed response data
|
|
613
|
+
* @returns The nested error object, or undefined when `error` is not an object
|
|
614
|
+
*/
|
|
615
|
+
nestedError(data) {
|
|
616
|
+
if (!data || typeof data !== "object") {
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
const error = data.error;
|
|
620
|
+
return error && typeof error === "object" && !Array.isArray(error)
|
|
621
|
+
? error
|
|
622
|
+
: undefined;
|
|
623
|
+
}
|
|
538
624
|
/**
|
|
539
625
|
* @category HTTP Client Internals
|
|
540
626
|
* @summary Extracts validation details from error response
|
|
@@ -578,6 +664,29 @@ class HttpClient {
|
|
|
578
664
|
* @param params - Optional query parameters
|
|
579
665
|
* @returns Complete request URL
|
|
580
666
|
*/
|
|
667
|
+
/**
|
|
668
|
+
* @category HTTP Client
|
|
669
|
+
* @summary Returns the configured base URL for another API version
|
|
670
|
+
* @description Replaces a trailing `/api/vN` path segment with `/api/{version}`, or appends `/api/{version}`
|
|
671
|
+
*
|
|
672
|
+
* @param version - API version such as "v2"
|
|
673
|
+
* @returns Base URL for that API version
|
|
674
|
+
*/
|
|
675
|
+
versionedBaseURL(version) {
|
|
676
|
+
const base = (this.config.baseURL || "https://panel.tapsilat.dev/api/v1").replace(/\/+$/, "");
|
|
677
|
+
const url = new URL(base);
|
|
678
|
+
const segments = url.pathname.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
679
|
+
if (segments.length >= 2 &&
|
|
680
|
+
segments[segments.length - 2] === "api" &&
|
|
681
|
+
segments[segments.length - 1].startsWith("v")) {
|
|
682
|
+
segments[segments.length - 1] = version;
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
segments.push("api", version);
|
|
686
|
+
}
|
|
687
|
+
url.pathname = "/" + segments.join("/");
|
|
688
|
+
return url.toString().replace(/\/+$/, "");
|
|
689
|
+
}
|
|
581
690
|
buildUrl(url, customBaseURL, params) {
|
|
582
691
|
const baseURL = customBaseURL ||
|
|
583
692
|
this.config.baseURL ||
|
|
@@ -1221,6 +1330,48 @@ function validateInstallments(installments) {
|
|
|
1221
1330
|
};
|
|
1222
1331
|
}
|
|
1223
1332
|
}
|
|
1333
|
+
// GSM NUMBER CLEANING
|
|
1334
|
+
// Summary: Cleans a GSM number the same way the other Tapsilat SDKs do
|
|
1335
|
+
// Description: Strips spaces, dashes and parentheses, accepts digits with an optional leading +, and checks a minimum length per format
|
|
1336
|
+
/**
|
|
1337
|
+
* Cleans and validates a GSM number for order creation
|
|
1338
|
+
*
|
|
1339
|
+
* @summary Clean a GSM number with the SDK-wide rules
|
|
1340
|
+
* @description Removes spaces, dashes and parentheses, then checks that only digits and + remain and that the
|
|
1341
|
+
* number is long enough for its format (international +, 00, national 0 or local). Any country is accepted.
|
|
1342
|
+
*
|
|
1343
|
+
* @param phone - Phone number as entered
|
|
1344
|
+
* @returns The cleaned number, or an empty string when phone is empty
|
|
1345
|
+
* @throws {TapsilatValidationError} When the number is malformed or too short
|
|
1346
|
+
*/
|
|
1347
|
+
function cleanGsmNumber(phone) {
|
|
1348
|
+
if (!phone) {
|
|
1349
|
+
return "";
|
|
1350
|
+
}
|
|
1351
|
+
const cleaned = phone.replace(/[\s\-()]/g, "");
|
|
1352
|
+
if (!/^[+0-9]+$/.test(cleaned) || cleaned.replace(/\+/g, "") === "") {
|
|
1353
|
+
throw new TapsilatValidationError(`Invalid phone number format: ${phone}`);
|
|
1354
|
+
}
|
|
1355
|
+
if (cleaned.startsWith("+")) {
|
|
1356
|
+
if (cleaned.length < 8) {
|
|
1357
|
+
throw new TapsilatValidationError(`International phone number too short: ${phone}`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
else if (cleaned.startsWith("00")) {
|
|
1361
|
+
if (cleaned.length < 9) {
|
|
1362
|
+
throw new TapsilatValidationError(`International phone number (00 format) too short: ${phone}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
else if (cleaned.startsWith("0")) {
|
|
1366
|
+
if (cleaned.length < 7) {
|
|
1367
|
+
throw new TapsilatValidationError(`National phone number too short: ${phone}`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
else if (cleaned.length < 6) {
|
|
1371
|
+
throw new TapsilatValidationError(`Local phone number too short: ${phone}`);
|
|
1372
|
+
}
|
|
1373
|
+
return cleaned;
|
|
1374
|
+
}
|
|
1224
1375
|
|
|
1225
1376
|
/**
|
|
1226
1377
|
* @category Configuration
|
|
@@ -1325,7 +1476,7 @@ class ConfigManager {
|
|
|
1325
1476
|
* @throws {TapsilatError} On any response error
|
|
1326
1477
|
*/
|
|
1327
1478
|
const handleResponse = (response, errorContext) => {
|
|
1328
|
-
if (!response.success ||
|
|
1479
|
+
if (!response.success || response.data === undefined || response.data === null) {
|
|
1329
1480
|
// Convert API errors to domain-specific errors
|
|
1330
1481
|
if (response.error) {
|
|
1331
1482
|
throw TapsilatError.fromAPIError(response.error);
|
|
@@ -1334,6 +1485,23 @@ const handleResponse = (response, errorContext) => {
|
|
|
1334
1485
|
}
|
|
1335
1486
|
return response.data;
|
|
1336
1487
|
};
|
|
1488
|
+
/**
|
|
1489
|
+
* Response handler for endpoints that may answer with an empty body
|
|
1490
|
+
*
|
|
1491
|
+
* @summary Process API responses whose success body can be empty
|
|
1492
|
+
* @description Like handleResponse, but a successful empty body (for example HTTP 204) resolves to undefined
|
|
1493
|
+
*
|
|
1494
|
+
* @param response - The API response to process
|
|
1495
|
+
* @param errorContext - Descriptive context for error messages
|
|
1496
|
+
* @returns The data from a successful response, or undefined when the body was empty
|
|
1497
|
+
* @throws {TapsilatError} On any response error
|
|
1498
|
+
*/
|
|
1499
|
+
const handleOptionalResponse = (response, errorContext) => {
|
|
1500
|
+
if (response.success) {
|
|
1501
|
+
return response.data ?? undefined;
|
|
1502
|
+
}
|
|
1503
|
+
return handleResponse(response, errorContext);
|
|
1504
|
+
};
|
|
1337
1505
|
// ERROR HANDLING
|
|
1338
1506
|
// Summary: Centralized error handling for consistent error reporting
|
|
1339
1507
|
// Description: Processes caught errors and ensures they are properly converted to TapsilatError types
|
|
@@ -1358,26 +1526,510 @@ const handleError = (error, errorContext) => {
|
|
|
1358
1526
|
: `Unknown error during ${errorContext}`, "NETWORK_ERROR");
|
|
1359
1527
|
};
|
|
1360
1528
|
|
|
1529
|
+
/**
|
|
1530
|
+
* Shared request handling for the SDK's grouped services
|
|
1531
|
+
*
|
|
1532
|
+
* @summary Base class for API service groups
|
|
1533
|
+
* @description Unwraps responses, converts failures to TapsilatError and builds version 2 request configs.
|
|
1534
|
+
*/
|
|
1535
|
+
class ServiceBase {
|
|
1536
|
+
httpClient;
|
|
1537
|
+
constructor(httpClient) {
|
|
1538
|
+
this.httpClient = httpClient;
|
|
1539
|
+
}
|
|
1540
|
+
/** Runs a request and returns its data, throwing on failure or an empty body */
|
|
1541
|
+
async request(context, call) {
|
|
1542
|
+
try {
|
|
1543
|
+
return handleResponse(await call(), context);
|
|
1544
|
+
}
|
|
1545
|
+
catch (error) {
|
|
1546
|
+
return handleError(error, context.toLowerCase());
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/** Runs a request whose successful response may have an empty body */
|
|
1550
|
+
async requestOptional(context, call) {
|
|
1551
|
+
try {
|
|
1552
|
+
return handleOptionalResponse(await call(), context);
|
|
1553
|
+
}
|
|
1554
|
+
catch (error) {
|
|
1555
|
+
return handleError(error, context.toLowerCase());
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/** Request config for version 2 routes, derived from the configured base URL */
|
|
1559
|
+
v2(config = {}) {
|
|
1560
|
+
return { ...config, baseURL: this.httpClient.versionedBaseURL("v2") };
|
|
1561
|
+
}
|
|
1562
|
+
/** Throws when a required identifier is missing and returns it URL-encoded */
|
|
1563
|
+
id(value, name) {
|
|
1564
|
+
if (!isNonEmptyString(value)) {
|
|
1565
|
+
throw new TapsilatValidationError(`${name} is required and must be a non-empty string`, { provided: value });
|
|
1566
|
+
}
|
|
1567
|
+
return encodeURIComponent(value);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Submerchant management
|
|
1573
|
+
*
|
|
1574
|
+
* @summary Create, read, update and delete submerchants
|
|
1575
|
+
* @description Covers the version 1 submerchant routes, item payout events and marketplace seller creation.
|
|
1576
|
+
* Currency fields accept a currency UUID or an organization unit such as "TRY".
|
|
1577
|
+
*/
|
|
1578
|
+
class SubmerchantService extends ServiceBase {
|
|
1579
|
+
/** Creates a submerchant; the response carries only code and message */
|
|
1580
|
+
async createSubmerchant(request) {
|
|
1581
|
+
return this.request("Create submerchant", () => this.httpClient.post("/submerchants", request));
|
|
1582
|
+
}
|
|
1583
|
+
async getSubmerchant(id) {
|
|
1584
|
+
const path = `/submerchants/${this.id(id, "Submerchant ID")}`;
|
|
1585
|
+
return this.request("Get submerchant", () => this.httpClient.get(path));
|
|
1586
|
+
}
|
|
1587
|
+
/** Lists submerchants; rows are returned under `row` */
|
|
1588
|
+
async listSubmerchants(page = 1, perPage = 10) {
|
|
1589
|
+
return this.request("List submerchants", () => this.httpClient.get("/submerchants", {
|
|
1590
|
+
params: { page, per_page: perPage },
|
|
1591
|
+
}));
|
|
1592
|
+
}
|
|
1593
|
+
/** Replaces the submerchant profile; send every field */
|
|
1594
|
+
async updateSubmerchant(id, request) {
|
|
1595
|
+
const path = `/submerchants/${this.id(id, "Submerchant ID")}`;
|
|
1596
|
+
return this.request("Update submerchant", () => this.httpClient.patch(path, request));
|
|
1597
|
+
}
|
|
1598
|
+
async deleteSubmerchant(id) {
|
|
1599
|
+
const path = `/submerchants/${this.id(id, "Submerchant ID")}`;
|
|
1600
|
+
return this.request("Delete submerchant", () => this.httpClient.delete(path));
|
|
1601
|
+
}
|
|
1602
|
+
async getSuborganizationBySubmerchant(submerchantId) {
|
|
1603
|
+
const path = `/submerchants/${this.id(submerchantId, "Submerchant ID")}/suborganization`;
|
|
1604
|
+
return this.request("Get suborganization by submerchant", () => this.httpClient.get(path));
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Records an item business event; time gates, approval and settlement remain separate.
|
|
1608
|
+
* Repeating an event with the same idempotency key and payload is safe.
|
|
1609
|
+
*/
|
|
1610
|
+
async recordSubmerchantPayoutEvent(request) {
|
|
1611
|
+
return this.request("Record submerchant payout event", () => this.httpClient.post("/submerchant-payout-events", request));
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Creates a logical marketplace seller; inspect `provisionings` for each VPOS account's readiness.
|
|
1615
|
+
* Reuse the same idempotency key and request data when retrying.
|
|
1616
|
+
*/
|
|
1617
|
+
async createMarketplaceSubmerchant(request) {
|
|
1618
|
+
return this.request("Create marketplace submerchant", () => this.httpClient.post("/submerchants", request, this.v2()));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Virtual POS management
|
|
1624
|
+
*
|
|
1625
|
+
* @summary Manage VPOS configurations, their submerchants and acquirer lookups
|
|
1626
|
+
* @description Updates replace the stored configuration: read the VPOS first and send every field back.
|
|
1627
|
+
*/
|
|
1628
|
+
class VposService extends ServiceBase {
|
|
1629
|
+
async listVpos(params = {}) {
|
|
1630
|
+
return this.request("List VPOS", () => this.httpClient.get("/vpos", {
|
|
1631
|
+
params: { page: 1, per_page: 10, ...params },
|
|
1632
|
+
}));
|
|
1633
|
+
}
|
|
1634
|
+
/** Creates a VPOS; `currencies` accepts currency UUIDs or units such as "TRY" */
|
|
1635
|
+
async createVpos(request) {
|
|
1636
|
+
return this.request("Create VPOS", () => this.httpClient.post("/vpos", request));
|
|
1637
|
+
}
|
|
1638
|
+
async getVpos(id) {
|
|
1639
|
+
const path = `/vpos/${this.id(id, "VPOS ID")}`;
|
|
1640
|
+
return this.request("Get VPOS", () => this.httpClient.get(path));
|
|
1641
|
+
}
|
|
1642
|
+
/** Replaces a VPOS configuration; fields left out are stored as empty or false */
|
|
1643
|
+
async updateVpos(id, request) {
|
|
1644
|
+
const path = `/vpos/${this.id(id, "VPOS ID")}`;
|
|
1645
|
+
return this.request("Update VPOS", () => this.httpClient.patch(path, request));
|
|
1646
|
+
}
|
|
1647
|
+
async deleteVpos(id) {
|
|
1648
|
+
const path = `/vpos/${this.id(id, "VPOS ID")}`;
|
|
1649
|
+
return this.request("Delete VPOS", () => this.httpClient.delete(path));
|
|
1650
|
+
}
|
|
1651
|
+
async listVposAcquirers() {
|
|
1652
|
+
return this.request("List VPOS acquirers", () => this.httpClient.get("/vpos/acquirers"));
|
|
1653
|
+
}
|
|
1654
|
+
async listCardSchemes() {
|
|
1655
|
+
return this.request("List card schemes", () => this.httpClient.get("/vpos/card-schemes"));
|
|
1656
|
+
}
|
|
1657
|
+
async listVposAcquirerTemplates() {
|
|
1658
|
+
return this.request("List VPOS acquirer templates", () => this.httpClient.get("/vpos/acquirer-templates"));
|
|
1659
|
+
}
|
|
1660
|
+
async listVposSubmerchants(params = {}) {
|
|
1661
|
+
return this.request("List VPOS submerchants", () => this.httpClient.get("/vpos-submerchant", {
|
|
1662
|
+
params: { page: 1, per_page: 10, ...params },
|
|
1663
|
+
}));
|
|
1664
|
+
}
|
|
1665
|
+
async createVposSubmerchant(request) {
|
|
1666
|
+
return this.request("Create VPOS submerchant", () => this.httpClient.post("/vpos-submerchant", request));
|
|
1667
|
+
}
|
|
1668
|
+
async getVposSubmerchant(id) {
|
|
1669
|
+
const path = `/vpos-submerchant/${this.id(id, "VPOS submerchant ID")}`;
|
|
1670
|
+
return this.request("Get VPOS submerchant", () => this.httpClient.get(path));
|
|
1671
|
+
}
|
|
1672
|
+
async updateVposSubmerchant(id, request) {
|
|
1673
|
+
const path = `/vpos-submerchant/${this.id(id, "VPOS submerchant ID")}`;
|
|
1674
|
+
return this.request("Update VPOS submerchant", () => this.httpClient.patch(path, request));
|
|
1675
|
+
}
|
|
1676
|
+
async deleteVposSubmerchant(id) {
|
|
1677
|
+
const path = `/vpos-submerchant/${this.id(id, "VPOS submerchant ID")}`;
|
|
1678
|
+
return this.request("Delete VPOS submerchant", () => this.httpClient.delete(path));
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Card tokenization
|
|
1684
|
+
*
|
|
1685
|
+
* @summary Tokenize, list and delete saved cards
|
|
1686
|
+
*/
|
|
1687
|
+
class TokenizationService extends ServiceBase {
|
|
1688
|
+
/** Tokenizes a card; returns 3D secure form details when verification is required */
|
|
1689
|
+
async tokenizeCard(request) {
|
|
1690
|
+
return this.request("Tokenize card", () => this.httpClient.post("/tokenization/card/tokenize", request));
|
|
1691
|
+
}
|
|
1692
|
+
async listSavedCards(page = 1, perPage = 10) {
|
|
1693
|
+
return this.request("List saved cards", () => this.httpClient.get("/tokenization/card/list", {
|
|
1694
|
+
params: { page, per_page: perPage },
|
|
1695
|
+
}));
|
|
1696
|
+
}
|
|
1697
|
+
async deleteSavedCard(id) {
|
|
1698
|
+
const path = `/tokenization/card/${this.id(id, "Card ID")}`;
|
|
1699
|
+
return this.request("Delete saved card", () => this.httpClient.delete(path));
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Marketplace API (version 2)
|
|
1705
|
+
*
|
|
1706
|
+
* @summary Manage marketplace sellers, payouts, policies and payout operations
|
|
1707
|
+
* @description All calls use the version 2 routes derived from the configured base URL and are scoped to the
|
|
1708
|
+
* API token's organization. Revisions are opaque strings: send them back exactly as received.
|
|
1709
|
+
*/
|
|
1710
|
+
class MarketplaceService extends ServiceBase {
|
|
1711
|
+
currencies;
|
|
1712
|
+
constructor(httpClient, currencies) {
|
|
1713
|
+
super(httpClient);
|
|
1714
|
+
this.currencies = currencies;
|
|
1715
|
+
}
|
|
1716
|
+
/** Lists sellers; `external_id` selects an exact external ID match */
|
|
1717
|
+
async listMarketplaceSubmerchants(params = {}) {
|
|
1718
|
+
return this.request("List marketplace submerchants", () => this.httpClient.get("/submerchants", this.v2({ params: { page: 1, per_page: 20, ...params } })));
|
|
1719
|
+
}
|
|
1720
|
+
/** Returns the seller profile and its current revision */
|
|
1721
|
+
async getMarketplaceSubmerchant(id) {
|
|
1722
|
+
const path = `/submerchants/${this.id(id, "Seller ID")}`;
|
|
1723
|
+
return this.request("Get marketplace submerchant", () => this.httpClient.get(path, this.v2()));
|
|
1724
|
+
}
|
|
1725
|
+
/** Replaces the seller profile at its revision; inspect `approval_required` */
|
|
1726
|
+
async updateMarketplaceSubmerchant(id, input) {
|
|
1727
|
+
const path = `/submerchants/${this.id(id, "Seller ID")}`;
|
|
1728
|
+
return this.request("Update marketplace submerchant", () => this.httpClient.patch(path, input, this.v2()));
|
|
1729
|
+
}
|
|
1730
|
+
/** Returns the selected accounts' requirements for a seller operation */
|
|
1731
|
+
async getMarketplaceSellerContracts(filter) {
|
|
1732
|
+
const query = new URLSearchParams({
|
|
1733
|
+
currency_id: filter.currency_id,
|
|
1734
|
+
seller_type: filter.seller_type,
|
|
1735
|
+
operation: filter.operation,
|
|
1736
|
+
});
|
|
1737
|
+
for (const vposId of filter.vpos_ids ?? []) {
|
|
1738
|
+
query.append("vpos_id", vposId);
|
|
1739
|
+
}
|
|
1740
|
+
return this.request("Get marketplace seller contracts", () => this.httpClient.get(`/marketplace/seller-contracts?${query.toString()}`, this.v2()));
|
|
1741
|
+
}
|
|
1742
|
+
/** Returns account registration, synchronization and available actions */
|
|
1743
|
+
async getMarketplaceSellerAccounts(id) {
|
|
1744
|
+
const path = `/submerchants/${this.id(id, "Seller ID")}/providers`;
|
|
1745
|
+
return this.request("Get marketplace seller accounts", () => this.httpClient.get(path, this.v2()));
|
|
1746
|
+
}
|
|
1747
|
+
/** Runs an action listed on the account at its revision and environment; inspect the returned status */
|
|
1748
|
+
async runMarketplaceSellerAccountAction(id, vposId, action) {
|
|
1749
|
+
const path = `/submerchants/${this.id(id, "Seller ID")}/providers/${this.id(vposId, "VPOS ID")}/actions`;
|
|
1750
|
+
return this.request("Run marketplace seller account action", () => this.httpClient.post(path, action, this.v2()));
|
|
1751
|
+
}
|
|
1752
|
+
async listMarketplacePayouts(filter = {}) {
|
|
1753
|
+
return this.request("List marketplace payouts", () => this.httpClient.get("/submerchant-payouts", this.v2({ params: { page: 1, per_page: 20, ...filter } })));
|
|
1754
|
+
}
|
|
1755
|
+
/** Returns a payout's eligibility, approval and settlement state */
|
|
1756
|
+
async getMarketplacePayout(id) {
|
|
1757
|
+
const path = `/submerchant-payouts/${this.id(id, "Payout ID")}`;
|
|
1758
|
+
return this.request("Get marketplace payout", () => this.httpClient.get(path, this.v2()));
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Submits up to 100 eligible payouts for approval without bypassing release gates.
|
|
1762
|
+
* "accepted" acknowledges approval; poll the payout for settlement.
|
|
1763
|
+
*/
|
|
1764
|
+
async approveMarketplacePayouts(input) {
|
|
1765
|
+
return this.request("Approve marketplace payouts", () => this.httpClient.post("/submerchant-payouts/approve", input, this.v2()));
|
|
1766
|
+
}
|
|
1767
|
+
async listMarketplaceApprovalPolicies() {
|
|
1768
|
+
return this.request("List marketplace approval policies", () => this.httpClient.get("/marketplace/approval-policies", this.v2()));
|
|
1769
|
+
}
|
|
1770
|
+
async getMarketplaceApprovalPolicy(id) {
|
|
1771
|
+
const path = `/marketplace/approval-policies/${this.id(id, "Policy ID")}`;
|
|
1772
|
+
return this.request("Get marketplace approval policy", () => this.httpClient.get(path, this.v2()));
|
|
1773
|
+
}
|
|
1774
|
+
/** Creates an approval rule; a currency unit such as "TRY" is resolved to its UUID */
|
|
1775
|
+
async createMarketplaceApprovalPolicy(input) {
|
|
1776
|
+
const body = await this.withResolvedCurrency(input);
|
|
1777
|
+
await this.requestOptional("Create marketplace approval policy", () => this.httpClient.post("/marketplace/approval-policies", body, this.v2()));
|
|
1778
|
+
}
|
|
1779
|
+
/** Replaces an approval rule; a currency unit such as "TRY" is resolved to its UUID */
|
|
1780
|
+
async updateMarketplaceApprovalPolicy(id, input) {
|
|
1781
|
+
const path = `/marketplace/approval-policies/${this.id(id, "Policy ID")}`;
|
|
1782
|
+
const body = await this.withResolvedCurrency(input);
|
|
1783
|
+
await this.requestOptional("Update marketplace approval policy", () => this.httpClient.patch(path, body, this.v2()));
|
|
1784
|
+
}
|
|
1785
|
+
async deleteMarketplaceApprovalPolicy(id) {
|
|
1786
|
+
const path = `/marketplace/approval-policies/${this.id(id, "Policy ID")}`;
|
|
1787
|
+
await this.requestOptional("Delete marketplace approval policy", () => this.httpClient.delete(path, this.v2()));
|
|
1788
|
+
}
|
|
1789
|
+
async listMarketplaceReleasePolicies() {
|
|
1790
|
+
return this.request("List marketplace release policies", () => this.httpClient.get("/marketplace/release-policies", this.v2()));
|
|
1791
|
+
}
|
|
1792
|
+
async getMarketplaceReleasePolicy(id) {
|
|
1793
|
+
const path = `/marketplace/release-policies/${this.id(id, "Policy ID")}`;
|
|
1794
|
+
return this.request("Get marketplace release policy", () => this.httpClient.get(path, this.v2()));
|
|
1795
|
+
}
|
|
1796
|
+
async createMarketplaceReleasePolicy(input) {
|
|
1797
|
+
return this.request("Create marketplace release policy", () => this.httpClient.post("/marketplace/release-policies", input, this.v2()));
|
|
1798
|
+
}
|
|
1799
|
+
async updateMarketplaceReleasePolicy(id, input) {
|
|
1800
|
+
const path = `/marketplace/release-policies/${this.id(id, "Policy ID")}`;
|
|
1801
|
+
return this.request("Update marketplace release policy", () => this.httpClient.patch(path, input, this.v2()));
|
|
1802
|
+
}
|
|
1803
|
+
async deleteMarketplaceReleasePolicy(id) {
|
|
1804
|
+
const path = `/marketplace/release-policies/${this.id(id, "Policy ID")}`;
|
|
1805
|
+
await this.requestOptional("Delete marketplace release policy", () => this.httpClient.delete(path, this.v2()));
|
|
1806
|
+
}
|
|
1807
|
+
async getMarketplaceApprovalSettings() {
|
|
1808
|
+
return this.request("Get marketplace approval settings", () => this.httpClient.get("/marketplace/approval-settings", this.v2()));
|
|
1809
|
+
}
|
|
1810
|
+
async updateMarketplaceApprovalSettings(input) {
|
|
1811
|
+
return this.request("Update marketplace approval settings", () => this.httpClient.patch("/marketplace/approval-settings", input, this.v2()));
|
|
1812
|
+
}
|
|
1813
|
+
/** Changes a payout allocation or withdraws approval; inspect `status` before treating it as complete */
|
|
1814
|
+
async runMarketplacePayoutOperation(payoutId, input) {
|
|
1815
|
+
const path = `/submerchant-payouts/${this.id(payoutId, "Payout ID")}/operations`;
|
|
1816
|
+
return this.request("Run marketplace payout operation", () => this.httpClient.post(path, input, this.v2()));
|
|
1817
|
+
}
|
|
1818
|
+
/** Returns a persisted payout operation receipt */
|
|
1819
|
+
async getMarketplacePayoutOperation(id) {
|
|
1820
|
+
const path = `/marketplace/payout-operations/${this.id(id, "Operation ID")}`;
|
|
1821
|
+
return this.request("Get marketplace payout operation", () => this.httpClient.get(path, this.v2()));
|
|
1822
|
+
}
|
|
1823
|
+
/** Checks the provider result without repeating the change */
|
|
1824
|
+
async reconcileMarketplacePayoutOperation(id) {
|
|
1825
|
+
const path = `/marketplace/payout-operations/${this.id(id, "Operation ID")}/reconcile`;
|
|
1826
|
+
return this.request("Reconcile marketplace payout operation", () => this.httpClient.post(path, undefined, this.v2()));
|
|
1827
|
+
}
|
|
1828
|
+
/** Repeats an unresolved change only after the previous provider state is verified */
|
|
1829
|
+
async retryMarketplacePayoutOperation(id) {
|
|
1830
|
+
const path = `/marketplace/payout-operations/${this.id(id, "Operation ID")}/retry`;
|
|
1831
|
+
return this.request("Retry marketplace payout operation", () => this.httpClient.post(path, undefined, this.v2()));
|
|
1832
|
+
}
|
|
1833
|
+
async withResolvedCurrency(input) {
|
|
1834
|
+
if (!input.currency_id || input.currency_id.trim() === "") {
|
|
1835
|
+
return input;
|
|
1836
|
+
}
|
|
1837
|
+
return {
|
|
1838
|
+
...input,
|
|
1839
|
+
currency_id: await this.currencies.resolve(input.currency_id),
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const UUID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1845
|
+
/**
|
|
1846
|
+
* Resolves organization currency units such as "TRY" to currency UUIDs
|
|
1847
|
+
*
|
|
1848
|
+
* @summary Cached lookup of organization currency IDs by unit
|
|
1849
|
+
* @description Most endpoints accept currency units directly. Use this where the server requires a UUID,
|
|
1850
|
+
* such as marketplace approval policies. The lookup is cached until invalidate() is called.
|
|
1851
|
+
*/
|
|
1852
|
+
class CurrencyResolver {
|
|
1853
|
+
httpClient;
|
|
1854
|
+
idsByUnit;
|
|
1855
|
+
constructor(httpClient) {
|
|
1856
|
+
this.httpClient = httpClient;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Returns the currency UUID for ref; a UUID is returned unchanged
|
|
1860
|
+
*
|
|
1861
|
+
* @param ref - Currency UUID or organization currency unit
|
|
1862
|
+
* @throws {TapsilatValidationError} When ref is empty or not one of the organization's currencies
|
|
1863
|
+
*/
|
|
1864
|
+
async resolve(ref) {
|
|
1865
|
+
const trimmed = (ref ?? "").trim();
|
|
1866
|
+
if (trimmed === "") {
|
|
1867
|
+
throw new TapsilatValidationError("currency_id is required");
|
|
1868
|
+
}
|
|
1869
|
+
if (UUID_PATTERN.test(trimmed)) {
|
|
1870
|
+
return trimmed;
|
|
1871
|
+
}
|
|
1872
|
+
const id = (await this.load()).get(trimmed.toUpperCase());
|
|
1873
|
+
if (!id) {
|
|
1874
|
+
throw new TapsilatValidationError(`Unknown currency reference "${ref}"; use a currency UUID or organization currency unit`);
|
|
1875
|
+
}
|
|
1876
|
+
return id;
|
|
1877
|
+
}
|
|
1878
|
+
/** Clears the cached currencies, for example after creating a currency */
|
|
1879
|
+
invalidate() {
|
|
1880
|
+
this.idsByUnit = undefined;
|
|
1881
|
+
}
|
|
1882
|
+
load() {
|
|
1883
|
+
if (!this.idsByUnit) {
|
|
1884
|
+
this.idsByUnit = this.fetch().catch((error) => {
|
|
1885
|
+
this.idsByUnit = undefined;
|
|
1886
|
+
throw error;
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
return this.idsByUnit;
|
|
1890
|
+
}
|
|
1891
|
+
async fetch() {
|
|
1892
|
+
try {
|
|
1893
|
+
const response = await this.httpClient.get("/organization/currencies");
|
|
1894
|
+
const data = handleResponse(response, "Get organization currencies");
|
|
1895
|
+
const map = new Map();
|
|
1896
|
+
for (const currency of data.currencies ?? []) {
|
|
1897
|
+
const unit = (currency.currency_unit ?? "").trim().toUpperCase();
|
|
1898
|
+
if (unit && currency.id) {
|
|
1899
|
+
map.set(unit, currency.id);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return map;
|
|
1903
|
+
}
|
|
1904
|
+
catch (error) {
|
|
1905
|
+
return handleError(error, "get organization currencies");
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/** What a redacted secret prints as */
|
|
1911
|
+
const REDACTED = "[redacted]";
|
|
1912
|
+
/** The hook Node.js calls when console.log or util.inspect prints an object */
|
|
1913
|
+
const INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
1914
|
+
/**
|
|
1915
|
+
* Makes console.log and util.inspect print an object's `client_secret` as "[redacted]"
|
|
1916
|
+
*
|
|
1917
|
+
* @summary Keep a client secret out of logs
|
|
1918
|
+
* @description
|
|
1919
|
+
* Adds a non-enumerable inspect hook to the object itself, so JSON.stringify, property access and
|
|
1920
|
+
* Object.keys still see the real secret. A copy made with spread or structuredClone does not keep
|
|
1921
|
+
* the hook.
|
|
1922
|
+
*
|
|
1923
|
+
* @param value - A response object that carries `client_secret`
|
|
1924
|
+
* @returns The same object
|
|
1925
|
+
*/
|
|
1926
|
+
const redactClientSecret = (value) => {
|
|
1927
|
+
if (value && typeof value === "object" && "client_secret" in value) {
|
|
1928
|
+
Object.defineProperty(value, INSPECT, {
|
|
1929
|
+
value: function () {
|
|
1930
|
+
return { ...this, client_secret: REDACTED };
|
|
1931
|
+
},
|
|
1932
|
+
enumerable: false,
|
|
1933
|
+
configurable: true,
|
|
1934
|
+
writable: true,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
return value;
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
/** The header that carries the signature of every callback Tapsilat sends */
|
|
1941
|
+
const WEBHOOK_SIGNATURE_HEADER = "Tapsilat-Signature";
|
|
1942
|
+
/** How many seconds a callback's signature timestamp may differ from the current time */
|
|
1943
|
+
const WEBHOOK_TOLERANCE = 300;
|
|
1944
|
+
const LEGACY_PREFIX = "sha256=";
|
|
1945
|
+
const hmacHex = (secret, ...parts) => {
|
|
1946
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
1947
|
+
for (const part of parts) {
|
|
1948
|
+
hmac.update(part);
|
|
1949
|
+
}
|
|
1950
|
+
return hmac.digest("hex");
|
|
1951
|
+
};
|
|
1952
|
+
// Compare in constant time so the signature cannot be guessed byte by byte
|
|
1953
|
+
const safeEqual = (expected, candidate) => {
|
|
1954
|
+
const a = Buffer.from(expected);
|
|
1955
|
+
const b = Buffer.from(candidate);
|
|
1956
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
1957
|
+
};
|
|
1958
|
+
const checkSeconds = (value, name) => {
|
|
1959
|
+
if (value !== undefined && (typeof value !== "number" || Number.isNaN(value))) {
|
|
1960
|
+
throw new TapsilatValidationError(`Webhook ${name} must be a number of seconds`);
|
|
1961
|
+
}
|
|
1962
|
+
return value;
|
|
1963
|
+
};
|
|
1361
1964
|
// WEBHOOK SIGNATURE VERIFICATION
|
|
1362
|
-
// Summary:
|
|
1363
|
-
// Description:
|
|
1965
|
+
// Summary: Verify the Tapsilat-Signature header of a callback
|
|
1966
|
+
// Description: Checks the timestamped HMAC-SHA256 that Tapsilat signs every callback with
|
|
1364
1967
|
/**
|
|
1365
|
-
* Verifies
|
|
1968
|
+
* Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
|
|
1969
|
+
*
|
|
1970
|
+
* @summary Verify the Tapsilat-Signature header of a callback
|
|
1971
|
+
* @description
|
|
1972
|
+
* Tapsilat sends "t=<unix timestamp>,v1=<hex HMAC-SHA256 of '<timestamp>.<raw body>'>". The timestamp
|
|
1973
|
+
* must be within `tolerance` seconds of `now` so that a captured callback cannot be replayed. The
|
|
1974
|
+
* comparison takes constant time.
|
|
1366
1975
|
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1976
|
+
* A bare hex HMAC-SHA256 of the payload, with or without a "sha256=" prefix, is still accepted for
|
|
1977
|
+
* signatures made the way earlier versions of this SDK documented.
|
|
1369
1978
|
*
|
|
1370
|
-
* @param payload -
|
|
1371
|
-
* @param signature -
|
|
1372
|
-
* @param secret -
|
|
1373
|
-
* @
|
|
1979
|
+
* @param payload - The raw request body, before any JSON decoding
|
|
1980
|
+
* @param signature - The Tapsilat-Signature header
|
|
1981
|
+
* @param secret - The webhook secret from the panel's callback settings
|
|
1982
|
+
* @param options - Timestamp tolerance and the current time
|
|
1983
|
+
* @returns true when the signature is valid; false when it is missing, malformed, stale or wrong
|
|
1984
|
+
* @throws {TapsilatValidationError} When payload is not a string or bytes, or an option is not a number
|
|
1374
1985
|
*/
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1986
|
+
const verifyWebhookSignature = (payload, signature, secret, options = {}) => {
|
|
1987
|
+
if (typeof payload !== "string" && !(payload instanceof Uint8Array)) {
|
|
1988
|
+
throw new TapsilatValidationError("Webhook payload must be the raw request body as a string or Buffer");
|
|
1989
|
+
}
|
|
1990
|
+
const tolerance = checkSeconds(options.tolerance, "tolerance") ?? WEBHOOK_TOLERANCE;
|
|
1991
|
+
const now = checkSeconds(options.now, "now") ?? Math.floor(Date.now() / 1000);
|
|
1992
|
+
const header = typeof signature === "string" ? signature.trim() : "";
|
|
1993
|
+
if (header === "" || typeof secret !== "string" || secret === "") {
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
if (header.startsWith(LEGACY_PREFIX) || !header.includes("=")) {
|
|
1997
|
+
const legacy = header.startsWith(LEGACY_PREFIX) ? header.slice(LEGACY_PREFIX.length) : header;
|
|
1998
|
+
return safeEqual(hmacHex(secret, payload), legacy.toLowerCase());
|
|
1999
|
+
}
|
|
2000
|
+
let timestamp;
|
|
2001
|
+
const candidates = [];
|
|
2002
|
+
for (const part of header.split(",")) {
|
|
2003
|
+
const pair = part.trim();
|
|
2004
|
+
const separator = pair.indexOf("=");
|
|
2005
|
+
if (separator === -1) {
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
const key = pair.slice(0, separator);
|
|
2009
|
+
const value = pair.slice(separator + 1);
|
|
2010
|
+
if (key === "t") {
|
|
2011
|
+
timestamp = value;
|
|
2012
|
+
}
|
|
2013
|
+
else if (key === "v1") {
|
|
2014
|
+
candidates.push(value.toLowerCase());
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (timestamp === undefined || !/^[0-9]+$/.test(timestamp) || candidates.length === 0) {
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
const signedAt = Number(timestamp);
|
|
2021
|
+
if (!Number.isSafeInteger(signedAt)) {
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
if (tolerance > 0 && Math.abs(now - signedAt) > tolerance) {
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
const expected = hmacHex(secret, `${timestamp}.`, payload);
|
|
2028
|
+
let valid = false;
|
|
2029
|
+
for (const candidate of candidates) {
|
|
2030
|
+
valid = safeEqual(expected, candidate) || valid;
|
|
2031
|
+
}
|
|
2032
|
+
return valid;
|
|
1381
2033
|
};
|
|
1382
2034
|
|
|
1383
2035
|
/**
|
|
@@ -1400,6 +2052,15 @@ const verifyHmacSignature = (payload, signature, secret) => {
|
|
|
1400
2052
|
class TapsilatSDK {
|
|
1401
2053
|
httpClient;
|
|
1402
2054
|
configManager;
|
|
2055
|
+
currencyResolver;
|
|
2056
|
+
/** Submerchant management, payout events and marketplace seller creation */
|
|
2057
|
+
submerchants;
|
|
2058
|
+
/** VPOS configurations, VPOS submerchants and acquirer lookups */
|
|
2059
|
+
vpos;
|
|
2060
|
+
/** Card tokenization and saved cards */
|
|
2061
|
+
tokenization;
|
|
2062
|
+
/** Marketplace sellers, payouts, policies and payout operations (API version 2) */
|
|
2063
|
+
marketplace;
|
|
1403
2064
|
// SDK INITIALIZATION
|
|
1404
2065
|
// Summary: Initializes the Tapsilat SDK with configuration options
|
|
1405
2066
|
// Description: Creates and configures a new instance of the TapsilatSDK with the provided configuration
|
|
@@ -1431,6 +2092,11 @@ class TapsilatSDK {
|
|
|
1431
2092
|
validateBearerToken(config.bearerToken);
|
|
1432
2093
|
this.configManager = new ConfigManager(config);
|
|
1433
2094
|
this.httpClient = new HttpClient(this.configManager.getInternalConfig());
|
|
2095
|
+
this.currencyResolver = new CurrencyResolver(this.httpClient);
|
|
2096
|
+
this.submerchants = new SubmerchantService(this.httpClient);
|
|
2097
|
+
this.vpos = new VposService(this.httpClient);
|
|
2098
|
+
this.tokenization = new TokenizationService(this.httpClient);
|
|
2099
|
+
this.marketplace = new MarketplaceService(this.httpClient, this.currencyResolver);
|
|
1434
2100
|
}
|
|
1435
2101
|
/**
|
|
1436
2102
|
* Access to order operations
|
|
@@ -1474,6 +2140,8 @@ class TapsilatSDK {
|
|
|
1474
2140
|
getExcel: (id) => this.getOrderExcel(id),
|
|
1475
2141
|
createRefundRequest: (request) => this.createOrderRefundRequest(request),
|
|
1476
2142
|
addOip: (request) => this.addOrderOip(request),
|
|
2143
|
+
charge: (request) => this.chargeOrder(request),
|
|
2144
|
+
allPayments: () => this.getAllOrdersPayments(),
|
|
1477
2145
|
};
|
|
1478
2146
|
}
|
|
1479
2147
|
/**
|
|
@@ -1502,10 +2170,11 @@ class TapsilatSDK {
|
|
|
1502
2170
|
currencies: () => this.getOrganizationCurrencies(),
|
|
1503
2171
|
getLimitUser: (request) => this.getOrganizationLimitUser(request),
|
|
1504
2172
|
setLimitUser: (request) => this.setOrganizationLimitUser(request),
|
|
1505
|
-
limits: () => this.
|
|
2173
|
+
limits: (organizationId) => this.getOrganizationLimitsById(organizationId),
|
|
1506
2174
|
meta: (name) => this.getOrganizationMeta(name),
|
|
1507
2175
|
scopes: () => this.getOrganizationScopes(),
|
|
1508
2176
|
suborganizations: (params) => this.getOrganizationSuborganizations(params),
|
|
2177
|
+
suborganization: (id) => this.getSuborganizationDetail(id),
|
|
1509
2178
|
createUser: (request) => this.createOrganizationUser(request),
|
|
1510
2179
|
verifyUser: (request) => this.verifyOrganizationUser(request),
|
|
1511
2180
|
verifyUserMobile: (request) => this.verifyOrganizationUserMobile(request),
|
|
@@ -1514,6 +2183,9 @@ class TapsilatSDK {
|
|
|
1514
2183
|
suborganizationDetails: (id) => this.getOrganizationSuborganizationDetails(id),
|
|
1515
2184
|
suborganizationSubmerchants: (id) => this.getOrganizationSuborganizationSubmerchants(id),
|
|
1516
2185
|
createUserToken: (request) => this.createOrganizationUserToken(request),
|
|
2186
|
+
createCurrency: (input) => this.createOrganizationCurrency(input),
|
|
2187
|
+
partners: () => this.getOrganizationPartners(),
|
|
2188
|
+
limitsById: (id, params) => this.getOrganizationLimitsById(id, params),
|
|
1517
2189
|
};
|
|
1518
2190
|
}
|
|
1519
2191
|
/**
|
|
@@ -1545,6 +2217,7 @@ class TapsilatSDK {
|
|
|
1545
2217
|
transactionPaymentTypes: () => this.getSystemTransactionPaymentTypes(),
|
|
1546
2218
|
transactionPurposes: () => this.getSystemTransactionPurposes(),
|
|
1547
2219
|
transactionStatuses: () => this.getSystemTransactionStatuses(),
|
|
2220
|
+
config: () => this.getSystemConfig(),
|
|
1548
2221
|
};
|
|
1549
2222
|
}
|
|
1550
2223
|
/**
|
|
@@ -1553,7 +2226,7 @@ class TapsilatSDK {
|
|
|
1553
2226
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1554
2227
|
get webhooks() {
|
|
1555
2228
|
return {
|
|
1556
|
-
verify: (payload, signature, secret) => this.verifyWebhook(payload, signature, secret),
|
|
2229
|
+
verify: (payload, signature, secret, options) => this.verifyWebhook(payload, signature, secret, options),
|
|
1557
2230
|
};
|
|
1558
2231
|
}
|
|
1559
2232
|
// ORDER CREATION
|
|
@@ -1649,11 +2322,30 @@ class TapsilatSDK {
|
|
|
1649
2322
|
if (!isValidEmail(buyer.email)) {
|
|
1650
2323
|
throw new TapsilatValidationError("Buyer email must be a valid email address", { provided: buyer.email });
|
|
1651
2324
|
}
|
|
2325
|
+
const request = buyer.gsm_number
|
|
2326
|
+
? {
|
|
2327
|
+
...orderRequest,
|
|
2328
|
+
buyer: { ...buyer, gsm_number: cleanGsmNumber(buyer.gsm_number) },
|
|
2329
|
+
}
|
|
2330
|
+
: orderRequest;
|
|
1652
2331
|
try {
|
|
1653
2332
|
// Make the API request
|
|
1654
|
-
const createOrderResponse = await this.httpClient.post("/order/create",
|
|
2333
|
+
const createOrderResponse = await this.httpClient.post("/order/create", request);
|
|
1655
2334
|
// Use our generic response handler
|
|
1656
|
-
|
|
2335
|
+
const created = handleResponse(createOrderResponse, "Order creation");
|
|
2336
|
+
// The create response has no checkout URL; read it from the order, ignoring failures
|
|
2337
|
+
if (created.reference_id && !created.checkout_url) {
|
|
2338
|
+
try {
|
|
2339
|
+
const order = await this.getOrder(created.reference_id);
|
|
2340
|
+
if (order?.checkout_url) {
|
|
2341
|
+
created.checkout_url = order.checkout_url;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
catch {
|
|
2345
|
+
// The order was created; a missing checkout URL is not an error
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return created;
|
|
1657
2349
|
}
|
|
1658
2350
|
catch (error) {
|
|
1659
2351
|
// Use our generic error handler
|
|
@@ -2220,19 +2912,15 @@ class TapsilatSDK {
|
|
|
2220
2912
|
if (!isNonEmptyString(referenceId)) {
|
|
2221
2913
|
throw new TapsilatValidationError("Order referenceId is required and must be a non-empty string", { provided: referenceId });
|
|
2222
2914
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
}
|
|
2233
|
-
catch (error) {
|
|
2234
|
-
return handleError(error, "order transactions retrieval");
|
|
2235
|
-
}
|
|
2915
|
+
const getOrderTransactionsResponse = await this.httpClient.get(`/order/${encodeURIComponent(referenceId)}/transactions`);
|
|
2916
|
+
if (!getOrderTransactionsResponse.success)
|
|
2917
|
+
throw TapsilatError.fromAPIError({
|
|
2918
|
+
message: "Order transactions retrieval failed",
|
|
2919
|
+
code: "TRANSACTIONS_RETRIEVAL_FAILED",
|
|
2920
|
+
...getOrderTransactionsResponse.error,
|
|
2921
|
+
});
|
|
2922
|
+
// The server answers null when an order has no transactions
|
|
2923
|
+
return getOrderTransactionsResponse.data ?? [];
|
|
2236
2924
|
}
|
|
2237
2925
|
// ORDER SUBMERCHANT LISTING
|
|
2238
2926
|
// Summary: Retrieve paginated list of submerchants for orders
|
|
@@ -2281,28 +2969,31 @@ class TapsilatSDK {
|
|
|
2281
2969
|
throw new TapsilatError("Checkout URL not found in order response", "CHECKOUT_URL_NOT_FOUND");
|
|
2282
2970
|
}
|
|
2283
2971
|
// WEBHOOK SIGNATURE VERIFICATION
|
|
2284
|
-
// Summary:
|
|
2285
|
-
// Description:
|
|
2972
|
+
// Summary: Verify the Tapsilat-Signature header of a callback
|
|
2973
|
+
// Description: Checks the timestamped HMAC-SHA256 that Tapsilat signs every callback with
|
|
2286
2974
|
/**
|
|
2287
|
-
* Verifies
|
|
2975
|
+
* Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
|
|
2288
2976
|
*
|
|
2289
|
-
* @summary
|
|
2290
|
-
* @description
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2295
|
-
*
|
|
2296
|
-
*
|
|
2977
|
+
* @summary Verify the Tapsilat-Signature header of a callback
|
|
2978
|
+
* @description
|
|
2979
|
+
* Tapsilat sends "t=<unix timestamp>,v1=<hex HMAC-SHA256 of '<timestamp>.<raw body>'>". The timestamp
|
|
2980
|
+
* must be within `options.tolerance` seconds (300 by default; 0 disables the check) of `options.now`
|
|
2981
|
+
* (the current time by default) so that a captured callback cannot be replayed. The comparison takes
|
|
2982
|
+
* constant time.
|
|
2983
|
+
*
|
|
2984
|
+
* A bare hex HMAC-SHA256 of the payload, with or without a "sha256=" prefix, is still accepted for
|
|
2985
|
+
* signatures made the way earlier versions of this SDK documented.
|
|
2986
|
+
*
|
|
2987
|
+
* @param payload - The raw request body as a string or Buffer, before any JSON decoding
|
|
2988
|
+
* @param signature - The Tapsilat-Signature header
|
|
2989
|
+
* @param secret - The webhook secret from the panel's callback settings
|
|
2990
|
+
* @param options - Timestamp tolerance in seconds and the current Unix time
|
|
2991
|
+
* @returns Promise resolving to true when the signature is valid, and to false when the header or
|
|
2992
|
+
* secret is empty, or the signature is malformed, stale or wrong
|
|
2993
|
+
* @throws {TapsilatValidationError} When payload is not a string or bytes, or an option is not a number
|
|
2297
2994
|
*/
|
|
2298
|
-
async verifyWebhook(payload, signature, secret) {
|
|
2299
|
-
|
|
2300
|
-
throw new TapsilatValidationError("Webhook payload is required and must be a non-empty string");
|
|
2301
|
-
if (!isNonEmptyString(signature))
|
|
2302
|
-
throw new TapsilatValidationError("Webhook signature is required and must be a non-empty string");
|
|
2303
|
-
if (!isNonEmptyString(secret))
|
|
2304
|
-
throw new TapsilatValidationError("Webhook secret is required and must be a non-empty string");
|
|
2305
|
-
return verifyHmacSignature(payload, signature, secret);
|
|
2995
|
+
async verifyWebhook(payload, signature, secret, options = {}) {
|
|
2996
|
+
return verifyWebhookSignature(payload, signature, secret, options);
|
|
2306
2997
|
}
|
|
2307
2998
|
// API HEALTH STATUS CHECK
|
|
2308
2999
|
// Summary: Check API service health and availability
|
|
@@ -2375,13 +3066,13 @@ class TapsilatSDK {
|
|
|
2375
3066
|
* across multiple payments with specific due dates and requirements.
|
|
2376
3067
|
*
|
|
2377
3068
|
* @param {OrderPaymentTermCreateDTO} termData - Payment term creation data
|
|
2378
|
-
* @param {string} termData.order_id -
|
|
2379
|
-
* @param {string} termData.term_reference_id - Unique identifier for this payment term
|
|
3069
|
+
* @param {string} termData.order_id - ID of the existing order (order_id from order creation, not its reference ID)
|
|
3070
|
+
* @param {string} [termData.term_reference_id] - Unique identifier for this payment term
|
|
2380
3071
|
* @param {number} termData.amount - Amount for this payment term
|
|
2381
|
-
* @param {string} termData.due_date - Due date for this payment (
|
|
2382
|
-
* @param {number} termData.term_sequence - Sequence number for this term
|
|
3072
|
+
* @param {string} termData.due_date - Due date for this payment (YYYY-MM-DD)
|
|
3073
|
+
* @param {number} [termData.term_sequence] - Sequence number for this term
|
|
2383
3074
|
* @param {boolean} termData.required - Whether this payment term is required
|
|
2384
|
-
* @param {string} termData.status - Initial status of the payment term
|
|
3075
|
+
* @param {string} [termData.status] - Initial status of the payment term
|
|
2385
3076
|
* @param {string} [termData.data] - Additional metadata for the term
|
|
2386
3077
|
* @param {string} [termData.paid_date] - Date when payment was completed
|
|
2387
3078
|
*
|
|
@@ -2394,9 +3085,6 @@ class TapsilatSDK {
|
|
|
2394
3085
|
if (!isNonEmptyString(termData.order_id)) {
|
|
2395
3086
|
throw new TapsilatValidationError("Order ID is required and must be a non-empty string", { provided: termData.order_id });
|
|
2396
3087
|
}
|
|
2397
|
-
if (!isNonEmptyString(termData.term_reference_id)) {
|
|
2398
|
-
throw new TapsilatValidationError("Term reference ID is required and must be a non-empty string", { provided: termData.term_reference_id });
|
|
2399
|
-
}
|
|
2400
3088
|
if (!isPositiveNumber(termData.amount)) {
|
|
2401
3089
|
throw new TapsilatValidationError("Amount must be a positive number", {
|
|
2402
3090
|
provided: termData.amount,
|
|
@@ -2408,7 +3096,8 @@ class TapsilatSDK {
|
|
|
2408
3096
|
if (!isNonEmptyString(termData.due_date)) {
|
|
2409
3097
|
throw new TapsilatValidationError("Due date is required and must be a non-empty string", { provided: termData.due_date });
|
|
2410
3098
|
}
|
|
2411
|
-
if (
|
|
3099
|
+
if (termData.term_sequence !== undefined &&
|
|
3100
|
+
!isInteger(termData.term_sequence)) {
|
|
2412
3101
|
throw new TapsilatValidationError("Term sequence must be an integer", {
|
|
2413
3102
|
provided: termData.term_sequence,
|
|
2414
3103
|
});
|
|
@@ -2418,9 +3107,6 @@ class TapsilatSDK {
|
|
|
2418
3107
|
provided: termData.required,
|
|
2419
3108
|
});
|
|
2420
3109
|
}
|
|
2421
|
-
if (!isNonEmptyString(termData.status)) {
|
|
2422
|
-
throw new TapsilatValidationError("Status is required and must be a non-empty string", { provided: termData.status });
|
|
2423
|
-
}
|
|
2424
3110
|
try {
|
|
2425
3111
|
const createTermResponse = await this.httpClient.post("/order/term", termData);
|
|
2426
3112
|
return handleResponse(createTermResponse, "Payment term creation");
|
|
@@ -2499,6 +3185,7 @@ class TapsilatSDK {
|
|
|
2499
3185
|
* Only unpaid payment terms can be deleted.
|
|
2500
3186
|
*
|
|
2501
3187
|
* @param {PaymentTermDeleteRequest} deleteData - Payment term deletion data
|
|
3188
|
+
* @param {string} deleteData.order_id - ID of the order (not its reference ID)
|
|
2502
3189
|
* @param {string} deleteData.term_reference_id - Reference ID of the term to delete
|
|
2503
3190
|
*
|
|
2504
3191
|
* @returns {Promise<PaymentTermResponse>} Promise resolving to deleted payment term details
|
|
@@ -2507,6 +3194,9 @@ class TapsilatSDK {
|
|
|
2507
3194
|
*/
|
|
2508
3195
|
async deleteOrderTerm(deleteData) {
|
|
2509
3196
|
// Validate required fields
|
|
3197
|
+
if (!isNonEmptyString(deleteData.order_id)) {
|
|
3198
|
+
throw new TapsilatValidationError("Order ID is required and must be a non-empty string", { provided: deleteData.order_id });
|
|
3199
|
+
}
|
|
2510
3200
|
if (!isNonEmptyString(deleteData.term_reference_id)) {
|
|
2511
3201
|
throw new TapsilatValidationError("Term reference ID is required and must be a non-empty string", { provided: deleteData.term_reference_id });
|
|
2512
3202
|
}
|
|
@@ -2529,7 +3219,7 @@ class TapsilatSDK {
|
|
|
2529
3219
|
* @param {OrderTermRefundRequest} refundData - Payment term refund data
|
|
2530
3220
|
* @param {string} refundData.term_id - ID of the term to refund
|
|
2531
3221
|
* @param {number} refundData.amount - Amount to refund
|
|
2532
|
-
* @param {string}
|
|
3222
|
+
* @param {string} refundData.reference_id - Reference ID of the order
|
|
2533
3223
|
* @param {string} [refundData.term_payment_id] - Optional term payment ID
|
|
2534
3224
|
*
|
|
2535
3225
|
* @returns {Promise<PaymentTermRefundResponse>} Promise resolving to refund details
|
|
@@ -2541,6 +3231,9 @@ class TapsilatSDK {
|
|
|
2541
3231
|
if (!isNonEmptyString(refundData.term_id)) {
|
|
2542
3232
|
throw new TapsilatValidationError("Term ID is required and must be a non-empty string", { provided: refundData.term_id });
|
|
2543
3233
|
}
|
|
3234
|
+
if (!isNonEmptyString(refundData.reference_id)) {
|
|
3235
|
+
throw new TapsilatValidationError("Order reference ID is required and must be a non-empty string", { provided: refundData.reference_id });
|
|
3236
|
+
}
|
|
2544
3237
|
if (!isPositiveNumber(refundData.amount)) {
|
|
2545
3238
|
throw new TapsilatValidationError("Amount must be a positive number", {
|
|
2546
3239
|
provided: refundData.amount,
|
|
@@ -2572,6 +3265,8 @@ class TapsilatSDK {
|
|
|
2572
3265
|
* @returns {Promise<PaymentTermResponse>} Promise resolving to terminated term details
|
|
2573
3266
|
* @throws {TapsilatValidationError} When input validation fails
|
|
2574
3267
|
* @throws {TapsilatError} When API returns an error response
|
|
3268
|
+
* @deprecated The API has no payment term termination route, so this call always fails.
|
|
3269
|
+
* Use deleteOrderTerm to remove a term or terminateOrder to end the whole order.
|
|
2575
3270
|
*/
|
|
2576
3271
|
async terminateOrderTerm(terminateData) {
|
|
2577
3272
|
// Validate required fields
|
|
@@ -2804,6 +3499,7 @@ class TapsilatSDK {
|
|
|
2804
3499
|
}
|
|
2805
3500
|
/**
|
|
2806
3501
|
* Retrieve limit information for a specific organization user
|
|
3502
|
+
* @deprecated The API has no organization user limit route, so this call always fails.
|
|
2807
3503
|
*/
|
|
2808
3504
|
async getOrganizationLimitUser(request) {
|
|
2809
3505
|
try {
|
|
@@ -2816,6 +3512,7 @@ class TapsilatSDK {
|
|
|
2816
3512
|
}
|
|
2817
3513
|
/**
|
|
2818
3514
|
* Set limit for a specific organization user
|
|
3515
|
+
* @deprecated The API has no organization user limit route, so this call always fails.
|
|
2819
3516
|
*/
|
|
2820
3517
|
async setOrganizationLimitUser(request) {
|
|
2821
3518
|
try {
|
|
@@ -2827,7 +3524,9 @@ class TapsilatSDK {
|
|
|
2827
3524
|
}
|
|
2828
3525
|
}
|
|
2829
3526
|
/**
|
|
2830
|
-
* Retrieve organization
|
|
3527
|
+
* Retrieve an organization's limits
|
|
3528
|
+
* @deprecated The API has no `/organization/limits` route, so this call always fails.
|
|
3529
|
+
* Use getOrganizationLimitsById.
|
|
2831
3530
|
*/
|
|
2832
3531
|
async getOrganizationLimits() {
|
|
2833
3532
|
try {
|
|
@@ -2839,15 +3538,17 @@ class TapsilatSDK {
|
|
|
2839
3538
|
}
|
|
2840
3539
|
}
|
|
2841
3540
|
/**
|
|
2842
|
-
* Retrieve meta
|
|
3541
|
+
* Retrieve an organization meta value by name
|
|
3542
|
+
*
|
|
3543
|
+
* @param name - Name of the meta value
|
|
2843
3544
|
*/
|
|
2844
3545
|
async getOrganizationMeta(name) {
|
|
2845
3546
|
if (!isNonEmptyString(name)) {
|
|
2846
|
-
throw new TapsilatValidationError("Meta name is required and must be a non-empty string");
|
|
3547
|
+
throw new TapsilatValidationError("Meta name is required and must be a non-empty string", { provided: name });
|
|
2847
3548
|
}
|
|
2848
3549
|
try {
|
|
2849
|
-
const
|
|
2850
|
-
return handleResponse(
|
|
3550
|
+
const response = await this.httpClient.get(`/organization/metas/${encodeURIComponent(name)}`);
|
|
3551
|
+
return handleResponse(response, "Get organization meta");
|
|
2851
3552
|
}
|
|
2852
3553
|
catch (error) {
|
|
2853
3554
|
return handleError(error, "get organization meta");
|
|
@@ -2866,7 +3567,7 @@ class TapsilatSDK {
|
|
|
2866
3567
|
}
|
|
2867
3568
|
}
|
|
2868
3569
|
/**
|
|
2869
|
-
* Retrieve
|
|
3570
|
+
* Retrieve a page of sub-organizations
|
|
2870
3571
|
*/
|
|
2871
3572
|
async getOrganizationSuborganizations(params = {}) {
|
|
2872
3573
|
if (params.page !== undefined) {
|
|
@@ -2935,6 +3636,271 @@ class TapsilatSDK {
|
|
|
2935
3636
|
return handleError(error, "list organization VPOs");
|
|
2936
3637
|
}
|
|
2937
3638
|
}
|
|
3639
|
+
// ORDER PAYMENTS AND CHARGING
|
|
3640
|
+
/**
|
|
3641
|
+
* Lists the payments made against an order with commission totals
|
|
3642
|
+
*
|
|
3643
|
+
* @param request - Identify the order by order_id, order_reference_id or conversation_id
|
|
3644
|
+
*/
|
|
3645
|
+
async getOrderPayments(request) {
|
|
3646
|
+
if (!isNonEmptyString(request.order_id) &&
|
|
3647
|
+
!isNonEmptyString(request.order_reference_id) &&
|
|
3648
|
+
!isNonEmptyString(request.conversation_id)) {
|
|
3649
|
+
throw new TapsilatValidationError("One of order_id, order_reference_id or conversation_id is required");
|
|
3650
|
+
}
|
|
3651
|
+
try {
|
|
3652
|
+
const response = await this.httpClient.post("/order/payments", request);
|
|
3653
|
+
return handleResponse(response, "Get order payments");
|
|
3654
|
+
}
|
|
3655
|
+
catch (error) {
|
|
3656
|
+
return handleError(error, "get order payments");
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Charges an order; inspect isSuccess, since the request can succeed while the charge fails
|
|
3661
|
+
*/
|
|
3662
|
+
async chargeOrder(request) {
|
|
3663
|
+
if (!isNonEmptyString(request.order_reference_id)) {
|
|
3664
|
+
throw new TapsilatValidationError("Order reference ID is required and must be a non-empty string", { provided: request.order_reference_id });
|
|
3665
|
+
}
|
|
3666
|
+
try {
|
|
3667
|
+
const response = await this.httpClient.post("/order/charge", request);
|
|
3668
|
+
return handleResponse(response, "Charge order");
|
|
3669
|
+
}
|
|
3670
|
+
catch (error) {
|
|
3671
|
+
return handleError(error, "charge order");
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Returns an order's payment details looked up by its conversation ID
|
|
3676
|
+
*/
|
|
3677
|
+
async getOrderPaymentDetailsByConversationId(conversationId) {
|
|
3678
|
+
if (!isNonEmptyString(conversationId)) {
|
|
3679
|
+
throw new TapsilatValidationError("Conversation ID is required and must be a non-empty string", { provided: conversationId });
|
|
3680
|
+
}
|
|
3681
|
+
try {
|
|
3682
|
+
const response = await this.httpClient.post("/order/payment-details", { conversation_id: conversationId });
|
|
3683
|
+
return handleResponse(response, "Get order payment details");
|
|
3684
|
+
}
|
|
3685
|
+
catch (error) {
|
|
3686
|
+
return handleError(error, "get order payment details");
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* Submits the stored item payout to the approval workflow
|
|
3691
|
+
* @deprecated Use marketplace.approveMarketplacePayouts with explicit payout IDs.
|
|
3692
|
+
*/
|
|
3693
|
+
async approveSubmerchantPayment(request) {
|
|
3694
|
+
try {
|
|
3695
|
+
const response = await this.httpClient.post("/submerchants/payment/approve", request);
|
|
3696
|
+
return handleResponse(response, "Approve submerchant payment");
|
|
3697
|
+
}
|
|
3698
|
+
catch (error) {
|
|
3699
|
+
return handleError(error, "approve submerchant payment");
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
/**
|
|
3703
|
+
* Withdraws a stored payout's approval; inspect status and keep operation_id
|
|
3704
|
+
*/
|
|
3705
|
+
async disapproveSubmerchantPayment(request) {
|
|
3706
|
+
try {
|
|
3707
|
+
const response = await this.httpClient.post("/submerchants/payment/disapprove", request);
|
|
3708
|
+
return handleResponse(response, "Disapprove submerchant payment");
|
|
3709
|
+
}
|
|
3710
|
+
catch (error) {
|
|
3711
|
+
return handleError(error, "disapprove submerchant payment");
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* Changes a stored payout allocation; inspect status and keep operation_id
|
|
3716
|
+
*/
|
|
3717
|
+
async updateSubmerchantPaymentItem(request) {
|
|
3718
|
+
try {
|
|
3719
|
+
const response = await this.httpClient.put("/submerchants/payment/item", request);
|
|
3720
|
+
return handleResponse(response, "Update submerchant payment item");
|
|
3721
|
+
}
|
|
3722
|
+
catch (error) {
|
|
3723
|
+
return handleError(error, "update submerchant payment item");
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
// CARD INSTALLMENTS
|
|
3727
|
+
/**
|
|
3728
|
+
* Returns installment options and commission rates for a card BIN and amount
|
|
3729
|
+
*/
|
|
3730
|
+
async getCardInstallments(request) {
|
|
3731
|
+
if (!isNonEmptyString(request.bin)) {
|
|
3732
|
+
throw new TapsilatValidationError("Card BIN is required", {
|
|
3733
|
+
provided: request.bin,
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
try {
|
|
3737
|
+
const response = await this.httpClient.get("/card/installments", { params: { ...request } });
|
|
3738
|
+
return handleResponse(response, "Get card installments");
|
|
3739
|
+
}
|
|
3740
|
+
catch (error) {
|
|
3741
|
+
return handleError(error, "get card installments");
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Returns installment options grouped by bank card program, with an interest-free flag per row
|
|
3746
|
+
*/
|
|
3747
|
+
async getInstallmentTable(request) {
|
|
3748
|
+
try {
|
|
3749
|
+
const response = await this.httpClient.get("/card/installment-table", { params: { ...request } });
|
|
3750
|
+
return handleResponse(response, "Get installment table");
|
|
3751
|
+
}
|
|
3752
|
+
catch (error) {
|
|
3753
|
+
return handleError(error, "get installment table");
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
// ELEMENTS SESSIONS
|
|
3757
|
+
// Summary: Create, read and cancel sessions for the Tapsilat Elements card sheet
|
|
3758
|
+
// Description: Your backend creates a session and hands only its client_secret to the app that opens the sheet
|
|
3759
|
+
/**
|
|
3760
|
+
* Creates a Tapsilat Elements session for the native card sheet
|
|
3761
|
+
*
|
|
3762
|
+
* @summary Create an Elements session
|
|
3763
|
+
* @description
|
|
3764
|
+
* Sends POST /elements/sessions. Hand the returned `client_secret` to your app, which opens the card
|
|
3765
|
+
* sheet with it, and do not log or store it. A `pay` session needs `order_reference_id`; a `tokenize`
|
|
3766
|
+
* session saves a card for `user_id` with the given `mode`.
|
|
3767
|
+
*
|
|
3768
|
+
* @param request - The session's purpose and its order or card owner
|
|
3769
|
+
* @returns The new session with its `client_secret` and `elements_base_url`
|
|
3770
|
+
* @throws {TapsilatValidationError} When `purpose` is missing
|
|
3771
|
+
* @throws {TapsilatError} When the API rejects the request; `apiCode` and `retryable` say why
|
|
3772
|
+
*/
|
|
3773
|
+
async createElementsSession(request) {
|
|
3774
|
+
if (!isNonEmptyString(request?.purpose)) {
|
|
3775
|
+
throw new TapsilatValidationError("Elements session purpose is required and must be a non-empty string", { provided: request?.purpose });
|
|
3776
|
+
}
|
|
3777
|
+
const body = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== undefined && value !== null));
|
|
3778
|
+
try {
|
|
3779
|
+
const response = await this.httpClient.post("/elements/sessions", body);
|
|
3780
|
+
return redactClientSecret(handleResponse(response, "Create Elements session"));
|
|
3781
|
+
}
|
|
3782
|
+
catch (error) {
|
|
3783
|
+
return handleError(error, "create Elements session");
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Returns the authoritative state and outcome of an Elements session
|
|
3788
|
+
*
|
|
3789
|
+
* @summary Get an Elements session
|
|
3790
|
+
* @description
|
|
3791
|
+
* Sends GET /elements/sessions/{id}. Read the outcome here on your backend before fulfilling an
|
|
3792
|
+
* order, rather than trusting what the app reports.
|
|
3793
|
+
*
|
|
3794
|
+
* @param id - Session ID, such as "es_test_8Kq2nV0rX4"
|
|
3795
|
+
* @throws {TapsilatError} With `apiCode` "session_not_found" and `statusCode` 404 for an unknown session
|
|
3796
|
+
*/
|
|
3797
|
+
async getElementsSession(id) {
|
|
3798
|
+
if (!isNonEmptyString(id)) {
|
|
3799
|
+
throw new TapsilatValidationError("Elements session ID is required and must be a non-empty string", { provided: id });
|
|
3800
|
+
}
|
|
3801
|
+
try {
|
|
3802
|
+
const response = await this.httpClient.get(`/elements/sessions/${encodeURIComponent(id)}`);
|
|
3803
|
+
return handleResponse(response, "Get Elements session");
|
|
3804
|
+
}
|
|
3805
|
+
catch (error) {
|
|
3806
|
+
return handleError(error, "get Elements session");
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Cancels an Elements session that is still waiting for a card
|
|
3811
|
+
*
|
|
3812
|
+
* @summary Cancel an Elements session
|
|
3813
|
+
* @description
|
|
3814
|
+
* Sends POST /elements/sessions/{id}/cancel with no body. A session that is confirming or has
|
|
3815
|
+
* completed comes back unchanged, so check `status` in the result.
|
|
3816
|
+
*
|
|
3817
|
+
* @param id - Session ID, such as "es_test_8Kq2nV0rX4"
|
|
3818
|
+
*/
|
|
3819
|
+
async cancelElementsSession(id) {
|
|
3820
|
+
if (!isNonEmptyString(id)) {
|
|
3821
|
+
throw new TapsilatValidationError("Elements session ID is required and must be a non-empty string", { provided: id });
|
|
3822
|
+
}
|
|
3823
|
+
try {
|
|
3824
|
+
const response = await this.httpClient.post(`/elements/sessions/${encodeURIComponent(id)}/cancel`);
|
|
3825
|
+
return handleResponse(response, "Cancel Elements session");
|
|
3826
|
+
}
|
|
3827
|
+
catch (error) {
|
|
3828
|
+
return handleError(error, "cancel Elements session");
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
// ORGANIZATION CURRENCIES, USERS AND SUBORGANIZATIONS
|
|
3832
|
+
async listOrganizationCurrencyPresets() {
|
|
3833
|
+
try {
|
|
3834
|
+
const response = await this.httpClient.get("/organization/currency-presets");
|
|
3835
|
+
return handleResponse(response, "List organization currency presets");
|
|
3836
|
+
}
|
|
3837
|
+
catch (error) {
|
|
3838
|
+
return handleError(error, "list organization currency presets");
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
/**
|
|
3842
|
+
* Adds a currency to the organization by ISO code, for example "USD"
|
|
3843
|
+
*/
|
|
3844
|
+
async createOrganizationCurrency(input) {
|
|
3845
|
+
const currencyCode = typeof input === "string" ? input : input?.currency_code;
|
|
3846
|
+
if (!isNonEmptyString(currencyCode)) {
|
|
3847
|
+
throw new TapsilatValidationError("Currency code is required", {
|
|
3848
|
+
provided: currencyCode,
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
try {
|
|
3852
|
+
const response = await this.httpClient.post("/organization/currencies", { currency_code: currencyCode.trim().toUpperCase() });
|
|
3853
|
+
const created = handleResponse(response, "Create organization currency");
|
|
3854
|
+
this.currencyResolver.invalidate();
|
|
3855
|
+
return created;
|
|
3856
|
+
}
|
|
3857
|
+
catch (error) {
|
|
3858
|
+
return handleError(error, "create organization currency");
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Creates an access token for an organization user; expire is in minutes
|
|
3863
|
+
*/
|
|
3864
|
+
async createOrganizationUserToken(request) {
|
|
3865
|
+
try {
|
|
3866
|
+
const response = await this.httpClient.post("/organization/user/token", request);
|
|
3867
|
+
return handleResponse(response, "Create organization user token");
|
|
3868
|
+
}
|
|
3869
|
+
catch (error) {
|
|
3870
|
+
return handleError(error, "create organization user token");
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
async getSuborganizationDetail(id) {
|
|
3874
|
+
if (!isNonEmptyString(id)) {
|
|
3875
|
+
throw new TapsilatValidationError("Suborganization ID is required and must be a non-empty string", { provided: id });
|
|
3876
|
+
}
|
|
3877
|
+
try {
|
|
3878
|
+
const response = await this.httpClient.get(`/organization/suborganizations/${encodeURIComponent(id)}`);
|
|
3879
|
+
return handleResponse(response, "Get suborganization");
|
|
3880
|
+
}
|
|
3881
|
+
catch (error) {
|
|
3882
|
+
return handleError(error, "get suborganization");
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
async getSubmerchantBySuborganization(suborganizationId) {
|
|
3886
|
+
if (!isNonEmptyString(suborganizationId)) {
|
|
3887
|
+
throw new TapsilatValidationError("Suborganization ID is required and must be a non-empty string", { provided: suborganizationId });
|
|
3888
|
+
}
|
|
3889
|
+
try {
|
|
3890
|
+
const response = await this.httpClient.get(`/organization/suborganizations/${encodeURIComponent(suborganizationId)}/submerchant`);
|
|
3891
|
+
return handleResponse(response, "Get submerchant by suborganization");
|
|
3892
|
+
}
|
|
3893
|
+
catch (error) {
|
|
3894
|
+
return handleError(error, "get submerchant by suborganization");
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Returns the organization currency UUID for a unit such as "TRY"; a UUID is returned unchanged.
|
|
3899
|
+
* Most endpoints accept units directly, so this is only needed where a UUID is required.
|
|
3900
|
+
*/
|
|
3901
|
+
async resolveCurrencyId(ref) {
|
|
3902
|
+
return this.currencyResolver.resolve(ref);
|
|
3903
|
+
}
|
|
2938
3904
|
async createSubscription(request) {
|
|
2939
3905
|
try {
|
|
2940
3906
|
const subcriptionCreateResponse = await this.httpClient.post("/subscription/create", request);
|
|
@@ -3004,25 +3970,6 @@ class TapsilatSDK {
|
|
|
3004
3970
|
// ORDER PAYMENTS
|
|
3005
3971
|
// Summary: Get payments for an order
|
|
3006
3972
|
// Description: Retrieve a list of payments made for a specific order.
|
|
3007
|
-
/**
|
|
3008
|
-
* Gets payments for an order.
|
|
3009
|
-
* Based on `get_order_payments` from the Python SDK.
|
|
3010
|
-
*
|
|
3011
|
-
* @summary Get order payments
|
|
3012
|
-
* @description Retrieve a list of payments made for a specific order.
|
|
3013
|
-
*
|
|
3014
|
-
* @param request - Order payments request containing order_id
|
|
3015
|
-
* @returns Promise resolving to order payments
|
|
3016
|
-
*/
|
|
3017
|
-
async getOrderPayments(request) {
|
|
3018
|
-
try {
|
|
3019
|
-
const response = await this.httpClient.get(`/order/${request.order_id}/payment`);
|
|
3020
|
-
return handleResponse(response, "Get order payments");
|
|
3021
|
-
}
|
|
3022
|
-
catch (error) {
|
|
3023
|
-
return handleError(error, "get order payments");
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
3973
|
// ORDER PDF
|
|
3027
3974
|
// Summary: Download order PDF
|
|
3028
3975
|
// Description: Download order as a PDF file.
|
|
@@ -3037,7 +3984,7 @@ class TapsilatSDK {
|
|
|
3037
3984
|
* @returns Promise resolving to a Blob representing the PDF
|
|
3038
3985
|
*/
|
|
3039
3986
|
async getOrderPdf(id) {
|
|
3040
|
-
const response = await this.httpClient.get(`/order/${id}/pdf`, { responseType: 'blob' });
|
|
3987
|
+
const response = await this.httpClient.get(`/order/${encodeURIComponent(id)}/export/pdf`, { responseType: 'blob' });
|
|
3041
3988
|
if (!response.success) {
|
|
3042
3989
|
handleError(response.error, "get order pdf");
|
|
3043
3990
|
}
|
|
@@ -3057,7 +4004,7 @@ class TapsilatSDK {
|
|
|
3057
4004
|
* @returns Promise resolving to a Blob representing the Excel file
|
|
3058
4005
|
*/
|
|
3059
4006
|
async getOrderExcel(id) {
|
|
3060
|
-
const response = await this.httpClient.get(`/order/${id}/excel`, { responseType: 'blob' });
|
|
4007
|
+
const response = await this.httpClient.get(`/order/${encodeURIComponent(id)}/export/excel`, { responseType: 'blob' });
|
|
3061
4008
|
if (!response.success) {
|
|
3062
4009
|
handleError(response.error, "get order excel");
|
|
3063
4010
|
}
|
|
@@ -3078,8 +4025,7 @@ class TapsilatSDK {
|
|
|
3078
4025
|
*/
|
|
3079
4026
|
async createOrderRefundRequest(request) {
|
|
3080
4027
|
try {
|
|
3081
|
-
const
|
|
3082
|
-
const response = await this.httpClient.post(`/order/${reference_id}/refund/request`, data);
|
|
4028
|
+
const response = await this.httpClient.post("/order/refund-request", request);
|
|
3083
4029
|
return handleResponse(response, "Create order refund request");
|
|
3084
4030
|
}
|
|
3085
4031
|
catch (error) {
|
|
@@ -3101,8 +4047,7 @@ class TapsilatSDK {
|
|
|
3101
4047
|
*/
|
|
3102
4048
|
async addOrderOip(request) {
|
|
3103
4049
|
try {
|
|
3104
|
-
const
|
|
3105
|
-
const response = await this.httpClient.post(`/order/${order_id}/oip`, data);
|
|
4050
|
+
const response = await this.httpClient.post("/order/oip", request);
|
|
3106
4051
|
return handleResponse(response, "Add order OIP");
|
|
3107
4052
|
}
|
|
3108
4053
|
catch (error) {
|
|
@@ -3252,22 +4197,63 @@ class TapsilatSDK {
|
|
|
3252
4197
|
// Summary: Create organization user token
|
|
3253
4198
|
// Description: Creates a token for an organization user.
|
|
3254
4199
|
/**
|
|
3255
|
-
*
|
|
3256
|
-
*
|
|
3257
|
-
*
|
|
3258
|
-
* @summary Create organization user token
|
|
3259
|
-
* @description Creates a token for an organization user.
|
|
3260
|
-
*
|
|
3261
|
-
* @param request - Token creation payload
|
|
3262
|
-
* @returns Promise resolving to the created token details
|
|
4200
|
+
* Get payments for an order
|
|
4201
|
+
* @deprecated The API has no `/orders/payments` route, so this call always fails.
|
|
4202
|
+
* Use getOrderPayments with an order ID, reference ID or conversation ID.
|
|
3263
4203
|
*/
|
|
3264
|
-
async
|
|
4204
|
+
async getAllOrdersPayments() {
|
|
3265
4205
|
try {
|
|
3266
|
-
const response = await this.httpClient.
|
|
3267
|
-
return handleResponse(response, "
|
|
4206
|
+
const response = await this.httpClient.get("/orders/payments");
|
|
4207
|
+
return handleResponse(response, "Get all orders payments");
|
|
3268
4208
|
}
|
|
3269
4209
|
catch (error) {
|
|
3270
|
-
return handleError(error, "
|
|
4210
|
+
return handleError(error, "get all orders payments");
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* Get organization partners
|
|
4215
|
+
*/
|
|
4216
|
+
async getOrganizationPartners() {
|
|
4217
|
+
try {
|
|
4218
|
+
const response = await this.httpClient.get("/organization/partners");
|
|
4219
|
+
return handleResponse(response, "Get organization partners");
|
|
4220
|
+
}
|
|
4221
|
+
catch (error) {
|
|
4222
|
+
return handleError(error, "get organization partners");
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Get organization limits by ID
|
|
4227
|
+
*/
|
|
4228
|
+
async getOrganizationLimitsById(id, params) {
|
|
4229
|
+
if (!isNonEmptyString(id)) {
|
|
4230
|
+
throw new TapsilatValidationError("ID is required and must be a non-empty string");
|
|
4231
|
+
}
|
|
4232
|
+
try {
|
|
4233
|
+
const queryParams = new URLSearchParams();
|
|
4234
|
+
if (params?.currency)
|
|
4235
|
+
queryParams.append("currency", params.currency);
|
|
4236
|
+
if (params?.operation)
|
|
4237
|
+
queryParams.append("operation", params.operation);
|
|
4238
|
+
const queryString = queryParams.toString();
|
|
4239
|
+
const url = `/organization/${encodeURIComponent(id)}/limits${queryString ? `?${queryString}` : ''}`;
|
|
4240
|
+
const response = await this.httpClient.get(url);
|
|
4241
|
+
return handleResponse(response, "Get organization limits by ID");
|
|
4242
|
+
}
|
|
4243
|
+
catch (error) {
|
|
4244
|
+
return handleError(error, "get organization limits by ID");
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* Get system config
|
|
4249
|
+
*/
|
|
4250
|
+
async getSystemConfig() {
|
|
4251
|
+
try {
|
|
4252
|
+
const response = await this.httpClient.get("/system/config");
|
|
4253
|
+
return handleResponse(response, "Get system config");
|
|
4254
|
+
}
|
|
4255
|
+
catch (error) {
|
|
4256
|
+
return handleError(error, "get system config");
|
|
3271
4257
|
}
|
|
3272
4258
|
}
|
|
3273
4259
|
}
|
|
@@ -3290,13 +4276,341 @@ exports.BusinessType = void 0;
|
|
|
3290
4276
|
BusinessType[BusinessType["UNKNOWN"] = 4] = "UNKNOWN";
|
|
3291
4277
|
})(exports.BusinessType || (exports.BusinessType = {}));
|
|
3292
4278
|
|
|
4279
|
+
/**
|
|
4280
|
+
* Request and response types for Tapsilat Elements sessions.
|
|
4281
|
+
*
|
|
4282
|
+
* Tapsilat Elements is the native Android and iOS card sheet. Your backend creates a session with
|
|
4283
|
+
* its secret API key and hands only the session's `client_secret` to the app, which opens the sheet
|
|
4284
|
+
* with it. Card data goes from the sheet to Tapsilat encrypted and never reaches your backend.
|
|
4285
|
+
*
|
|
4286
|
+
* Field names match the JSON wire format. Response fields are optional because the server omits
|
|
4287
|
+
* empty values. `purpose`, `status` and `brand` are plain strings so that values added later still
|
|
4288
|
+
* decode; compare them with the constants below.
|
|
4289
|
+
*/
|
|
4290
|
+
/**
|
|
4291
|
+
* @category Elements
|
|
4292
|
+
* @summary Purposes of an Elements session
|
|
4293
|
+
*/
|
|
4294
|
+
const ElementsSessionPurpose = Object.freeze({
|
|
4295
|
+
/** Save a card for a user without charging it */
|
|
4296
|
+
TOKENIZE: "tokenize",
|
|
4297
|
+
/** Pay an existing order */
|
|
4298
|
+
PAY: "pay",
|
|
4299
|
+
});
|
|
4300
|
+
/**
|
|
4301
|
+
* @category Elements
|
|
4302
|
+
* @summary Statuses of an Elements session
|
|
4303
|
+
*/
|
|
4304
|
+
const ElementsSessionStatus = Object.freeze({
|
|
4305
|
+
REQUIRES_PAYMENT_METHOD: "requires_payment_method",
|
|
4306
|
+
PROCESSING: "processing",
|
|
4307
|
+
REQUIRES_ACTION: "requires_action",
|
|
4308
|
+
SUCCEEDED: "succeeded",
|
|
4309
|
+
FAILED: "failed",
|
|
4310
|
+
EXPIRED: "expired",
|
|
4311
|
+
CANCELED: "canceled",
|
|
4312
|
+
});
|
|
4313
|
+
/**
|
|
4314
|
+
* @category Elements
|
|
4315
|
+
* @summary Tokenize modes of an Elements session
|
|
4316
|
+
*/
|
|
4317
|
+
const ElementsTokenizeMode = Object.freeze({
|
|
4318
|
+
/** 3D Secure */
|
|
4319
|
+
SECURE: 1,
|
|
4320
|
+
HALF_SECURE: 2,
|
|
4321
|
+
NON_SECURE: 3,
|
|
4322
|
+
});
|
|
4323
|
+
/**
|
|
4324
|
+
* @category Elements
|
|
4325
|
+
* @summary Card brands an Elements session reports
|
|
4326
|
+
*/
|
|
4327
|
+
const ElementsCardBrand = Object.freeze({
|
|
4328
|
+
VISA: "visa",
|
|
4329
|
+
MASTERCARD: "mastercard",
|
|
4330
|
+
AMEX: "amex",
|
|
4331
|
+
TROY: "troy",
|
|
4332
|
+
DISCOVER: "discover",
|
|
4333
|
+
UNIONPAY: "unionpay",
|
|
4334
|
+
UNKNOWN: "unknown",
|
|
4335
|
+
});
|
|
4336
|
+
|
|
4337
|
+
const CODES = {
|
|
4338
|
+
RECEIVED: 1,
|
|
4339
|
+
UNPAID: 2,
|
|
4340
|
+
PAID: 3,
|
|
4341
|
+
PROCESSING: 4,
|
|
4342
|
+
SHIPPED: 5,
|
|
4343
|
+
ON_HOLD: 6,
|
|
4344
|
+
WAITING_FOR_PAYMENT: 7,
|
|
4345
|
+
CANCELLED: 8,
|
|
4346
|
+
COMPLETED: 9,
|
|
4347
|
+
REFUNDED: 10,
|
|
4348
|
+
FRAUD: 11,
|
|
4349
|
+
REJECTED: 12,
|
|
4350
|
+
FAILURE: 13,
|
|
4351
|
+
RETRYING: 14,
|
|
4352
|
+
PARTIALLY_REFUNDED: 15,
|
|
4353
|
+
SUB_MERCHANT_PAYMENT_APPROVED: 16,
|
|
4354
|
+
SUB_MERCHANT_PAYMENT_DISAPPROVED: 17,
|
|
4355
|
+
SUB_MERCHANT_PAYMENT_ERRORED: 18,
|
|
4356
|
+
STILL_HAS_UNPAID_INSTALLMENTS: 19,
|
|
4357
|
+
STILL_HAS_UNPAID_TERMS: 20,
|
|
4358
|
+
EXPIRED: 21,
|
|
4359
|
+
STILL_HAS_UNPAID_SUB_MERCHANT_PAYMENTS: 22,
|
|
4360
|
+
PARTIALLY_PAID: 23,
|
|
4361
|
+
TERMINATED: 24,
|
|
4362
|
+
CARD_TOKENIZATION: 25,
|
|
4363
|
+
PRE_AUTHORIZED: 26,
|
|
4364
|
+
DISPUTED: 27,
|
|
4365
|
+
PARTIALLY_DISPUTED: 28,
|
|
4366
|
+
SUSPECT: 29,
|
|
4367
|
+
};
|
|
4368
|
+
/** The status names that GET /order/{reference_id}/status returns */
|
|
4369
|
+
const NAMES = Object.freeze({
|
|
4370
|
+
[CODES.RECEIVED]: "Received",
|
|
4371
|
+
[CODES.UNPAID]: "Unpaid",
|
|
4372
|
+
[CODES.PAID]: "Paid",
|
|
4373
|
+
[CODES.PROCESSING]: "Processing",
|
|
4374
|
+
[CODES.SHIPPED]: "Shipped",
|
|
4375
|
+
[CODES.ON_HOLD]: "On hold",
|
|
4376
|
+
[CODES.WAITING_FOR_PAYMENT]: "Waiting for payment",
|
|
4377
|
+
[CODES.CANCELLED]: "Cancelled",
|
|
4378
|
+
[CODES.COMPLETED]: "Completed",
|
|
4379
|
+
[CODES.REFUNDED]: "Refunded",
|
|
4380
|
+
[CODES.FRAUD]: "Fraud",
|
|
4381
|
+
[CODES.REJECTED]: "Rejected",
|
|
4382
|
+
[CODES.FAILURE]: "Failure",
|
|
4383
|
+
[CODES.RETRYING]: "Retrying",
|
|
4384
|
+
[CODES.PARTIALLY_REFUNDED]: "Partially refunded",
|
|
4385
|
+
[CODES.SUB_MERCHANT_PAYMENT_APPROVED]: "Sub merchant payment approved",
|
|
4386
|
+
[CODES.SUB_MERCHANT_PAYMENT_DISAPPROVED]: "Sub merchant payment disapproved",
|
|
4387
|
+
[CODES.SUB_MERCHANT_PAYMENT_ERRORED]: "Sub merchant payment errored",
|
|
4388
|
+
[CODES.STILL_HAS_UNPAID_INSTALLMENTS]: "Still has unpaid installments",
|
|
4389
|
+
[CODES.STILL_HAS_UNPAID_TERMS]: "Still has unpaid terms",
|
|
4390
|
+
[CODES.EXPIRED]: "Expired",
|
|
4391
|
+
[CODES.STILL_HAS_UNPAID_SUB_MERCHANT_PAYMENTS]: "Still has unpaid sub merchant payments",
|
|
4392
|
+
[CODES.PARTIALLY_PAID]: "Partially Paid",
|
|
4393
|
+
[CODES.TERMINATED]: "Terminated",
|
|
4394
|
+
[CODES.CARD_TOKENIZATION]: "Card Tokenization",
|
|
4395
|
+
[CODES.PRE_AUTHORIZED]: "Pre Authorized",
|
|
4396
|
+
[CODES.DISPUTED]: "Disputed",
|
|
4397
|
+
[CODES.PARTIALLY_DISPUTED]: "Partially disputed",
|
|
4398
|
+
[CODES.SUSPECT]: "Suspect",
|
|
4399
|
+
});
|
|
4400
|
+
/** Statuses of an order whose payment was captured */
|
|
4401
|
+
const PAID_STATUSES = Object.freeze([
|
|
4402
|
+
CODES.PAID,
|
|
4403
|
+
CODES.COMPLETED,
|
|
4404
|
+
CODES.SUB_MERCHANT_PAYMENT_APPROVED,
|
|
4405
|
+
]);
|
|
4406
|
+
/** Statuses of an order that will not be paid */
|
|
4407
|
+
const FAILED_STATUSES = Object.freeze([
|
|
4408
|
+
CODES.CANCELLED,
|
|
4409
|
+
CODES.FRAUD,
|
|
4410
|
+
CODES.REJECTED,
|
|
4411
|
+
CODES.FAILURE,
|
|
4412
|
+
CODES.EXPIRED,
|
|
4413
|
+
CODES.TERMINATED,
|
|
4414
|
+
]);
|
|
4415
|
+
/** Statuses of an order whose payment was refunded in full or in part */
|
|
4416
|
+
const REFUNDED_STATUSES = Object.freeze([CODES.REFUNDED, CODES.PARTIALLY_REFUNDED]);
|
|
4417
|
+
const isCode = (value) => Object.prototype.hasOwnProperty.call(NAMES, value);
|
|
4418
|
+
/**
|
|
4419
|
+
* Returns the status code for a code, a numeric string such as order_lite callbacks send, or a status
|
|
4420
|
+
* name such as "Paid" (case-insensitive), or null when the value is not a status
|
|
4421
|
+
*/
|
|
4422
|
+
const code = (status) => {
|
|
4423
|
+
if (typeof status === "number") {
|
|
4424
|
+
return Number.isInteger(status) && isCode(status) ? status : null;
|
|
4425
|
+
}
|
|
4426
|
+
if (typeof status !== "string") {
|
|
4427
|
+
return null;
|
|
4428
|
+
}
|
|
4429
|
+
const trimmed = status.trim();
|
|
4430
|
+
if (/^[0-9]+$/.test(trimmed)) {
|
|
4431
|
+
return code(Number(trimmed));
|
|
4432
|
+
}
|
|
4433
|
+
const lower = trimmed.toLowerCase();
|
|
4434
|
+
for (const [value, label] of Object.entries(NAMES)) {
|
|
4435
|
+
if (label.toLowerCase() === lower) {
|
|
4436
|
+
return Number(value);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
return null;
|
|
4440
|
+
};
|
|
4441
|
+
/** Returns the status name, such as "Paid", or null when the value is not a status */
|
|
4442
|
+
const name = (status) => {
|
|
4443
|
+
const statusCode = code(status);
|
|
4444
|
+
return statusCode === null ? null : NAMES[statusCode];
|
|
4445
|
+
};
|
|
4446
|
+
const inGroup = (group, status) => {
|
|
4447
|
+
const statusCode = code(status);
|
|
4448
|
+
return statusCode !== null && group.includes(statusCode);
|
|
4449
|
+
};
|
|
4450
|
+
/**
|
|
4451
|
+
* @category Order Management
|
|
4452
|
+
* @summary Order status codes, their names and the paid, failed and refunded groups
|
|
4453
|
+
* @description
|
|
4454
|
+
* The numeric codes that callbacks and order details carry, and the names that
|
|
4455
|
+
* GET /order/{reference_id}/status returns for them. Every helper accepts a code, a numeric string or
|
|
4456
|
+
* a name.
|
|
4457
|
+
*
|
|
4458
|
+
* @example
|
|
4459
|
+
* ```typescript
|
|
4460
|
+
* const { status } = await tapsilat.getOrderStatus(referenceId); // e.g. "Waiting for payment"
|
|
4461
|
+
* OrderStatus.isPaid(status); // Paid, Completed, Sub merchant payment approved
|
|
4462
|
+
* OrderStatus.code(status); // 7; also accepts "7" or 7
|
|
4463
|
+
* ```
|
|
4464
|
+
*/
|
|
4465
|
+
const OrderStatus = Object.freeze({
|
|
4466
|
+
...CODES,
|
|
4467
|
+
NAMES,
|
|
4468
|
+
PAID_STATUSES,
|
|
4469
|
+
FAILED_STATUSES,
|
|
4470
|
+
REFUNDED_STATUSES,
|
|
4471
|
+
/** The code for a code, a numeric string or a name (case-insensitive), or null */
|
|
4472
|
+
code,
|
|
4473
|
+
/** The status name, such as "Paid", or null */
|
|
4474
|
+
name,
|
|
4475
|
+
/** Paid, Completed or Sub merchant payment approved */
|
|
4476
|
+
isPaid: (status) => inGroup(PAID_STATUSES, status),
|
|
4477
|
+
/** Cancelled, Fraud, Rejected, Failure, Expired or Terminated */
|
|
4478
|
+
isFailed: (status) => inGroup(FAILED_STATUSES, status),
|
|
4479
|
+
/** Refunded or Partially refunded */
|
|
4480
|
+
isRefunded: (status) => inGroup(REFUNDED_STATUSES, status),
|
|
4481
|
+
});
|
|
4482
|
+
|
|
4483
|
+
const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
|
4484
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4485
|
+
const firstString = (values) => {
|
|
4486
|
+
for (const value of values) {
|
|
4487
|
+
if (typeof value === "string" && value !== "" && value !== ZERO_UUID) {
|
|
4488
|
+
return value;
|
|
4489
|
+
}
|
|
4490
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
4491
|
+
return String(value);
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
return null;
|
|
4495
|
+
};
|
|
4496
|
+
/**
|
|
4497
|
+
* @category Webhooks
|
|
4498
|
+
* @summary A callback Tapsilat posts to an organization's callback URLs
|
|
4499
|
+
* @description
|
|
4500
|
+
* Tapsilat posts {"type": "...", "data": {...}, "token": "<delivery id>"} to the organization's callback
|
|
4501
|
+
* URLs and to an order's payment_success_url and payment_failure_url. The shape of "data" depends on the
|
|
4502
|
+
* type, which each organization chooses in the panel: "order_lite" (the default), "order",
|
|
4503
|
+
* "order_payment_detail", "extended_order" or "payment_facilitator". The getters read the order's
|
|
4504
|
+
* reference ID, conversation ID and status from whichever shape arrived.
|
|
4505
|
+
*
|
|
4506
|
+
* Verify the Tapsilat-Signature header with `verifyWebhook()` before trusting a callback.
|
|
4507
|
+
*
|
|
4508
|
+
* @example
|
|
4509
|
+
* ```typescript
|
|
4510
|
+
* const event = WebhookEvent.fromPayload(rawBody);
|
|
4511
|
+
* if (event.isPaid()) {
|
|
4512
|
+
* await fulfil(event.referenceId);
|
|
4513
|
+
* }
|
|
4514
|
+
* ```
|
|
4515
|
+
*/
|
|
4516
|
+
class WebhookEvent {
|
|
4517
|
+
static TYPE_ORDER = "order";
|
|
4518
|
+
static TYPE_ORDER_LITE = "order_lite";
|
|
4519
|
+
static TYPE_ORDER_PAYMENT_DETAIL = "order_payment_detail";
|
|
4520
|
+
static TYPE_EXTENDED_ORDER = "extended_order";
|
|
4521
|
+
static TYPE_PAYMENT_FACILITATOR = "payment_facilitator";
|
|
4522
|
+
/** The callback type, such as "order_lite" */
|
|
4523
|
+
type;
|
|
4524
|
+
/** Unique per delivery */
|
|
4525
|
+
token;
|
|
4526
|
+
/** The callback's "data" object, or the whole payload when it has none */
|
|
4527
|
+
data;
|
|
4528
|
+
/** The decoded payload */
|
|
4529
|
+
payload;
|
|
4530
|
+
constructor(payload) {
|
|
4531
|
+
this.payload = payload;
|
|
4532
|
+
this.type = typeof payload.type === "string" ? payload.type : null;
|
|
4533
|
+
this.token = typeof payload.token === "string" ? payload.token : null;
|
|
4534
|
+
this.data = isObject(payload.data) ? payload.data : payload;
|
|
4535
|
+
}
|
|
4536
|
+
/**
|
|
4537
|
+
* Decodes a raw callback body
|
|
4538
|
+
*
|
|
4539
|
+
* @param payload - The raw request body
|
|
4540
|
+
* @throws {TapsilatValidationError} When the body is not a JSON object
|
|
4541
|
+
*/
|
|
4542
|
+
static fromPayload(payload) {
|
|
4543
|
+
const body = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
4544
|
+
let decoded;
|
|
4545
|
+
try {
|
|
4546
|
+
decoded = JSON.parse(body);
|
|
4547
|
+
}
|
|
4548
|
+
catch {
|
|
4549
|
+
decoded = undefined;
|
|
4550
|
+
}
|
|
4551
|
+
if (!isObject(decoded)) {
|
|
4552
|
+
throw new TapsilatValidationError("The webhook payload is not a JSON object");
|
|
4553
|
+
}
|
|
4554
|
+
return new WebhookEvent(decoded);
|
|
4555
|
+
}
|
|
4556
|
+
/** The order's reference ID */
|
|
4557
|
+
get referenceId() {
|
|
4558
|
+
const order = isObject(this.data.order) ? this.data.order : {};
|
|
4559
|
+
return firstString([
|
|
4560
|
+
this.data.orderReferenceId,
|
|
4561
|
+
this.data.order_reference_id,
|
|
4562
|
+
this.data.reference_id,
|
|
4563
|
+
order.reference_id,
|
|
4564
|
+
]);
|
|
4565
|
+
}
|
|
4566
|
+
/** The conversation ID the order was created with */
|
|
4567
|
+
get conversationId() {
|
|
4568
|
+
return firstString([this.data.conversationId, this.data.conversation_id]);
|
|
4569
|
+
}
|
|
4570
|
+
/** The order status code (see OrderStatus), or null when this callback type carries none */
|
|
4571
|
+
get statusCode() {
|
|
4572
|
+
const code = OrderStatus.code(this.data.status);
|
|
4573
|
+
if (code === null && isObject(this.data.order)) {
|
|
4574
|
+
return OrderStatus.code(this.data.order.status);
|
|
4575
|
+
}
|
|
4576
|
+
return code;
|
|
4577
|
+
}
|
|
4578
|
+
/** The order status name, such as "Paid" */
|
|
4579
|
+
get status() {
|
|
4580
|
+
return OrderStatus.name(this.statusCode);
|
|
4581
|
+
}
|
|
4582
|
+
isPaid() {
|
|
4583
|
+
return OrderStatus.isPaid(this.statusCode);
|
|
4584
|
+
}
|
|
4585
|
+
isFailed() {
|
|
4586
|
+
return OrderStatus.isFailed(this.statusCode);
|
|
4587
|
+
}
|
|
4588
|
+
isRefunded() {
|
|
4589
|
+
return OrderStatus.isRefunded(this.statusCode);
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
|
|
3293
4593
|
exports.ConfigManager = ConfigManager;
|
|
4594
|
+
exports.CurrencyResolver = CurrencyResolver;
|
|
4595
|
+
exports.ElementsCardBrand = ElementsCardBrand;
|
|
4596
|
+
exports.ElementsSessionPurpose = ElementsSessionPurpose;
|
|
4597
|
+
exports.ElementsSessionStatus = ElementsSessionStatus;
|
|
4598
|
+
exports.ElementsTokenizeMode = ElementsTokenizeMode;
|
|
4599
|
+
exports.MarketplaceService = MarketplaceService;
|
|
4600
|
+
exports.OrderStatus = OrderStatus;
|
|
4601
|
+
exports.SubmerchantService = SubmerchantService;
|
|
3294
4602
|
exports.TapsilatAuthenticationError = TapsilatAuthenticationError;
|
|
3295
4603
|
exports.TapsilatError = TapsilatError;
|
|
3296
4604
|
exports.TapsilatNetworkError = TapsilatNetworkError;
|
|
3297
4605
|
exports.TapsilatRateLimitError = TapsilatRateLimitError;
|
|
3298
4606
|
exports.TapsilatSDK = TapsilatSDK;
|
|
3299
4607
|
exports.TapsilatValidationError = TapsilatValidationError;
|
|
4608
|
+
exports.TokenizationService = TokenizationService;
|
|
4609
|
+
exports.VposService = VposService;
|
|
4610
|
+
exports.WEBHOOK_SIGNATURE_HEADER = WEBHOOK_SIGNATURE_HEADER;
|
|
4611
|
+
exports.WEBHOOK_TOLERANCE = WEBHOOK_TOLERANCE;
|
|
4612
|
+
exports.WebhookEvent = WebhookEvent;
|
|
4613
|
+
exports.cleanGsmNumber = cleanGsmNumber;
|
|
3300
4614
|
exports.isValidCurrency = isValidCurrency;
|
|
3301
4615
|
exports.isValidPaymentMethod = isValidPaymentMethod;
|
|
3302
4616
|
exports.isValidUrl = isValidUrl;
|