@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.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
- return new TapsilatError(apiError.message, apiError.code, apiError.details);
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
- const isJson = contentType?.includes("application/json");
440
- let responseData;
441
- try {
442
- if (responseType === "blob") {
443
- responseData = await response.blob();
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
- else if (responseType === "arraybuffer") {
446
- responseData = await response.arrayBuffer();
463
+ catch {
464
+ throw new TapsilatError("Failed to read response body", "PARSE_ERROR");
447
465
  }
448
- else if (responseType === "text") {
449
- responseData = await response.text();
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 (isJson || responseType === "json") {
452
- responseData = await response.json();
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 = await response.text();
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
- return (obj.message || obj.error || obj.detail);
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 || !response.data) {
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: Security verification of webhook payloads using HMAC-SHA256
1361
- // Description: Computes expected signature from payload and secret, compares with provided signature
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 HMAC-SHA256 signature for webhook security
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
- * @summary Security verification of webhook payloads using HMAC-SHA256
1366
- * @description Computes expected signature from payload and secret, compares with provided signature
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 - Raw webhook payload string
1369
- * @param signature - Webhook signature from headers (should include "sha256=" prefix)
1370
- * @param secret - Your webhook secret key
1371
- * @returns true if signature is valid, false otherwise
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 verifyHmacSignature = (payload, signature, secret) => {
1374
- const expectedSignature = crypto
1375
- .createHmac("sha256", secret)
1376
- .update(payload)
1377
- .digest("hex");
1378
- return `sha256=${expectedSignature}` === signature;
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.getOrganizationLimits(),
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", orderRequest);
2331
+ const createOrderResponse = await this.httpClient.post("/order/create", request);
1653
2332
  // Use our generic response handler
1654
- return handleResponse(createOrderResponse, "Order creation");
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
- try {
2222
- const getOrderTransactionsResponse = await this.httpClient.get(`/order/${referenceId}/transactions`);
2223
- if (!getOrderTransactionsResponse.success)
2224
- throw new TapsilatError(getOrderTransactionsResponse.error?.message ||
2225
- "Order transactions retrieval failed", getOrderTransactionsResponse.error?.code ||
2226
- "TRANSACTIONS_RETRIEVAL_FAILED");
2227
- if (!getOrderTransactionsResponse.data)
2228
- throw new TapsilatError("Order transactions response data is missing", "TRANSACTIONS_DATA_MISSING");
2229
- return getOrderTransactionsResponse.data;
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: Validate webhook signature for security verification
2283
- // Description: Verifies the authenticity of webhook payloads using HMAC signature validation
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 webhook signature for security
2973
+ * Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
2286
2974
  *
2287
- * @summary Validate webhook signature for security verification
2288
- * @description Verifies the authenticity of webhook payloads using HMAC signature validation.
2289
- *
2290
- * @param payload - Raw webhook payload string
2291
- * @param signature - Webhook signature from headers
2292
- * @param secret - Your webhook secret key
2293
- * @returns Promise resolving to true if signature is valid
2294
- * @throws {TapsilatValidationError} When input validation fails for payload, signature or secret
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
- if (!isNonEmptyString(payload))
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 - Reference ID of the existing order
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 (ISO 8601 format)
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 (!isInteger(termData.term_sequence)) {
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} [refundData.reference_id] - Optional reference ID
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 overall limits
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 information for the organization
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 organizationMetaResponse = await this.httpClient.get(`/organization/metas/${name}`);
2848
- return handleResponse(organizationMetaResponse, "Get organization meta");
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 list of sub-organizations
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 { reference_id, ...data } = request;
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 { order_id, ...data } = request;
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
- * Creates an organization user token.
3254
- * Based on `create_organization_user_token` from the Python SDK.
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 createOrganizationUserToken(request) {
4202
+ async getAllOrdersPayments() {
3263
4203
  try {
3264
- const response = await this.httpClient.post(`/organization/user/token`, request);
3265
- return handleResponse(response, "Create organization user token");
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, "create organization user token");
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
- export { BusinessType, ConfigManager, TapsilatAuthenticationError, TapsilatError, TapsilatNetworkError, TapsilatRateLimitError, TapsilatSDK, TapsilatValidationError, isValidCurrency, isValidPaymentMethod, isValidUrl, sanitizeMetadata, validateEmail, validateGsmNumber, validateInstallments, validatePaymentRequest };
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