@tapsilat/tapsilat-js 2026.10.20 → 2026.10.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ class TapsilatError extends Error {
18
18
  apiStatus;
19
19
  /** Per-field validation messages from the API response body */
20
20
  errors;
21
+ /** Whether the API error says the request can be retried, as Elements errors do */
22
+ retryable;
21
23
  /** Raw API response body */
22
24
  rawBody;
23
25
  /**
@@ -53,6 +55,7 @@ class TapsilatError extends Error {
53
55
  error.apiCode = apiError.apiCode;
54
56
  error.apiStatus = apiError.apiStatus;
55
57
  error.errors = apiError.errors;
58
+ error.retryable = apiError.retryable;
56
59
  error.rawBody = apiError.rawBody;
57
60
  return error;
58
61
  }
@@ -519,6 +522,8 @@ class HttpClient {
519
522
  const errors = body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)
520
523
  ? body.errors
521
524
  : undefined;
525
+ const nested = this.nestedError(body);
526
+ const apiCode = body.code !== undefined && body.code !== null ? body.code : nested?.code;
522
527
  return {
523
528
  success: false,
524
529
  status: response.status,
@@ -527,13 +532,14 @@ class HttpClient {
527
532
  code: error.code,
528
533
  details: error.details,
529
534
  statusCode: response.status,
530
- apiCode: body.code !== undefined && body.code !== null
531
- ? String(body.code)
535
+ apiCode: apiCode !== undefined && apiCode !== null
536
+ ? String(apiCode)
532
537
  : undefined,
533
538
  apiStatus: body.status !== undefined && body.status !== null
534
539
  ? String(body.status)
535
540
  : undefined,
536
541
  errors,
542
+ retryable: typeof nested?.retryable === "boolean" ? nested.retryable : undefined,
537
543
  rawBody,
538
544
  },
539
545
  };
@@ -591,10 +597,28 @@ class HttpClient {
591
597
  }
592
598
  if (data && typeof data === "object") {
593
599
  const obj = data;
594
- return (obj.message || obj.error || obj.detail);
600
+ const nested = this.nestedError(obj);
601
+ return (obj.message || (nested ? nested.message : obj.error) || obj.detail);
595
602
  }
596
603
  return undefined;
597
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
+ }
598
622
  /**
599
623
  * @category HTTP Client Internals
600
624
  * @summary Extracts validation details from error response
@@ -1881,30 +1905,129 @@ class CurrencyResolver {
1881
1905
  }
1882
1906
  }
1883
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
+ };
1884
1962
  // WEBHOOK SIGNATURE VERIFICATION
1885
- // Summary: Security verification of webhook payloads using HMAC-SHA256
1886
- // 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
1887
1965
  /**
1888
- * Verifies HMAC-SHA256 signature for webhook security
1966
+ * Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
1889
1967
  *
1890
- * @summary Security verification of webhook payloads using HMAC-SHA256
1891
- * @description Computes expected signature from payload and secret, compares with provided signature
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.
1973
+ *
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.
1892
1976
  *
1893
- * @param payload - Raw webhook payload string
1894
- * @param signature - Webhook signature from headers (should include "sha256=" prefix)
1895
- * @param secret - Your webhook secret key
1896
- * @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
1897
1983
  */
1898
- const verifyHmacSignature = (payload, signature, secret) => {
1899
- const expectedSignature = crypto
1900
- .createHmac("sha256", secret)
1901
- .update(payload)
1902
- .digest("hex");
1903
- const expected = Buffer.from(`sha256=${expectedSignature}`);
1904
- const provided = Buffer.from(signature ?? "");
1905
- // Compare in constant time so the signature cannot be guessed byte by byte
1906
- return (expected.length === provided.length &&
1907
- crypto.timingSafeEqual(expected, provided));
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;
1908
2031
  };
1909
2032
 
1910
2033
  /**
@@ -2101,7 +2224,7 @@ class TapsilatSDK {
2101
2224
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2102
2225
  get webhooks() {
2103
2226
  return {
2104
- verify: (payload, signature, secret) => this.verifyWebhook(payload, signature, secret),
2227
+ verify: (payload, signature, secret, options) => this.verifyWebhook(payload, signature, secret, options),
2105
2228
  };
2106
2229
  }
2107
2230
  // ORDER CREATION
@@ -2844,28 +2967,31 @@ class TapsilatSDK {
2844
2967
  throw new TapsilatError("Checkout URL not found in order response", "CHECKOUT_URL_NOT_FOUND");
2845
2968
  }
2846
2969
  // WEBHOOK SIGNATURE VERIFICATION
2847
- // Summary: Validate webhook signature for security verification
2848
- // 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
2849
2972
  /**
2850
- * Verifies webhook signature for security
2973
+ * Verifies the Tapsilat-Signature header of a callback against the organization's webhook secret
2851
2974
  *
2852
- * @summary Validate webhook signature for security verification
2853
- * @description Verifies the authenticity of webhook payloads using HMAC signature validation.
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.
2854
2984
  *
2855
- * @param payload - Raw webhook payload string
2856
- * @param signature - Webhook signature from headers
2857
- * @param secret - Your webhook secret key
2858
- * @returns Promise resolving to true if signature is valid
2859
- * @throws {TapsilatValidationError} When input validation fails for payload, signature or secret
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
2860
2992
  */
2861
- async verifyWebhook(payload, signature, secret) {
2862
- if (!isNonEmptyString(payload))
2863
- throw new TapsilatValidationError("Webhook payload is required and must be a non-empty string");
2864
- if (!isNonEmptyString(signature))
2865
- throw new TapsilatValidationError("Webhook signature is required and must be a non-empty string");
2866
- if (!isNonEmptyString(secret))
2867
- throw new TapsilatValidationError("Webhook secret is required and must be a non-empty string");
2868
- return verifyHmacSignature(payload, signature, secret);
2993
+ async verifyWebhook(payload, signature, secret, options = {}) {
2994
+ return verifyWebhookSignature(payload, signature, secret, options);
2869
2995
  }
2870
2996
  // API HEALTH STATUS CHECK
2871
2997
  // Summary: Check API service health and availability
@@ -3625,6 +3751,81 @@ class TapsilatSDK {
3625
3751
  return handleError(error, "get installment table");
3626
3752
  }
3627
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
+ }
3628
3829
  // ORGANIZATION CURRENCIES, USERS AND SUBORGANIZATIONS
3629
3830
  async listOrganizationCurrencyPresets() {
3630
3831
  try {
@@ -4073,5 +4274,319 @@ var BusinessType;
4073
4274
  BusinessType[BusinessType["UNKNOWN"] = 4] = "UNKNOWN";
4074
4275
  })(BusinessType || (BusinessType = {}));
4075
4276
 
4076
- export { BusinessType, ConfigManager, CurrencyResolver, MarketplaceService, SubmerchantService, TapsilatAuthenticationError, TapsilatError, TapsilatNetworkError, TapsilatRateLimitError, TapsilatSDK, TapsilatValidationError, TokenizationService, VposService, cleanGsmNumber, 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 };
4077
4592
  //# sourceMappingURL=index.js.map