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