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