@tapsilat/tapsilat-js 1.0.2

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 ADDED
@@ -0,0 +1,1131 @@
1
+ 'use strict';
2
+
3
+ class TapsilatError extends Error {
4
+ code;
5
+ details;
6
+ constructor(message, code, details) {
7
+ super(message);
8
+ this.name = 'TapsilatError';
9
+ this.code = code;
10
+ this.details = details;
11
+ // Maintain proper stack trace for where our error was thrown
12
+ if (Error.captureStackTrace) {
13
+ Error.captureStackTrace(this, TapsilatError);
14
+ }
15
+ }
16
+ static fromAPIError(apiError) {
17
+ return new TapsilatError(apiError.message, apiError.code, apiError.details);
18
+ }
19
+ }
20
+ class TapsilatNetworkError extends TapsilatError {
21
+ statusCode;
22
+ constructor(message, code, statusCode, details) {
23
+ super(message, code, details);
24
+ this.name = 'TapsilatNetworkError';
25
+ this.statusCode = statusCode;
26
+ }
27
+ }
28
+ class TapsilatValidationError extends TapsilatError {
29
+ constructor(message, details) {
30
+ super(message, 'VALIDATION_ERROR', details);
31
+ this.name = 'TapsilatValidationError';
32
+ }
33
+ }
34
+ class TapsilatAuthenticationError extends TapsilatError {
35
+ constructor(message = 'Authentication failed') {
36
+ super(message, 'AUTHENTICATION_ERROR');
37
+ this.name = 'TapsilatAuthenticationError';
38
+ }
39
+ }
40
+ class TapsilatRateLimitError extends TapsilatError {
41
+ retryAfter;
42
+ constructor(message = 'Rate limit exceeded', retryAfter) {
43
+ super(message, 'RATE_LIMIT_ERROR');
44
+ this.name = 'TapsilatRateLimitError';
45
+ this.retryAfter = retryAfter;
46
+ }
47
+ }
48
+
49
+ class InterceptorManager {
50
+ requestInterceptors = [];
51
+ responseInterceptors = [];
52
+ errorInterceptors = [];
53
+ // Request interceptors
54
+ addRequestInterceptor(interceptor) {
55
+ this.requestInterceptors.push(interceptor);
56
+ return this.requestInterceptors.length - 1;
57
+ }
58
+ removeRequestInterceptor(id) {
59
+ if (this.requestInterceptors[id]) {
60
+ delete this.requestInterceptors[id];
61
+ }
62
+ }
63
+ async executeRequestInterceptors(url, options) {
64
+ let currentUrl = url;
65
+ let currentOptions = options;
66
+ for (const interceptor of this.requestInterceptors) {
67
+ if (interceptor) {
68
+ const result = await interceptor(currentUrl, currentOptions);
69
+ currentUrl = result.url;
70
+ currentOptions = result.options;
71
+ }
72
+ }
73
+ return { url: currentUrl, options: currentOptions };
74
+ }
75
+ // Response interceptors
76
+ addResponseInterceptor(interceptor) {
77
+ this.responseInterceptors.push(interceptor);
78
+ return this.responseInterceptors.length - 1;
79
+ }
80
+ removeResponseInterceptor(id) {
81
+ if (this.responseInterceptors[id]) {
82
+ delete this.responseInterceptors[id];
83
+ }
84
+ }
85
+ async executeResponseInterceptors(response, request) {
86
+ let currentResponse = response;
87
+ for (const interceptor of this.responseInterceptors) {
88
+ if (interceptor) {
89
+ currentResponse = await interceptor(currentResponse, request);
90
+ }
91
+ }
92
+ return currentResponse;
93
+ }
94
+ // Error interceptors
95
+ addErrorInterceptor(interceptor) {
96
+ this.errorInterceptors.push(interceptor);
97
+ return this.errorInterceptors.length - 1;
98
+ }
99
+ removeErrorInterceptor(id) {
100
+ if (this.errorInterceptors[id]) {
101
+ delete this.errorInterceptors[id];
102
+ }
103
+ }
104
+ async executeErrorInterceptors(error, request) {
105
+ for (const interceptor of this.errorInterceptors) {
106
+ if (interceptor) {
107
+ try {
108
+ return await interceptor(error, request);
109
+ }
110
+ catch (interceptorError) {
111
+ // Continue to next interceptor if this one fails
112
+ continue;
113
+ }
114
+ }
115
+ }
116
+ // If no interceptor handled the error, re-throw
117
+ throw error;
118
+ }
119
+ // Clear all interceptors
120
+ clear() {
121
+ this.requestInterceptors = [];
122
+ this.responseInterceptors = [];
123
+ this.errorInterceptors = [];
124
+ }
125
+ }
126
+
127
+ /**
128
+ * HTTP Client for making API requests with comprehensive error handling,
129
+ * retry logic, and request/response interceptors
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const client = new HttpClient({
134
+ * apiKey: 'your-api-key',
135
+ * baseURL: 'https://api.example.com/v1'
136
+ * });
137
+ *
138
+ * const data = await client.get<UserResponse>('/users/123');
139
+ * ```
140
+ */
141
+ class HttpClient {
142
+ config;
143
+ interceptors = new InterceptorManager();
144
+ constructor(config) {
145
+ this.config = config;
146
+ }
147
+ async get(url, config) {
148
+ return this.request("GET", url, undefined, config);
149
+ }
150
+ async post(url, body, config) {
151
+ return this.request("POST", url, body, config);
152
+ }
153
+ async put(url, body, config) {
154
+ return this.request("PUT", url, body, config);
155
+ }
156
+ async patch(url, body, config) {
157
+ return this.request("PATCH", url, body, config);
158
+ }
159
+ async delete(url, config) {
160
+ return this.request("DELETE", url, undefined, config);
161
+ }
162
+ /**
163
+ * Core request method with comprehensive error handling and retry logic
164
+ *
165
+ * @param method - HTTP method to use
166
+ * @param url - Request URL (relative to baseURL)
167
+ * @param body - Request body data
168
+ * @param config - Additional request configuration
169
+ * @returns Promise resolving to API response
170
+ */
171
+ async request(method, url, body, config) {
172
+ const fullUrl = this.buildUrl(url, config?.baseURL, config?.params);
173
+ const requestOptions = await this.buildRequestOptions(method, body, config);
174
+ const maxRetries = config?.retries ?? this.config.maxRetries ?? 3;
175
+ // Execute request interceptors
176
+ const { url: interceptedUrl, options: interceptedOptions } = await this.interceptors.executeRequestInterceptors(fullUrl, requestOptions);
177
+ let lastError = null;
178
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
179
+ try {
180
+ const response = await this.makeRequest(interceptedUrl, interceptedOptions, config?.timeout);
181
+ // Execute response interceptors
182
+ const interceptedResponse = await this.interceptors.executeResponseInterceptors(response, {
183
+ url: interceptedUrl,
184
+ options: interceptedOptions,
185
+ });
186
+ return interceptedResponse;
187
+ }
188
+ catch (error) {
189
+ lastError = error;
190
+ // Don't retry on certain error types
191
+ if (this.shouldNotRetry(lastError)) {
192
+ break;
193
+ }
194
+ // Don't retry on the last attempt
195
+ if (attempt === maxRetries) {
196
+ break;
197
+ }
198
+ // Wait before retry with exponential backoff
199
+ await this.delay(this.calculateRetryDelay(attempt));
200
+ }
201
+ }
202
+ // Execute error interceptors as last resort
203
+ try {
204
+ return await this.interceptors.executeErrorInterceptors(lastError, {
205
+ url: interceptedUrl,
206
+ options: interceptedOptions,
207
+ });
208
+ }
209
+ catch (finalError) {
210
+ throw finalError;
211
+ }
212
+ }
213
+ /**
214
+ * Makes the actual HTTP request with timeout handling
215
+ */
216
+ async makeRequest(url, options, timeout) {
217
+ const controller = new AbortController();
218
+ const timeoutId = timeout
219
+ ? setTimeout(() => controller.abort(), timeout)
220
+ : null;
221
+ try {
222
+ const requestOptions = {
223
+ ...options,
224
+ signal: controller.signal,
225
+ };
226
+ const response = await fetch(url, requestOptions);
227
+ if (timeoutId) {
228
+ clearTimeout(timeoutId);
229
+ }
230
+ return await this.processResponse(response);
231
+ }
232
+ catch (error) {
233
+ if (timeoutId) {
234
+ clearTimeout(timeoutId);
235
+ }
236
+ if (error instanceof Error) {
237
+ if (error.name === "AbortError") {
238
+ throw new TapsilatNetworkError("Request timeout", "TIMEOUT");
239
+ }
240
+ throw new TapsilatNetworkError(`Network request failed: ${error.message}`, "NETWORK_ERROR");
241
+ }
242
+ throw new TapsilatNetworkError("Unknown network error", "UNKNOWN");
243
+ }
244
+ }
245
+ /**
246
+ * Processes the fetch response and converts to APIResponse format
247
+ */
248
+ async processResponse(response) {
249
+ const contentType = response.headers.get("content-type");
250
+ const isJson = contentType?.includes("application/json");
251
+ let responseData;
252
+ try {
253
+ if (isJson) {
254
+ responseData = await response.json();
255
+ }
256
+ else {
257
+ responseData = await response.text();
258
+ }
259
+ }
260
+ catch (parseError) {
261
+ throw new TapsilatError("Failed to parse response body", "PARSE_ERROR");
262
+ }
263
+ // Handle successful responses
264
+ if (response.ok) {
265
+ return {
266
+ success: true,
267
+ data: responseData,
268
+ };
269
+ }
270
+ // Handle error responses
271
+ const error = this.createErrorFromResponse(response, responseData);
272
+ return {
273
+ success: false,
274
+ error: {
275
+ message: error.message,
276
+ code: error.code,
277
+ details: error.details,
278
+ },
279
+ };
280
+ }
281
+ /**
282
+ * Creates appropriate error based on response status and data
283
+ */
284
+ createErrorFromResponse(response, data) {
285
+ const status = response.status;
286
+ const errorMessage = this.extractErrorMessage(data);
287
+ switch (status) {
288
+ case 401:
289
+ return new TapsilatAuthenticationError(errorMessage || "Authentication failed - invalid API key");
290
+ case 403:
291
+ return new TapsilatAuthenticationError(errorMessage || "Access forbidden - insufficient permissions");
292
+ case 400:
293
+ return new TapsilatValidationError(errorMessage || "Invalid request data");
294
+ case 422:
295
+ return new TapsilatValidationError(errorMessage || "Validation failed", this.extractValidationDetails(data));
296
+ case 429:
297
+ const rateLimitInfo = this.extractRateLimitInfo(response);
298
+ return new TapsilatRateLimitError(errorMessage || "Rate limit exceeded", rateLimitInfo.resetTime
299
+ ? Math.floor((rateLimitInfo.resetTime.getTime() - Date.now()) / 1000)
300
+ : undefined);
301
+ case 404:
302
+ return new TapsilatError(errorMessage || "Resource not found", "NOT_FOUND");
303
+ case 500:
304
+ return new TapsilatError(errorMessage || "Internal server error", "INTERNAL_ERROR");
305
+ case 502:
306
+ case 503:
307
+ case 504:
308
+ return new TapsilatNetworkError(errorMessage || "Service temporarily unavailable", "SERVICE_UNAVAILABLE", status);
309
+ default:
310
+ return new TapsilatError(errorMessage || `HTTP ${status} error`, "HTTP_ERROR");
311
+ }
312
+ }
313
+ /**
314
+ * Extracts error message from response data
315
+ */
316
+ extractErrorMessage(data) {
317
+ if (typeof data === "string") {
318
+ return data;
319
+ }
320
+ if (data && typeof data === "object") {
321
+ const obj = data;
322
+ return (obj.message || obj.error || obj.detail);
323
+ }
324
+ return undefined;
325
+ }
326
+ /**
327
+ * Extracts validation details from error response
328
+ */
329
+ extractValidationDetails(data) {
330
+ if (data && typeof data === "object") {
331
+ const obj = data;
332
+ return obj.errors;
333
+ }
334
+ return undefined;
335
+ }
336
+ /**
337
+ * Extracts rate limit information from response headers
338
+ */
339
+ extractRateLimitInfo(response) {
340
+ const limit = response.headers.get("x-ratelimit-limit");
341
+ const remaining = response.headers.get("x-ratelimit-remaining");
342
+ const reset = response.headers.get("x-ratelimit-reset");
343
+ return {
344
+ limit: limit ? parseInt(limit, 10) : undefined,
345
+ remaining: remaining ? parseInt(remaining, 10) : undefined,
346
+ resetTime: reset ? new Date(parseInt(reset, 10) * 1000) : undefined,
347
+ };
348
+ }
349
+ /**
350
+ * Builds the complete URL for the request
351
+ */
352
+ buildUrl(url, customBaseURL, params) {
353
+ const baseURL = customBaseURL || this.config.baseURL || "https://api.tapsilat.com/v1";
354
+ if (url.startsWith("http://") || url.startsWith("https://")) {
355
+ return url;
356
+ }
357
+ const base = baseURL.replace(/\/+$/, "");
358
+ const path = url.replace(/^\/+/, "");
359
+ let fullUrl = `${base}/${path}`;
360
+ // Add query parameters if provided
361
+ if (params && Object.keys(params).length > 0) {
362
+ const searchParams = new URLSearchParams();
363
+ for (const [key, value] of Object.entries(params)) {
364
+ if (value !== undefined && value !== null && value !== "") {
365
+ searchParams.append(key, String(value));
366
+ }
367
+ }
368
+ const queryString = searchParams.toString();
369
+ if (queryString) {
370
+ fullUrl += `?${queryString}`;
371
+ }
372
+ }
373
+ return fullUrl;
374
+ }
375
+ /**
376
+ * Builds RequestInit options for the fetch request
377
+ */
378
+ async buildRequestOptions(method, body, config) {
379
+ const headers = new Headers({
380
+ "User-Agent": `TapsilatSDK/1.0.0`,
381
+ Authorization: `Bearer ${this.config.bearerToken}`,
382
+ Accept: "application/json",
383
+ ...config?.headers,
384
+ });
385
+ // Set content type for requests with body
386
+ if (body && method !== "GET" && method !== "DELETE") {
387
+ if (!(body instanceof FormData)) {
388
+ headers.set("Content-Type", "application/json");
389
+ }
390
+ }
391
+ const options = {
392
+ method,
393
+ headers,
394
+ ...config,
395
+ };
396
+ // Add body for non-GET/DELETE requests
397
+ if (body && method !== "GET" && method !== "DELETE") {
398
+ if (body instanceof FormData) {
399
+ options.body = body;
400
+ }
401
+ else if (typeof body === "string") {
402
+ options.body = body;
403
+ }
404
+ else {
405
+ options.body = JSON.stringify(body);
406
+ }
407
+ }
408
+ return options;
409
+ }
410
+ /**
411
+ * Determines if an error should prevent retrying
412
+ */
413
+ shouldNotRetry(error) {
414
+ // Don't retry authentication, validation, or client errors
415
+ return (error instanceof TapsilatAuthenticationError ||
416
+ error instanceof TapsilatValidationError ||
417
+ (error instanceof TapsilatError && error.code === "NOT_FOUND"));
418
+ }
419
+ /**
420
+ * Calculates retry delay with exponential backoff
421
+ */
422
+ calculateRetryDelay(attempt) {
423
+ const baseDelay = 1000; // 1 second
424
+ const maxDelay = 30000; // 30 seconds
425
+ const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
426
+ // Add jitter to prevent thundering herd
427
+ return delay + Math.random() * 1000;
428
+ }
429
+ /**
430
+ * Utility method to create delay
431
+ */
432
+ delay(ms) {
433
+ return new Promise((resolve) => setTimeout(resolve, ms));
434
+ }
435
+ // Interceptor management methods
436
+ /**
437
+ * Gets the interceptor manager for advanced request/response handling
438
+ *
439
+ * @returns InterceptorManager instance
440
+ */
441
+ getInterceptors() {
442
+ return this.interceptors;
443
+ }
444
+ /**
445
+ * Adds a request interceptor
446
+ *
447
+ * @param interceptor - Request interceptor function
448
+ * @returns Interceptor ID for removal
449
+ */
450
+ addRequestInterceptor(interceptor) {
451
+ return this.interceptors.addRequestInterceptor(interceptor);
452
+ }
453
+ /**
454
+ * Adds a response interceptor
455
+ *
456
+ * @param interceptor - Response interceptor function
457
+ * @returns Interceptor ID for removal
458
+ */
459
+ addResponseInterceptor(interceptor) {
460
+ return this.interceptors.addResponseInterceptor(interceptor);
461
+ }
462
+ /**
463
+ * Adds an error interceptor
464
+ *
465
+ * @param interceptor - Error interceptor function
466
+ * @returns Interceptor ID for removal
467
+ */
468
+ addErrorInterceptor(interceptor) {
469
+ return this.interceptors.addErrorInterceptor(interceptor);
470
+ }
471
+ }
472
+
473
+ // Type Guards
474
+ /**
475
+ * Type guard to check if a value is a valid PaymentMethod
476
+ */
477
+ function isPaymentMethod(value) {
478
+ return (typeof value === "string" &&
479
+ ["credit_card", "debit_card", "bank_transfer", "digital_wallet"].includes(value));
480
+ }
481
+ /**
482
+ * Type guard to check if a value is a valid Currency
483
+ */
484
+ function isCurrency(value) {
485
+ return (typeof value === "string" && ["TRY", "USD", "EUR", "GBP"].includes(value));
486
+ }
487
+ /**
488
+ * Type guard to check if a value is a non-empty string
489
+ */
490
+ function isNonEmptyString(value) {
491
+ return typeof value === "string" && value.trim().length > 0;
492
+ }
493
+ /**
494
+ * Type guard to check if a value is a positive number
495
+ */
496
+ function isPositiveNumber(value) {
497
+ return (typeof value === "number" && value > 0 && !isNaN(value) && isFinite(value));
498
+ }
499
+ /**
500
+ * Type guard to check if amount has max 2 decimal places
501
+ */
502
+ function hasValidDecimalPlaces(amount) {
503
+ const decimalPlaces = (amount.toString().split(".")[1] || "").length;
504
+ return decimalPlaces <= 2;
505
+ }
506
+ // Validation Functions
507
+ /**
508
+ * Validates Bearer token format and strength
509
+ *
510
+ * @param bearerToken - Bearer token to validate
511
+ * @throws {TapsilatValidationError} When Bearer token is invalid
512
+ */
513
+ function validateBearerToken(bearerToken) {
514
+ if (!isNonEmptyString(bearerToken)) {
515
+ throw new TapsilatValidationError("Bearer token must be a non-empty string");
516
+ }
517
+ if (bearerToken.length < 10) {
518
+ throw new TapsilatValidationError("Bearer token must be at least 10 characters long");
519
+ }
520
+ // Karakter kontrolü (opsiyonel, isterseniz ek kurallar koyabilirsiniz)
521
+ if (!/^[a-zA-Z0-9._-]+$/.test(bearerToken)) {
522
+ throw new TapsilatValidationError("Bearer token contains invalid characters");
523
+ }
524
+ }
525
+ /**
526
+ * Validates payment request data comprehensively
527
+ *
528
+ * @param request - Payment request to validate
529
+ * @throws {TapsilatValidationError} When any field is invalid
530
+ */
531
+ function validatePaymentRequest(request) {
532
+ if (!request || typeof request !== "object") {
533
+ throw new TapsilatValidationError("Payment request must be an object");
534
+ }
535
+ const req = request;
536
+ // Amount validation
537
+ if (!isPositiveNumber(req.amount)) {
538
+ throw new TapsilatValidationError("Amount must be a positive number");
539
+ }
540
+ if (!hasValidDecimalPlaces(req.amount)) {
541
+ throw new TapsilatValidationError("Amount cannot have more than 2 decimal places");
542
+ }
543
+ // Currency validation
544
+ if (!isCurrency(req.currency)) {
545
+ throw new TapsilatValidationError(`Currency must be one of: ${["TRY", "USD", "EUR", "GBP"].join(", ")}`);
546
+ }
547
+ // Payment method validation
548
+ if (!isPaymentMethod(req.paymentMethod)) {
549
+ throw new TapsilatValidationError(`Payment method must be one of: ${[
550
+ "credit_card",
551
+ "debit_card",
552
+ "bank_transfer",
553
+ "digital_wallet",
554
+ ].join(", ")}`);
555
+ }
556
+ // Optional field validations
557
+ if (req.description !== undefined && !isNonEmptyString(req.description)) {
558
+ throw new TapsilatValidationError("Description must be a non-empty string when provided");
559
+ }
560
+ if (req.returnUrl !== undefined && !isValidUrl(req.returnUrl)) {
561
+ throw new TapsilatValidationError("Return URL must be a valid HTTP/HTTPS URL when provided");
562
+ }
563
+ if (req.cancelUrl !== undefined && !isValidUrl(req.cancelUrl)) {
564
+ throw new TapsilatValidationError("Cancel URL must be a valid HTTP/HTTPS URL when provided");
565
+ }
566
+ if (req.metadata !== undefined &&
567
+ (typeof req.metadata !== "object" || req.metadata === null)) {
568
+ throw new TapsilatValidationError("Metadata must be an object when provided");
569
+ }
570
+ }
571
+ /**
572
+ * Validates email format using robust regex
573
+ *
574
+ * @param email - Email address to validate
575
+ * @returns True if email format is valid
576
+ */
577
+ function validateEmail(email) {
578
+ if (!isNonEmptyString(email)) {
579
+ return false;
580
+ }
581
+ // More comprehensive email regex
582
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
583
+ return emailRegex.test(email);
584
+ }
585
+ /**
586
+ * @deprecated Use isCurrency type guard instead
587
+ */
588
+ function isValidCurrency(currency) {
589
+ return isCurrency(currency);
590
+ }
591
+ /**
592
+ * @deprecated Use isPaymentMethod type guard instead
593
+ */
594
+ function isValidPaymentMethod(paymentMethod) {
595
+ return isPaymentMethod(paymentMethod);
596
+ }
597
+ /**
598
+ * Validates URL format and protocol
599
+ *
600
+ * @param url - URL to validate
601
+ * @returns True if URL is valid HTTP/HTTPS
602
+ */
603
+ function isValidUrl(url) {
604
+ if (!isNonEmptyString(url)) {
605
+ return false;
606
+ }
607
+ try {
608
+ const urlObj = new URL(url);
609
+ return urlObj.protocol === "http:" || urlObj.protocol === "https:";
610
+ }
611
+ catch {
612
+ return false;
613
+ }
614
+ }
615
+ /**
616
+ * Sanitizes metadata by removing sensitive keys and cleaning values
617
+ *
618
+ * @param metadata - Raw metadata object
619
+ * @returns Sanitized metadata object
620
+ */
621
+ function sanitizeMetadata(metadata) {
622
+ const sensitiveKeys = new Set([
623
+ "password",
624
+ "secret",
625
+ "token",
626
+ "apikey",
627
+ "api_key",
628
+ "auth",
629
+ "authorization",
630
+ "cookie",
631
+ "session",
632
+ "bearertoken",
633
+ ]);
634
+ const sanitized = {};
635
+ for (const [key, value] of Object.entries(metadata)) {
636
+ // Skip sensitive keys (case-insensitive)
637
+ if (sensitiveKeys.has(key.toLowerCase())) {
638
+ continue;
639
+ }
640
+ // Clean and validate values
641
+ let cleanValue = value;
642
+ if (typeof value === "string") {
643
+ cleanValue = value.trim();
644
+ // Skip empty strings
645
+ if (cleanValue === "") {
646
+ continue;
647
+ }
648
+ }
649
+ else if (value === null || value === undefined) {
650
+ // Skip null/undefined values
651
+ continue;
652
+ }
653
+ else if (typeof value === "object") {
654
+ // Convert objects to string representation
655
+ cleanValue = "[object Object]";
656
+ }
657
+ sanitized[key] = cleanValue;
658
+ }
659
+ return sanitized;
660
+ }
661
+
662
+ /**
663
+ * Main SDK class for Tapsilat payment operations
664
+ *
665
+ * @example
666
+ * ```typescript
667
+ * const tapsilat = new TapsilatSDK({
668
+ * bearerToken: 'your-bearer-token',
669
+ * baseURL: 'https://api.tapsilat.com/v1'
670
+ * });
671
+ *
672
+ * const payment = await tapsilat.createPayment({
673
+ * amount: 100.50,
674
+ * currency: 'TRY',
675
+ * paymentMethod: 'credit_card'
676
+ * });
677
+ * ```
678
+ */
679
+ class TapsilatSDK {
680
+ httpClient;
681
+ config;
682
+ /**
683
+ * Creates a new TapsilatSDK instance
684
+ *
685
+ * @param config - SDK configuration options
686
+ * @throws {TapsilatValidationError} When Bearer token is invalid
687
+ */
688
+ constructor(config) {
689
+ validateBearerToken(config.bearerToken);
690
+ this.config = config;
691
+ this.httpClient = new HttpClient(config);
692
+ }
693
+ // Order Operations
694
+ /**
695
+ * Creates a new order and returns a checkout URL.
696
+ *
697
+ * @param orderRequest - The order details including amount, currency, and buyer information
698
+ * @returns Promise resolving to the created order response including reference ID and checkout URL
699
+ * @throws {TapsilatValidationError} When order request data is invalid
700
+ * @throws {TapsilatNetworkError} When API request fails due to network issues
701
+ * @throws {TapsilatError} When API returns an error response
702
+ *
703
+ * @example
704
+ * ```typescript
705
+ * try {
706
+ * const orderResponse = await tapsilat.createOrder({
707
+ * amount: 100.50,
708
+ * currency: 'TRY',
709
+ * locale: 'en',
710
+ * buyer: {
711
+ * name: 'John',
712
+ * surname: 'Doe',
713
+ * email: 'john.doe@example.com'
714
+ * }
715
+ * });
716
+ * console.log(`Checkout URL: ${orderResponse.checkoutUrl}`);
717
+ * } catch (error) {
718
+ * console.error(`Order creation failed: ${error.message}`);
719
+ * }
720
+ * ```
721
+ */
722
+ async createOrder(orderRequest) {
723
+ // Validate the order request
724
+ this.validateOrderRequest(orderRequest);
725
+ try {
726
+ // Make the API request
727
+ const response = await this.httpClient.post("/order/create", orderRequest);
728
+ // Use our generic response handler
729
+ return this.handleResponse(response, "Order creation");
730
+ }
731
+ catch (error) {
732
+ // Use our generic error handler
733
+ this.handleError(error, "order creation");
734
+ }
735
+ }
736
+ /**
737
+ * Validates an order request object
738
+ * @private
739
+ */
740
+ validateOrderRequest(orderRequest) {
741
+ // Check if request is null/undefined
742
+ if (!orderRequest) {
743
+ throw new TapsilatValidationError("Order request cannot be null or undefined");
744
+ }
745
+ // Validate amount
746
+ if (!isPositiveNumber(orderRequest.amount) ||
747
+ !hasValidDecimalPlaces(orderRequest.amount)) {
748
+ throw new TapsilatValidationError("Amount must be a positive number with maximum 2 decimal places", { provided: orderRequest.amount });
749
+ }
750
+ // Validate currency
751
+ if (!isNonEmptyString(orderRequest.currency)) {
752
+ throw new TapsilatValidationError("Currency is required and must be a non-empty string", { provided: orderRequest.currency });
753
+ }
754
+ // Validate buyer information
755
+ if (!orderRequest.buyer) {
756
+ throw new TapsilatValidationError("Buyer information is required");
757
+ }
758
+ const { buyer } = orderRequest;
759
+ if (!isNonEmptyString(buyer.name)) {
760
+ throw new TapsilatValidationError("Buyer name is required and must be a non-empty string", { provided: buyer.name });
761
+ }
762
+ if (!isNonEmptyString(buyer.surname)) {
763
+ throw new TapsilatValidationError("Buyer surname is required and must be a non-empty string", { provided: buyer.surname });
764
+ }
765
+ if (!isNonEmptyString(buyer.email) || !this.isValidEmail(buyer.email)) {
766
+ throw new TapsilatValidationError("Buyer email is required and must be a valid email address", { provided: buyer.email });
767
+ }
768
+ }
769
+ /**
770
+ * Basic email validation
771
+ * @private
772
+ */
773
+ isValidEmail(email) {
774
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
775
+ return emailRegex.test(email);
776
+ }
777
+ /**
778
+ * Generic response handler - consistent way to handle API responses across SDK methods
779
+ * @private
780
+ *
781
+ * @param response - The API response to process
782
+ * @param errorContext - Descriptive context for error messages
783
+ * @returns The data from a successful response
784
+ * @throws {TapsilatError} On any response error
785
+ */
786
+ handleResponse(response, errorContext) {
787
+ if (!response.success || !response.data) {
788
+ // Convert API errors to domain-specific errors
789
+ if (response.error) {
790
+ throw TapsilatError.fromAPIError(response.error);
791
+ }
792
+ throw new TapsilatError(`${errorContext} failed with no error details`, "UNKNOWN_ERROR");
793
+ }
794
+ return response.data;
795
+ }
796
+ /**
797
+ * Generic error handler - consistent way to handle errors across SDK methods
798
+ * @private
799
+ *
800
+ * @param error - The caught error to process
801
+ * @param errorContext - Descriptive context for error messages
802
+ * @throws {TapsilatError} Always throws an appropriate error type
803
+ */
804
+ handleError(error, errorContext) {
805
+ // Rethrow TapsilatErrors as-is
806
+ if (error instanceof TapsilatError) {
807
+ throw error;
808
+ }
809
+ // Convert generic errors to TapsilatNetworkError
810
+ throw new TapsilatNetworkError(error instanceof Error
811
+ ? error.message
812
+ : `Unknown error during ${errorContext}`, "NETWORK_ERROR");
813
+ }
814
+ /**
815
+ * Gets an order by reference ID.
816
+ *
817
+ * @param referenceId - The unique reference ID of the order
818
+ * @returns Promise resolving to the complete order details
819
+ * @throws {TapsilatValidationError} When referenceId is invalid
820
+ * @throws {TapsilatNetworkError} When API request fails due to network issues
821
+ * @throws {TapsilatError} When API returns an error response
822
+ *
823
+ * @example
824
+ * ```typescript
825
+ * try {
826
+ * const order = await tapsilat.getOrder("ord_123456789");
827
+ * console.log(`Order amount: ${order.amount} ${order.currency}`);
828
+ * } catch (error) {
829
+ * console.error(`Failed to get order: ${error.message}`);
830
+ * }
831
+ * ```
832
+ */
833
+ async getOrder(referenceId) {
834
+ // Validate input
835
+ if (!isNonEmptyString(referenceId)) {
836
+ throw new TapsilatValidationError("Order referenceId is required and must be a non-empty string", { provided: referenceId });
837
+ }
838
+ try {
839
+ // Make the API request
840
+ const response = await this.httpClient.get(`/order/${referenceId}`);
841
+ // Use our generic response handler
842
+ return this.handleResponse(response, "Order retrieval");
843
+ }
844
+ catch (error) {
845
+ // Use our generic error handler
846
+ this.handleError(error, "order retrieval");
847
+ }
848
+ }
849
+ /**
850
+ * Lists orders with pagination support.
851
+ *
852
+ * @param params - Optional pagination parameters (page number and items per page)
853
+ * @returns Promise resolving to paginated list of orders
854
+ * @throws {TapsilatNetworkError} When API request fails due to network issues
855
+ * @throws {TapsilatError} When API returns an error response
856
+ *
857
+ * @example
858
+ * ```typescript
859
+ * try {
860
+ * // Get the second page with 20 items per page
861
+ * const orderList = await tapsilat.getOrders({ page: 2, per_page: 20 });
862
+ * console.log(`Total orders: ${orderList.total}`);
863
+ * orderList.rows.forEach(order => {
864
+ * console.log(`Order ${order.referenceId}: ${order.amount} ${order.currency}`);
865
+ * });
866
+ * } catch (error) {
867
+ * console.error(`Failed to list orders: ${error.message}`);
868
+ * }
869
+ * ```
870
+ */
871
+ async getOrders(params = {}) {
872
+ try {
873
+ // Validate pagination parameters if provided
874
+ if (params.page !== undefined &&
875
+ (!Number.isInteger(params.page) || params.page < 1)) {
876
+ throw new TapsilatValidationError("Page number must be a positive integer", { provided: params.page });
877
+ }
878
+ if (params.per_page !== undefined &&
879
+ (!Number.isInteger(params.per_page) || params.per_page < 1)) {
880
+ throw new TapsilatValidationError("Items per page must be a positive integer", { provided: params.per_page });
881
+ }
882
+ // Make the API request
883
+ const response = await this.httpClient.get("/order/list", { params: params });
884
+ // Use our generic response handler
885
+ return this.handleResponse(response, "Order listing");
886
+ }
887
+ catch (error) {
888
+ // Use our generic error handler
889
+ this.handleError(error, "order listing");
890
+ }
891
+ }
892
+ /**
893
+ * Cancels an order by reference_id.
894
+ */
895
+ async cancelOrder(referenceId) {
896
+ if (!referenceId) {
897
+ throw new Error("Order referenceId is required for cancellation");
898
+ }
899
+ const response = await this.httpClient.post("/order/cancel", {
900
+ reference_id: referenceId,
901
+ });
902
+ if (!response.success || !response.data) {
903
+ throw new Error(response.error?.message || "Order cancellation failed");
904
+ }
905
+ return response.data;
906
+ }
907
+ /**
908
+ * Gets the status of an order by reference ID.
909
+ *
910
+ * @param referenceId - The unique reference ID of the order
911
+ * @returns Promise resolving to the current order status, including status code and last update time
912
+ * @throws {TapsilatValidationError} When referenceId is invalid
913
+ * @throws {TapsilatNetworkError} When API request fails due to network issues
914
+ * @throws {TapsilatError} When API returns an error response
915
+ *
916
+ * @example
917
+ * ```typescript
918
+ * try {
919
+ * const orderStatus = await tapsilat.getOrderStatus("ord_123456789");
920
+ * console.log(`Order status: ${orderStatus.status}`);
921
+ * } catch (error) {
922
+ * if (error instanceof TapsilatValidationError) {
923
+ * console.error("Invalid reference ID format");
924
+ * } else if (error instanceof TapsilatError) {
925
+ * console.error(`API error: ${error.message}`);
926
+ * }
927
+ * }
928
+ * ```
929
+ */
930
+ async getOrderStatus(referenceId) {
931
+ // Validate input using proper validation
932
+ if (!isNonEmptyString(referenceId)) {
933
+ throw new TapsilatValidationError("Order referenceId is required and must be a non-empty string", { provided: referenceId });
934
+ }
935
+ try {
936
+ // Make the API request
937
+ const response = await this.httpClient.get(`/order/${referenceId}/status`);
938
+ // Use our generic response handler
939
+ return this.handleResponse(response, "Order status retrieval");
940
+ }
941
+ catch (error) {
942
+ // Use our generic error handler
943
+ this.handleError(error, "order status retrieval");
944
+ }
945
+ }
946
+ /**
947
+ * Partially refunds an order.
948
+ * Based on `refund_order` from the Python SDK.
949
+ *
950
+ * @param refundData - The refund details, including referenceId and amount.
951
+ * @returns Promise resolving to the refund transaction details.
952
+ */
953
+ async refundOrder(refundData) {
954
+ const response = await this.httpClient.post("/order/refund", refundData);
955
+ if (!response.success || !response.data) {
956
+ throw new Error(response.error?.message || "Order refund failed");
957
+ }
958
+ return response.data;
959
+ }
960
+ /**
961
+ * Fully refunds an order by its reference ID.
962
+ * Based on `refund_all_order` from the Python SDK.
963
+ *
964
+ * @param referenceId - The unique identifier of the order to fully refund.
965
+ * @returns Promise resolving to the refund transaction details.
966
+ */
967
+ async refundAllOrder(referenceId) {
968
+ const response = await this.httpClient.post("/order/refund-all", { reference_id: referenceId });
969
+ if (!response.success || !response.data) {
970
+ throw new Error(response.error?.message || "Full order refund failed");
971
+ }
972
+ return response.data;
973
+ }
974
+ /**
975
+ * Retrieves the payment details for an order.
976
+ * Based on `get_order_payment_details` from the Python SDK.
977
+ *
978
+ * @param referenceId - The unique identifier of the order.
979
+ * @param conversationId - Optional conversation ID for more specific querying.
980
+ * @returns Promise resolving to a list of payment details.
981
+ */
982
+ async getOrderPaymentDetails(referenceId, conversationId) {
983
+ if (conversationId) {
984
+ // If conversation_id is provided, use POST to /order/payment-details
985
+ const response = await this.httpClient.post("/order/payment-details", { conversation_id: conversationId, reference_id: referenceId });
986
+ if (!response.success || !response.data) {
987
+ throw new Error(response.error?.message || "Failed to get payment details");
988
+ }
989
+ return response.data;
990
+ }
991
+ else {
992
+ // If no conversation_id, use GET to /order/{reference_id}/payment-details
993
+ const response = await this.httpClient.get(`/order/${referenceId}/payment-details`);
994
+ if (!response.success || !response.data) {
995
+ throw new Error(response.error?.message || "Failed to get payment details");
996
+ }
997
+ return response.data;
998
+ }
999
+ }
1000
+ /**
1001
+ * Gets an order by conversation_id.
1002
+ * Based on `get_order_by_conversation_id` from the Python SDK.
1003
+ */
1004
+ async getOrderByConversationId(conversationId) {
1005
+ if (!conversationId) {
1006
+ throw new Error("Order conversationId is required");
1007
+ }
1008
+ const response = await this.httpClient.get(`/order/conversation/${conversationId}`);
1009
+ if (!response.success || !response.data) {
1010
+ throw new Error(response.error?.message || "Order retrieval by conversation ID failed");
1011
+ }
1012
+ return response.data;
1013
+ }
1014
+ /**
1015
+ * Gets order transactions by reference_id.
1016
+ * Based on `get_order_transactions` from the Python SDK.
1017
+ */
1018
+ async getOrderTransactions(referenceId) {
1019
+ if (!referenceId) {
1020
+ throw new Error("Order referenceId is required");
1021
+ }
1022
+ const response = await this.httpClient.get(`/order/${referenceId}/transactions`);
1023
+ if (!response.success || !response.data) {
1024
+ throw new Error(response.error?.message || "Order transactions retrieval failed");
1025
+ }
1026
+ return response.data;
1027
+ }
1028
+ /**
1029
+ * Gets order submerchants with pagination.
1030
+ * Based on `get_order_submerchants` from the Python SDK.
1031
+ */
1032
+ async getOrderSubmerchants(params = {}) {
1033
+ const response = await this.httpClient.get("/order/submerchants", {
1034
+ params: params,
1035
+ });
1036
+ if (!response.success || !response.data) {
1037
+ throw new Error(response.error?.message || "Order submerchants retrieval failed");
1038
+ }
1039
+ return response.data;
1040
+ }
1041
+ /**
1042
+ * Gets checkout URL for an order.
1043
+ * Based on `get_checkout_url` from the Python SDK.
1044
+ */
1045
+ async getCheckoutUrl(referenceId) {
1046
+ const order = await this.getOrder(referenceId);
1047
+ if (order && order.checkout_url) {
1048
+ return order.checkout_url;
1049
+ }
1050
+ throw new Error("Checkout URL not found in order response");
1051
+ }
1052
+ // Utility Methods
1053
+ /**
1054
+ * Verifies webhook signature for security
1055
+ *
1056
+ * @param payload - Raw webhook payload string
1057
+ * @param signature - Webhook signature from headers
1058
+ * @param secret - Your webhook secret key
1059
+ * @returns Promise resolving to true if signature is valid
1060
+ *
1061
+ * @example
1062
+ * ```typescript
1063
+ * const isValid = await sdk.verifyWebhook(
1064
+ * JSON.stringify(req.body),
1065
+ * req.headers['x-tapsilat-signature'],
1066
+ * 'your-webhook-secret'
1067
+ * );
1068
+ * ```
1069
+ */
1070
+ async verifyWebhook(payload, signature, secret) {
1071
+ // Implementation would depend on your webhook signature verification method
1072
+ // This is a placeholder implementation
1073
+ const crypto = await import('crypto');
1074
+ const expectedSignature = crypto
1075
+ .createHmac("sha256", secret)
1076
+ .update(payload)
1077
+ .digest("hex");
1078
+ return `sha256=${expectedSignature}` === signature;
1079
+ }
1080
+ /**
1081
+ * Checks API service health and availability
1082
+ *
1083
+ * @returns Promise resolving to service status
1084
+ */
1085
+ async healthCheck() {
1086
+ const response = await this.httpClient.get("/health");
1087
+ if (!response.success || !response.data) {
1088
+ throw new Error(response.error?.message || "Health check failed");
1089
+ }
1090
+ return response.data;
1091
+ }
1092
+ // Configuration methods
1093
+ /**
1094
+ * Gets current SDK configuration (without sensitive data)
1095
+ *
1096
+ * @returns Copy of current configuration
1097
+ */
1098
+ getConfig() {
1099
+ const { bearerToken, ...safeConfig } = this.config;
1100
+ return {
1101
+ ...safeConfig,
1102
+ hasBearerToken: Boolean(bearerToken),
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Updates SDK configuration
1107
+ *
1108
+ * @param newConfig - Partial configuration to update
1109
+ * @throws {TapsilatValidationError} When Bearer token is invalid
1110
+ */
1111
+ updateConfig(newConfig) {
1112
+ if (newConfig.bearerToken) {
1113
+ validateBearerToken(newConfig.bearerToken);
1114
+ }
1115
+ Object.assign(this.config, newConfig);
1116
+ }
1117
+ }
1118
+
1119
+ exports.TapsilatAuthenticationError = TapsilatAuthenticationError;
1120
+ exports.TapsilatError = TapsilatError;
1121
+ exports.TapsilatNetworkError = TapsilatNetworkError;
1122
+ exports.TapsilatRateLimitError = TapsilatRateLimitError;
1123
+ exports.TapsilatSDK = TapsilatSDK;
1124
+ exports.TapsilatValidationError = TapsilatValidationError;
1125
+ exports.isValidCurrency = isValidCurrency;
1126
+ exports.isValidPaymentMethod = isValidPaymentMethod;
1127
+ exports.isValidUrl = isValidUrl;
1128
+ exports.sanitizeMetadata = sanitizeMetadata;
1129
+ exports.validateEmail = validateEmail;
1130
+ exports.validatePaymentRequest = validatePaymentRequest;
1131
+ //# sourceMappingURL=index.js.map