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