@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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/dist/TapsilatSDK.d.ts +293 -0
- package/dist/TapsilatSDK.d.ts.map +1 -0
- package/dist/errors/TapsilatError.d.ts +22 -0
- package/dist/errors/TapsilatError.d.ts.map +1 -0
- package/dist/http/HttpClient.d.ts +149 -0
- package/dist/http/HttpClient.d.ts.map +1 -0
- package/dist/http/interceptors.d.ts +50 -0
- package/dist/http/interceptors.d.ts.map +1 -0
- package/dist/index.d.ts +541 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +1118 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1131 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +93 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types.d.ts +401 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/validators.d.ts +84 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/package.json +87 -0
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
|