xendit-fn 1.0.0
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/LICENSE +21 -0
- package/README.md +442 -0
- package/lib/index.cjs +1588 -0
- package/lib/index.d.cts +9574 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.esm.d.ts +9574 -0
- package/lib/index.esm.js +1544 -0
- package/lib/sdk/axios.d.ts +3 -0
- package/lib/sdk/axios.d.ts.map +1 -0
- package/lib/sdk/card/index.d.ts +2 -0
- package/lib/sdk/card/index.d.ts.map +1 -0
- package/lib/sdk/card/schema.d.ts +586 -0
- package/lib/sdk/card/schema.d.ts.map +1 -0
- package/lib/sdk/common.d.ts +28 -0
- package/lib/sdk/common.d.ts.map +1 -0
- package/lib/sdk/customer/index.d.ts +7 -0
- package/lib/sdk/customer/index.d.ts.map +1 -0
- package/lib/sdk/customer/schema.d.ts +5933 -0
- package/lib/sdk/customer/schema.d.ts.map +1 -0
- package/lib/sdk/ewallet/create.d.ts +173 -0
- package/lib/sdk/ewallet/create.d.ts.map +1 -0
- package/lib/sdk/ewallet/schema.d.ts +783 -0
- package/lib/sdk/ewallet/schema.d.ts.map +1 -0
- package/lib/sdk/index.d.ts +2201 -0
- package/lib/sdk/index.d.ts.map +1 -0
- package/lib/sdk/invoice/index.d.ts +8 -0
- package/lib/sdk/invoice/index.d.ts.map +1 -0
- package/lib/sdk/invoice/schema.d.ts +2198 -0
- package/lib/sdk/invoice/schema.d.ts.map +1 -0
- package/lib/sdk/payment-method/index.d.ts +7 -0
- package/lib/sdk/payment-method/index.d.ts.map +1 -0
- package/lib/sdk/payment-method/schema.d.ts +990 -0
- package/lib/sdk/payment-method/schema.d.ts.map +1 -0
- package/lib/utils/errors.d.ts +58 -0
- package/lib/utils/errors.d.ts.map +1 -0
- package/lib/utils/index.d.ts +6 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/pagination.d.ts +134 -0
- package/lib/utils/pagination.d.ts.map +1 -0
- package/lib/utils/rate-limit.d.ts +90 -0
- package/lib/utils/rate-limit.d.ts.map +1 -0
- package/lib/utils/type-guards.d.ts +13 -0
- package/lib/utils/type-guards.d.ts.map +1 -0
- package/lib/utils/webhook.d.ts +101 -0
- package/lib/utils/webhook.d.ts.map +1 -0
- package/package.json +83 -0
package/lib/index.esm.js
ADDED
|
@@ -0,0 +1,1544 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createHmac } from 'crypto';
|
|
4
|
+
|
|
5
|
+
const createAxiosInstance = (config)=>axios.create({
|
|
6
|
+
...config,
|
|
7
|
+
headers: {
|
|
8
|
+
Accept: "application/json",
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
common: {
|
|
11
|
+
Accept: "application/json",
|
|
12
|
+
"Content-Type": "application/json"
|
|
13
|
+
},
|
|
14
|
+
...config?.headers
|
|
15
|
+
},
|
|
16
|
+
baseURL: "https://api.xendit.co"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Rate limiter implementation using token bucket algorithm
|
|
21
|
+
*/ class RateLimiter {
|
|
22
|
+
/**
|
|
23
|
+
* Refill tokens based on elapsed time
|
|
24
|
+
*/ refillTokens() {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const timePassed = now - this.lastRefill;
|
|
27
|
+
const tokensToAdd = timePassed / this.config.windowMs * this.config.maxRequests;
|
|
28
|
+
this.tokens = Math.min(this.config.maxRequests, this.tokens + tokensToAdd);
|
|
29
|
+
this.lastRefill = now;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if a request can be made
|
|
33
|
+
*/ canMakeRequest() {
|
|
34
|
+
this.refillTokens();
|
|
35
|
+
return this.tokens >= 1;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Consume a token for a request
|
|
39
|
+
*/ consumeToken() {
|
|
40
|
+
this.refillTokens();
|
|
41
|
+
if (this.tokens >= 1) {
|
|
42
|
+
this.tokens -= 1;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get time until next token is available
|
|
49
|
+
*/ getWaitTime() {
|
|
50
|
+
if (this.canMakeRequest()) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const tokensNeeded = 1 - this.tokens;
|
|
54
|
+
return tokensNeeded / this.config.maxRequests * this.config.windowMs;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Wait for a token to become available
|
|
58
|
+
*/ async waitForToken() {
|
|
59
|
+
const waitTime = this.getWaitTime();
|
|
60
|
+
if (waitTime > 0) {
|
|
61
|
+
await this.sleep(waitTime);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sleep for specified milliseconds
|
|
66
|
+
*/ sleep(ms) {
|
|
67
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
68
|
+
}
|
|
69
|
+
constructor(config = {}){
|
|
70
|
+
this.config = {
|
|
71
|
+
maxRequests: config.maxRequests ?? 100,
|
|
72
|
+
windowMs: config.windowMs ?? 60000,
|
|
73
|
+
requestDelayMs: config.requestDelayMs ?? 0,
|
|
74
|
+
maxRetries: config.maxRetries ?? 3,
|
|
75
|
+
baseRetryDelayMs: config.baseRetryDelayMs ?? 1000,
|
|
76
|
+
maxRetryDelayMs: config.maxRetryDelayMs ?? 30000
|
|
77
|
+
};
|
|
78
|
+
this.tokens = this.config.maxRequests;
|
|
79
|
+
this.lastRefill = Date.now();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Axios interceptor for rate limiting
|
|
84
|
+
*/ function createRateLimitInterceptor(rateLimiter) {
|
|
85
|
+
return {
|
|
86
|
+
request: async (config)=>{
|
|
87
|
+
// Wait for token availability
|
|
88
|
+
await rateLimiter.waitForToken();
|
|
89
|
+
// Consume token
|
|
90
|
+
rateLimiter.consumeToken();
|
|
91
|
+
// Apply request delay if configured
|
|
92
|
+
const delay = rateLimiter.config.requestDelayMs;
|
|
93
|
+
if (delay > 0) {
|
|
94
|
+
await rateLimiter.sleep(delay);
|
|
95
|
+
}
|
|
96
|
+
return config;
|
|
97
|
+
},
|
|
98
|
+
response: (response)=>{
|
|
99
|
+
// Check for rate limit headers and adjust if needed
|
|
100
|
+
const rateLimitRemaining = response.headers["x-ratelimit-remaining"];
|
|
101
|
+
if (rateLimitRemaining !== undefined && Number(rateLimitRemaining) === 0) {
|
|
102
|
+
console.warn("Rate limit reached, requests will be throttled");
|
|
103
|
+
}
|
|
104
|
+
return response;
|
|
105
|
+
},
|
|
106
|
+
responseError: async (error)=>{
|
|
107
|
+
// Handle rate limit errors (HTTP 429)
|
|
108
|
+
if (error.response?.status === 429) {
|
|
109
|
+
const retryAfter = error.response.headers["retry-after"];
|
|
110
|
+
const delay = retryAfter ? Number(retryAfter) * 1000 : rateLimiter.config.baseRetryDelayMs;
|
|
111
|
+
console.warn(`Rate limited, retrying after ${delay}ms`);
|
|
112
|
+
await rateLimiter.sleep(delay);
|
|
113
|
+
// Don't automatically retry here, let the retry interceptor handle it
|
|
114
|
+
return Promise.reject(error);
|
|
115
|
+
}
|
|
116
|
+
return Promise.reject(error);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Basic retry interceptor - simplified for compatibility
|
|
122
|
+
*/ function createRetryInterceptor(_config = {}) {
|
|
123
|
+
return async (error)=>{
|
|
124
|
+
// For now, just log retryable errors and reject
|
|
125
|
+
// This can be enhanced in the future with proper retry logic
|
|
126
|
+
if (isRetryableError(error)) {
|
|
127
|
+
console.warn("Request failed with retryable error:", error.message);
|
|
128
|
+
}
|
|
129
|
+
return Promise.reject(error);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if an error is retryable
|
|
134
|
+
*/ function isRetryableError(error) {
|
|
135
|
+
// No response means network error, which is retryable
|
|
136
|
+
if (!error.response) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
// Specific status codes that are retryable
|
|
140
|
+
const retryableStatusCodes = [
|
|
141
|
+
408,
|
|
142
|
+
429,
|
|
143
|
+
500,
|
|
144
|
+
502,
|
|
145
|
+
503,
|
|
146
|
+
504
|
|
147
|
+
];
|
|
148
|
+
return retryableStatusCodes.includes(error.response.status);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Setup rate limiting and retry logic for an Axios instance
|
|
152
|
+
*/ function setupRateLimit(axiosInstance, config = {}) {
|
|
153
|
+
const rateLimiter = new RateLimiter(config);
|
|
154
|
+
const rateLimitInterceptor = createRateLimitInterceptor(rateLimiter);
|
|
155
|
+
const retryInterceptor = createRetryInterceptor(config);
|
|
156
|
+
// Add request interceptor for rate limiting
|
|
157
|
+
axiosInstance.interceptors.request.use(rateLimitInterceptor.request, (error)=>Promise.reject(error));
|
|
158
|
+
// Add response interceptors
|
|
159
|
+
axiosInstance.interceptors.response.use(rateLimitInterceptor.response, rateLimitInterceptor.responseError);
|
|
160
|
+
// Add retry interceptor (should be last)
|
|
161
|
+
axiosInstance.interceptors.response.use((response)=>response, retryInterceptor);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Create a rate-limited axios instance
|
|
165
|
+
*/ function createRateLimitedAxios(baseURL, apiKey, config = {}) {
|
|
166
|
+
const axiosInstance = axios.create({
|
|
167
|
+
baseURL,
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Basic ${Buffer.from(`${apiKey}:`).toString("base64")}`,
|
|
170
|
+
"Content-Type": "application/json"
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
setupRateLimit(axiosInstance, config);
|
|
174
|
+
return axiosInstance;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const XenditErrorSchema = z.object({
|
|
178
|
+
error_code: z.string(),
|
|
179
|
+
message: z.string(),
|
|
180
|
+
errors: z.array(z.object({
|
|
181
|
+
field: z.string().optional(),
|
|
182
|
+
message: z.string()
|
|
183
|
+
})).optional()
|
|
184
|
+
});
|
|
185
|
+
class XenditApiError extends Error {
|
|
186
|
+
constructor(message, code, statusCode, details){
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = "XenditApiError";
|
|
189
|
+
this.code = code;
|
|
190
|
+
this.statusCode = statusCode;
|
|
191
|
+
this.details = details;
|
|
192
|
+
// Maintain proper stack trace for where our error was thrown
|
|
193
|
+
if (Error.captureStackTrace) {
|
|
194
|
+
Error.captureStackTrace(this, XenditApiError);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
class ValidationError extends Error {
|
|
199
|
+
constructor(message, validationErrors, field){
|
|
200
|
+
super(message);
|
|
201
|
+
this.name = "ValidationError";
|
|
202
|
+
this.field = field;
|
|
203
|
+
this.validationErrors = validationErrors;
|
|
204
|
+
if (Error.captureStackTrace) {
|
|
205
|
+
Error.captureStackTrace(this, ValidationError);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
class AuthenticationError extends XenditApiError {
|
|
210
|
+
constructor(message = "Authentication failed"){
|
|
211
|
+
super(message, "AUTHENTICATION_ERROR", 401);
|
|
212
|
+
this.name = "AuthenticationError";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
class NotFoundError extends XenditApiError {
|
|
216
|
+
constructor(message = "Resource not found"){
|
|
217
|
+
super(message, "NOT_FOUND_ERROR", 404);
|
|
218
|
+
this.name = "NotFoundError";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
class RateLimitError extends XenditApiError {
|
|
222
|
+
constructor(message = "Rate limit exceeded"){
|
|
223
|
+
super(message, "RATE_LIMIT_ERROR", 429);
|
|
224
|
+
this.name = "RateLimitError";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const handleAxiosError = (error)=>{
|
|
228
|
+
if (error.response?.data) {
|
|
229
|
+
const parsed = XenditErrorSchema.safeParse(error.response.data);
|
|
230
|
+
if (parsed.success) {
|
|
231
|
+
throw new XenditApiError(parsed.data.message, parsed.data.error_code, error.response.status, parsed.data.errors ? {
|
|
232
|
+
errors: parsed.data.errors
|
|
233
|
+
} : undefined);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw new XenditApiError(error.message || "Unknown API error", error.code || "UNKNOWN_ERROR", error.response?.status, error.response?.data);
|
|
237
|
+
};
|
|
238
|
+
const validateInput = (schema, data, fieldName)=>{
|
|
239
|
+
const result = schema.safeParse(data);
|
|
240
|
+
if (!result.success) {
|
|
241
|
+
throw new ValidationError(`Validation failed${fieldName ? ` for field ${fieldName}` : ""}`, result.error.issues, fieldName);
|
|
242
|
+
}
|
|
243
|
+
return result.data;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const PhoneSchema = z.string().min(7).max(15).refine((value)=>value.startsWith("+"));
|
|
247
|
+
const CountrySchema = z.union([
|
|
248
|
+
z.literal("PH"),
|
|
249
|
+
z.literal("ID"),
|
|
250
|
+
z.literal("MY"),
|
|
251
|
+
z.literal("TH"),
|
|
252
|
+
z.literal("VN")
|
|
253
|
+
]);
|
|
254
|
+
const CurrencySchema = z.union([
|
|
255
|
+
z.literal("PHP"),
|
|
256
|
+
z.literal("IDR"),
|
|
257
|
+
z.literal("MYR"),
|
|
258
|
+
z.literal("THB"),
|
|
259
|
+
z.literal("VND")
|
|
260
|
+
]);
|
|
261
|
+
z.object({
|
|
262
|
+
given_names: z.string(),
|
|
263
|
+
surname: z.string().optional(),
|
|
264
|
+
email: z.string().email().optional(),
|
|
265
|
+
mobile_number: PhoneSchema.optional(),
|
|
266
|
+
phone_number: PhoneSchema.optional()
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
z.union([
|
|
270
|
+
z.literal("INDIVIDUAL"),
|
|
271
|
+
z.literal("BUSINESS")
|
|
272
|
+
]);
|
|
273
|
+
const IndividualDetailSchema = z.object({
|
|
274
|
+
given_names: z.string(),
|
|
275
|
+
surname: z.string().optional(),
|
|
276
|
+
nationality: z.string().optional(),
|
|
277
|
+
place_of_birth: z.string().optional(),
|
|
278
|
+
date_of_birth: z.string().optional(),
|
|
279
|
+
gender: z.union([
|
|
280
|
+
z.literal("MALE"),
|
|
281
|
+
z.literal("FEMALE"),
|
|
282
|
+
z.literal("OTHER")
|
|
283
|
+
]).optional(),
|
|
284
|
+
employment: z.object({
|
|
285
|
+
employer_name: z.string(),
|
|
286
|
+
nature_of_business: z.string(),
|
|
287
|
+
role_description: z.string()
|
|
288
|
+
}).optional()
|
|
289
|
+
});
|
|
290
|
+
z.union([
|
|
291
|
+
z.literal("CORPORATION"),
|
|
292
|
+
z.literal("SOLE_PROPRIETOR"),
|
|
293
|
+
z.literal("PARTNERSHIP"),
|
|
294
|
+
z.literal("COOPERATIVE"),
|
|
295
|
+
z.literal("TRUST"),
|
|
296
|
+
z.literal("NON_PROFIT"),
|
|
297
|
+
z.literal("GOVERNMENT")
|
|
298
|
+
]);
|
|
299
|
+
const BusinessDetailSchema = z.object({
|
|
300
|
+
business_name: z.string(),
|
|
301
|
+
trading_name: z.string().optional(),
|
|
302
|
+
business_type: z.string()
|
|
303
|
+
});
|
|
304
|
+
const AddressSchema = z.object({
|
|
305
|
+
street_line1: z.string().nullable().optional(),
|
|
306
|
+
street_line2: z.string().nullable().optional(),
|
|
307
|
+
city: z.string().nullable().optional(),
|
|
308
|
+
province_state: z.string().nullable().optional(),
|
|
309
|
+
postal_code: z.string().nullable().optional(),
|
|
310
|
+
country: z.string(),
|
|
311
|
+
category: z.string().nullable().optional(),
|
|
312
|
+
is_primary: z.boolean().nullable().optional()
|
|
313
|
+
});
|
|
314
|
+
const AccountTypeSchema = z.union([
|
|
315
|
+
z.literal("BANK_ACCOUNT"),
|
|
316
|
+
z.literal("EWALLET"),
|
|
317
|
+
z.literal("CREDIT_CARD"),
|
|
318
|
+
z.literal("PAY_LATER"),
|
|
319
|
+
z.literal("OTC"),
|
|
320
|
+
z.literal("QR_CODE"),
|
|
321
|
+
z.literal("SOCIAL_MEDIA")
|
|
322
|
+
]);
|
|
323
|
+
const BankAccountSchema = z.object({
|
|
324
|
+
account_number: z.string(),
|
|
325
|
+
account_holder_name: z.string(),
|
|
326
|
+
swift_code: z.string().optional(),
|
|
327
|
+
account_type: z.string().optional(),
|
|
328
|
+
account_details: z.string().optional(),
|
|
329
|
+
currency: z.string().optional()
|
|
330
|
+
});
|
|
331
|
+
const EWalletAccountSchema = z.object({
|
|
332
|
+
account_number: z.string(),
|
|
333
|
+
account_holder_name: z.string(),
|
|
334
|
+
currency: z.string().optional()
|
|
335
|
+
});
|
|
336
|
+
const CreditCardAccountSchema = z.object({
|
|
337
|
+
token_id: z.string()
|
|
338
|
+
});
|
|
339
|
+
const OTCAccountSchema = z.object({
|
|
340
|
+
payment_code: z.string(),
|
|
341
|
+
expires_at: z.string().optional()
|
|
342
|
+
});
|
|
343
|
+
const QrAccountSchema = z.object({
|
|
344
|
+
qr_string: z.string()
|
|
345
|
+
});
|
|
346
|
+
const PayLaterAccountSchema = z.object({
|
|
347
|
+
account_id: z.string(),
|
|
348
|
+
account_holder_name: z.string().optional(),
|
|
349
|
+
currency: CurrencySchema.optional()
|
|
350
|
+
});
|
|
351
|
+
const SocialMediaAccountSchema = z.object({
|
|
352
|
+
account_id: z.string(),
|
|
353
|
+
account_handle: z.string().optional()
|
|
354
|
+
});
|
|
355
|
+
const PropertiesSchema = z.discriminatedUnion("type", [
|
|
356
|
+
z.object({
|
|
357
|
+
type: z.literal("BANK_ACCOUNT"),
|
|
358
|
+
properties: BankAccountSchema
|
|
359
|
+
}),
|
|
360
|
+
z.object({
|
|
361
|
+
type: z.literal("EWALLET"),
|
|
362
|
+
properties: EWalletAccountSchema
|
|
363
|
+
}),
|
|
364
|
+
z.object({
|
|
365
|
+
type: z.literal("CREDIT_CARD"),
|
|
366
|
+
properties: CreditCardAccountSchema
|
|
367
|
+
}),
|
|
368
|
+
z.object({
|
|
369
|
+
type: z.literal("OTC"),
|
|
370
|
+
properties: OTCAccountSchema
|
|
371
|
+
}),
|
|
372
|
+
z.object({
|
|
373
|
+
type: z.literal("QR_CODE"),
|
|
374
|
+
properties: QrAccountSchema
|
|
375
|
+
}),
|
|
376
|
+
z.object({
|
|
377
|
+
type: z.literal("PAY_LATER"),
|
|
378
|
+
properties: PayLaterAccountSchema
|
|
379
|
+
}),
|
|
380
|
+
z.object({
|
|
381
|
+
type: z.literal("SOCIAL_MEDIA"),
|
|
382
|
+
properties: SocialMediaAccountSchema
|
|
383
|
+
})
|
|
384
|
+
]);
|
|
385
|
+
const IdentityAccountSchema = z.object({
|
|
386
|
+
type: AccountTypeSchema,
|
|
387
|
+
company: z.string().nullable().optional(),
|
|
388
|
+
description: z.string().nullable().optional(),
|
|
389
|
+
country: z.string().nullable().optional(),
|
|
390
|
+
properties: PropertiesSchema
|
|
391
|
+
});
|
|
392
|
+
const KYCDocumentSchema = z.object({
|
|
393
|
+
type: z.string(),
|
|
394
|
+
sub_type: z.string(),
|
|
395
|
+
country: z.string(),
|
|
396
|
+
document_name: z.string(),
|
|
397
|
+
document_number: z.string(),
|
|
398
|
+
expires_at: z.null(),
|
|
399
|
+
holder_name: z.string(),
|
|
400
|
+
document_images: z.array(z.string())
|
|
401
|
+
});
|
|
402
|
+
const CommonCustomerResourceSchema = z.object({
|
|
403
|
+
individual_detail: IndividualDetailSchema.optional(),
|
|
404
|
+
business_detail: BusinessDetailSchema.optional(),
|
|
405
|
+
email: z.string().email().optional(),
|
|
406
|
+
mobile_number: PhoneSchema.optional(),
|
|
407
|
+
phone_number: PhoneSchema.optional(),
|
|
408
|
+
hashed_phone_number: z.string().nullable().optional(),
|
|
409
|
+
addresses: z.array(AddressSchema).optional(),
|
|
410
|
+
identity_accounts: z.array(IdentityAccountSchema).optional(),
|
|
411
|
+
kyc_documents: z.array(KYCDocumentSchema).optional(),
|
|
412
|
+
description: z.string().nullable().optional(),
|
|
413
|
+
date_of_registration: z.string().nullable().optional(),
|
|
414
|
+
domicile_of_registration: z.string().nullable().optional(),
|
|
415
|
+
metadata: z.object({}).nullable().optional()
|
|
416
|
+
});
|
|
417
|
+
// Create a discriminated union for customer types
|
|
418
|
+
const CustomerSchema = z.discriminatedUnion("type", [
|
|
419
|
+
z.object({
|
|
420
|
+
type: z.literal("INDIVIDUAL"),
|
|
421
|
+
reference_id: z.string(),
|
|
422
|
+
individual_detail: IndividualDetailSchema,
|
|
423
|
+
business_detail: z.undefined().optional(),
|
|
424
|
+
email: z.string().email().optional(),
|
|
425
|
+
mobile_number: PhoneSchema.optional(),
|
|
426
|
+
phone_number: PhoneSchema.optional(),
|
|
427
|
+
hashed_phone_number: z.string().nullable().optional(),
|
|
428
|
+
addresses: z.array(AddressSchema).optional(),
|
|
429
|
+
identity_accounts: z.array(IdentityAccountSchema).optional(),
|
|
430
|
+
kyc_documents: z.array(KYCDocumentSchema).optional(),
|
|
431
|
+
description: z.string().nullable().optional(),
|
|
432
|
+
date_of_registration: z.string().nullable().optional(),
|
|
433
|
+
domicile_of_registration: z.string().nullable().optional(),
|
|
434
|
+
metadata: z.object({}).nullable().optional()
|
|
435
|
+
}),
|
|
436
|
+
z.object({
|
|
437
|
+
type: z.literal("BUSINESS"),
|
|
438
|
+
reference_id: z.string(),
|
|
439
|
+
individual_detail: z.undefined().optional(),
|
|
440
|
+
business_detail: BusinessDetailSchema,
|
|
441
|
+
email: z.string().email().optional(),
|
|
442
|
+
mobile_number: PhoneSchema.optional(),
|
|
443
|
+
phone_number: PhoneSchema.optional(),
|
|
444
|
+
hashed_phone_number: z.string().nullable().optional(),
|
|
445
|
+
addresses: z.array(AddressSchema).optional(),
|
|
446
|
+
identity_accounts: z.array(IdentityAccountSchema).optional(),
|
|
447
|
+
kyc_documents: z.array(KYCDocumentSchema).optional(),
|
|
448
|
+
description: z.string().nullable().optional(),
|
|
449
|
+
date_of_registration: z.string().nullable().optional(),
|
|
450
|
+
domicile_of_registration: z.string().nullable().optional(),
|
|
451
|
+
metadata: z.object({}).nullable().optional()
|
|
452
|
+
})
|
|
453
|
+
]);
|
|
454
|
+
const GetCustomerSchema = z.object({
|
|
455
|
+
id: z.string()
|
|
456
|
+
});
|
|
457
|
+
// Create CustomerResourceSchema by extending both discriminated union options
|
|
458
|
+
const CustomerResourceSchema = z.discriminatedUnion("type", [
|
|
459
|
+
z.object({
|
|
460
|
+
type: z.literal("INDIVIDUAL"),
|
|
461
|
+
id: z.string(),
|
|
462
|
+
reference_id: z.string(),
|
|
463
|
+
individual_detail: IndividualDetailSchema,
|
|
464
|
+
business_detail: z.undefined().optional(),
|
|
465
|
+
email: z.string().email().optional(),
|
|
466
|
+
mobile_number: PhoneSchema.optional(),
|
|
467
|
+
phone_number: PhoneSchema.optional(),
|
|
468
|
+
hashed_phone_number: z.string().nullable().optional(),
|
|
469
|
+
addresses: z.array(AddressSchema).optional(),
|
|
470
|
+
identity_accounts: z.array(IdentityAccountSchema).optional(),
|
|
471
|
+
kyc_documents: z.array(KYCDocumentSchema).optional(),
|
|
472
|
+
description: z.string().nullable().optional(),
|
|
473
|
+
date_of_registration: z.string().nullable().optional(),
|
|
474
|
+
domicile_of_registration: z.string().nullable().optional(),
|
|
475
|
+
metadata: z.object({}).nullable().optional(),
|
|
476
|
+
created: z.string().datetime(),
|
|
477
|
+
updated: z.string().datetime()
|
|
478
|
+
}),
|
|
479
|
+
z.object({
|
|
480
|
+
type: z.literal("BUSINESS"),
|
|
481
|
+
id: z.string(),
|
|
482
|
+
reference_id: z.string(),
|
|
483
|
+
individual_detail: z.undefined().optional(),
|
|
484
|
+
business_detail: BusinessDetailSchema,
|
|
485
|
+
email: z.string().email().optional(),
|
|
486
|
+
mobile_number: PhoneSchema.optional(),
|
|
487
|
+
phone_number: PhoneSchema.optional(),
|
|
488
|
+
hashed_phone_number: z.string().nullable().optional(),
|
|
489
|
+
addresses: z.array(AddressSchema).optional(),
|
|
490
|
+
identity_accounts: z.array(IdentityAccountSchema).optional(),
|
|
491
|
+
kyc_documents: z.array(KYCDocumentSchema).optional(),
|
|
492
|
+
description: z.string().nullable().optional(),
|
|
493
|
+
date_of_registration: z.string().nullable().optional(),
|
|
494
|
+
domicile_of_registration: z.string().nullable().optional(),
|
|
495
|
+
metadata: z.object({}).nullable().optional(),
|
|
496
|
+
created: z.string().datetime(),
|
|
497
|
+
updated: z.string().datetime()
|
|
498
|
+
})
|
|
499
|
+
]);
|
|
500
|
+
const GetCustomerByRefIdSchema = z.object({
|
|
501
|
+
reference_id: z.string()
|
|
502
|
+
});
|
|
503
|
+
z.object({
|
|
504
|
+
data: z.array(CustomerResourceSchema),
|
|
505
|
+
hasMore: z.boolean()
|
|
506
|
+
});
|
|
507
|
+
const UpdateParamsSchema = z.object({
|
|
508
|
+
id: z.string(),
|
|
509
|
+
payload: CommonCustomerResourceSchema
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const createCustomer = async (params, axiosInstance, config)=>{
|
|
513
|
+
try {
|
|
514
|
+
const validatedParams = validateInput(CustomerSchema, params, "customer params");
|
|
515
|
+
const response = await axiosInstance.post(config?.url ?? "/customers", validatedParams, config);
|
|
516
|
+
// Note: Actual API response might not match discriminated union exactly
|
|
517
|
+
// For production use, consider making the schema more flexible
|
|
518
|
+
return response.data;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
521
|
+
handleAxiosError(error);
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const getCustomerId = async (params, axiosInstance, config)=>{
|
|
527
|
+
try {
|
|
528
|
+
const validatedParams = validateInput(GetCustomerSchema, params, "get customer params");
|
|
529
|
+
const response = await axiosInstance.get(config?.url ?? `/customers/${validatedParams.id}`, config);
|
|
530
|
+
// Note: Actual API response might not match discriminated union exactly
|
|
531
|
+
return response.data;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
534
|
+
handleAxiosError(error);
|
|
535
|
+
}
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const getCustomerRefId = async (params, axiosInstance, config)=>{
|
|
540
|
+
try {
|
|
541
|
+
const validatedParams = validateInput(GetCustomerByRefIdSchema, params, "get customer by ref params");
|
|
542
|
+
const response = await axiosInstance.get(config?.url ?? `/customers?reference_id=${validatedParams.reference_id}`, config);
|
|
543
|
+
// Note: Actual API response might not match discriminated union exactly
|
|
544
|
+
return response.data;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
547
|
+
handleAxiosError(error);
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const updateCustomer = async (params, axiosInstance, config)=>{
|
|
553
|
+
try {
|
|
554
|
+
const validatedParams = validateInput(UpdateParamsSchema, params, "update customer params");
|
|
555
|
+
const response = await axiosInstance.patch(config?.url ?? `/customers/${validatedParams.id}`, validatedParams.payload, config);
|
|
556
|
+
// Note: Actual API response might not match discriminated union exactly
|
|
557
|
+
return response.data;
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
560
|
+
handleAxiosError(error);
|
|
561
|
+
}
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const createEwalletCharge = async (params, axiosInstance, config)=>(await axiosInstance.post(config?.url ?? "/ewallets/charges", params, config)).data;
|
|
567
|
+
const getEwalletCharge = async (params, axiosInstance, config)=>(await axiosInstance.get(config?.url ?? `/ewallets/charges/${params.id}`, config)).data;
|
|
568
|
+
|
|
569
|
+
// Payment Method Types
|
|
570
|
+
const PaymentMethodTypeSchema = z.union([
|
|
571
|
+
z.literal("CARD"),
|
|
572
|
+
z.literal("BANK_ACCOUNT"),
|
|
573
|
+
z.literal("EWALLET"),
|
|
574
|
+
z.literal("OVER_THE_COUNTER"),
|
|
575
|
+
z.literal("VIRTUAL_ACCOUNT"),
|
|
576
|
+
z.literal("QR_CODE")
|
|
577
|
+
]);
|
|
578
|
+
// Payment Method Status
|
|
579
|
+
const PaymentMethodStatusSchema = z.union([
|
|
580
|
+
z.literal("ACTIVE"),
|
|
581
|
+
z.literal("INACTIVE"),
|
|
582
|
+
z.literal("PENDING"),
|
|
583
|
+
z.literal("EXPIRED"),
|
|
584
|
+
z.literal("FAILED")
|
|
585
|
+
]);
|
|
586
|
+
// Card Properties
|
|
587
|
+
const CardPropertiesSchema = z.object({
|
|
588
|
+
card_last_four: z.string(),
|
|
589
|
+
card_expiry_month: z.string(),
|
|
590
|
+
card_expiry_year: z.string(),
|
|
591
|
+
network: z.string(),
|
|
592
|
+
country: CountrySchema.optional(),
|
|
593
|
+
issuer: z.string().optional(),
|
|
594
|
+
type: z.union([
|
|
595
|
+
z.literal("CREDIT"),
|
|
596
|
+
z.literal("DEBIT")
|
|
597
|
+
]).optional(),
|
|
598
|
+
currency: CurrencySchema.optional()
|
|
599
|
+
});
|
|
600
|
+
// Bank Account Properties
|
|
601
|
+
const BankAccountPropertiesSchema = z.object({
|
|
602
|
+
account_number: z.string(),
|
|
603
|
+
account_holder_name: z.string(),
|
|
604
|
+
bank_code: z.string(),
|
|
605
|
+
account_type: z.string().optional(),
|
|
606
|
+
currency: CurrencySchema
|
|
607
|
+
});
|
|
608
|
+
// E-wallet Properties
|
|
609
|
+
const EwalletPropertiesSchema = z.object({
|
|
610
|
+
account_details: z.string(),
|
|
611
|
+
currency: CurrencySchema
|
|
612
|
+
});
|
|
613
|
+
// Payment Method Properties (Discriminated Union)
|
|
614
|
+
z.discriminatedUnion("type", [
|
|
615
|
+
z.object({
|
|
616
|
+
type: z.literal("CARD"),
|
|
617
|
+
card: CardPropertiesSchema
|
|
618
|
+
}),
|
|
619
|
+
z.object({
|
|
620
|
+
type: z.literal("BANK_ACCOUNT"),
|
|
621
|
+
bank_account: BankAccountPropertiesSchema
|
|
622
|
+
}),
|
|
623
|
+
z.object({
|
|
624
|
+
type: z.literal("EWALLET"),
|
|
625
|
+
ewallet: EwalletPropertiesSchema
|
|
626
|
+
})
|
|
627
|
+
]);
|
|
628
|
+
// Create Payment Method Request
|
|
629
|
+
const CreatePaymentMethodSchema = z.object({
|
|
630
|
+
type: PaymentMethodTypeSchema,
|
|
631
|
+
country: CountrySchema.optional(),
|
|
632
|
+
reusability: z.union([
|
|
633
|
+
z.literal("ONE_TIME_USE"),
|
|
634
|
+
z.literal("MULTIPLE_USE")
|
|
635
|
+
]),
|
|
636
|
+
description: z.string().optional(),
|
|
637
|
+
reference_id: z.string().optional(),
|
|
638
|
+
metadata: z.record(z.unknown()).optional(),
|
|
639
|
+
// Specific properties based on type
|
|
640
|
+
card: z.object({
|
|
641
|
+
currency: CurrencySchema.optional(),
|
|
642
|
+
channel_properties: z.object({
|
|
643
|
+
success_return_url: z.string().url().optional(),
|
|
644
|
+
failure_return_url: z.string().url().optional()
|
|
645
|
+
}).optional()
|
|
646
|
+
}).optional(),
|
|
647
|
+
bank_account: z.object({
|
|
648
|
+
currency: CurrencySchema,
|
|
649
|
+
channel_properties: z.object({
|
|
650
|
+
account_mobile_number: z.string().optional(),
|
|
651
|
+
card_last_four: z.string().optional(),
|
|
652
|
+
card_expiry_month: z.string().optional(),
|
|
653
|
+
card_expiry_year: z.string().optional(),
|
|
654
|
+
account_email: z.string().email().optional()
|
|
655
|
+
}).optional()
|
|
656
|
+
}).optional(),
|
|
657
|
+
ewallet: z.object({
|
|
658
|
+
channel_code: z.string(),
|
|
659
|
+
channel_properties: z.object({
|
|
660
|
+
success_return_url: z.string().url().optional(),
|
|
661
|
+
failure_return_url: z.string().url().optional(),
|
|
662
|
+
cancel_return_url: z.string().url().optional()
|
|
663
|
+
}).optional()
|
|
664
|
+
}).optional()
|
|
665
|
+
});
|
|
666
|
+
// Update Payment Method Request
|
|
667
|
+
const UpdatePaymentMethodSchema = z.object({
|
|
668
|
+
description: z.string().optional(),
|
|
669
|
+
reference_id: z.string().optional(),
|
|
670
|
+
status: PaymentMethodStatusSchema.optional(),
|
|
671
|
+
metadata: z.record(z.unknown()).optional()
|
|
672
|
+
});
|
|
673
|
+
// Payment Method Resource
|
|
674
|
+
const PaymentMethodResourceSchema = z.object({
|
|
675
|
+
id: z.string(),
|
|
676
|
+
type: PaymentMethodTypeSchema,
|
|
677
|
+
country: CountrySchema.optional(),
|
|
678
|
+
business_id: z.string(),
|
|
679
|
+
customer_id: z.string().optional(),
|
|
680
|
+
reference_id: z.string().optional(),
|
|
681
|
+
description: z.string().optional(),
|
|
682
|
+
status: PaymentMethodStatusSchema,
|
|
683
|
+
reusability: z.union([
|
|
684
|
+
z.literal("ONE_TIME_USE"),
|
|
685
|
+
z.literal("MULTIPLE_USE")
|
|
686
|
+
]),
|
|
687
|
+
actions: z.array(z.object({
|
|
688
|
+
action: z.string(),
|
|
689
|
+
url: z.string().url().optional(),
|
|
690
|
+
url_type: z.string().optional(),
|
|
691
|
+
method: z.string().optional()
|
|
692
|
+
})).optional(),
|
|
693
|
+
metadata: z.record(z.unknown()).optional(),
|
|
694
|
+
billing_information: z.object({
|
|
695
|
+
country: CountrySchema.optional(),
|
|
696
|
+
street_line1: z.string().optional(),
|
|
697
|
+
street_line2: z.string().optional(),
|
|
698
|
+
city: z.string().optional(),
|
|
699
|
+
province_state: z.string().optional(),
|
|
700
|
+
postal_code: z.string().optional()
|
|
701
|
+
}).optional(),
|
|
702
|
+
failure_code: z.string().nullable().optional(),
|
|
703
|
+
created: z.string().datetime(),
|
|
704
|
+
updated: z.string().datetime(),
|
|
705
|
+
// Type-specific properties
|
|
706
|
+
card: CardPropertiesSchema.optional(),
|
|
707
|
+
bank_account: BankAccountPropertiesSchema.optional(),
|
|
708
|
+
ewallet: EwalletPropertiesSchema.optional()
|
|
709
|
+
});
|
|
710
|
+
// Get Payment Method Request
|
|
711
|
+
const GetPaymentMethodSchema = z.object({
|
|
712
|
+
id: z.string()
|
|
713
|
+
});
|
|
714
|
+
// List Payment Methods Request
|
|
715
|
+
const ListPaymentMethodsSchema = z.object({
|
|
716
|
+
id: z.array(z.string()).optional(),
|
|
717
|
+
type: z.array(PaymentMethodTypeSchema).optional(),
|
|
718
|
+
status: z.array(PaymentMethodStatusSchema).optional(),
|
|
719
|
+
reusability: z.union([
|
|
720
|
+
z.literal("ONE_TIME_USE"),
|
|
721
|
+
z.literal("MULTIPLE_USE")
|
|
722
|
+
]).optional(),
|
|
723
|
+
customer_id: z.string().optional(),
|
|
724
|
+
reference_id: z.string().optional(),
|
|
725
|
+
after_id: z.string().optional(),
|
|
726
|
+
before_id: z.string().optional(),
|
|
727
|
+
limit: z.number().min(1).max(100).default(10).optional()
|
|
728
|
+
});
|
|
729
|
+
// List Payment Methods Response
|
|
730
|
+
z.object({
|
|
731
|
+
data: z.array(PaymentMethodResourceSchema),
|
|
732
|
+
has_more: z.boolean(),
|
|
733
|
+
links: z.object({
|
|
734
|
+
href: z.string(),
|
|
735
|
+
rel: z.string(),
|
|
736
|
+
method: z.string()
|
|
737
|
+
}).array()
|
|
738
|
+
});
|
|
739
|
+
// Update Payment Method Params
|
|
740
|
+
const UpdatePaymentMethodParamsSchema = z.object({
|
|
741
|
+
id: z.string(),
|
|
742
|
+
payload: UpdatePaymentMethodSchema
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const createPaymentMethod = async (params, axiosInstance, config)=>{
|
|
746
|
+
try {
|
|
747
|
+
const validatedParams = validateInput(CreatePaymentMethodSchema, params, "payment method params");
|
|
748
|
+
const response = await axiosInstance.post(config?.url ?? "/v2/payment_methods", validatedParams, config);
|
|
749
|
+
// Note: Actual API response handling - relaxed validation for production flexibility
|
|
750
|
+
return response.data;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
753
|
+
handleAxiosError(error);
|
|
754
|
+
}
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
const getPaymentMethod = async (params, axiosInstance, config)=>{
|
|
759
|
+
try {
|
|
760
|
+
const validatedParams = validateInput(GetPaymentMethodSchema, params, "get payment method params");
|
|
761
|
+
const response = await axiosInstance.get(config?.url ?? `/v2/payment_methods/${validatedParams.id}`, config);
|
|
762
|
+
return response.data;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
765
|
+
handleAxiosError(error);
|
|
766
|
+
}
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const listPaymentMethods = async (params, axiosInstance, config)=>{
|
|
771
|
+
try {
|
|
772
|
+
const validatedParams = params ? validateInput(ListPaymentMethodsSchema, params, "list payment methods params") : {};
|
|
773
|
+
const queryParams = new URLSearchParams();
|
|
774
|
+
if (validatedParams.id) {
|
|
775
|
+
validatedParams.id.forEach((id)=>queryParams.append("id[]", id));
|
|
776
|
+
}
|
|
777
|
+
if (validatedParams.type) {
|
|
778
|
+
validatedParams.type.forEach((type)=>queryParams.append("type[]", type));
|
|
779
|
+
}
|
|
780
|
+
if (validatedParams.status) {
|
|
781
|
+
validatedParams.status.forEach((status)=>queryParams.append("status[]", status));
|
|
782
|
+
}
|
|
783
|
+
if (validatedParams.reusability) {
|
|
784
|
+
queryParams.append("reusability", validatedParams.reusability);
|
|
785
|
+
}
|
|
786
|
+
if (validatedParams.customer_id) {
|
|
787
|
+
queryParams.append("customer_id", validatedParams.customer_id);
|
|
788
|
+
}
|
|
789
|
+
if (validatedParams.reference_id) {
|
|
790
|
+
queryParams.append("reference_id", validatedParams.reference_id);
|
|
791
|
+
}
|
|
792
|
+
if (validatedParams.after_id) {
|
|
793
|
+
queryParams.append("after_id", validatedParams.after_id);
|
|
794
|
+
}
|
|
795
|
+
if (validatedParams.before_id) {
|
|
796
|
+
queryParams.append("before_id", validatedParams.before_id);
|
|
797
|
+
}
|
|
798
|
+
if (validatedParams.limit) {
|
|
799
|
+
queryParams.append("limit", validatedParams.limit.toString());
|
|
800
|
+
}
|
|
801
|
+
const queryString = queryParams.toString();
|
|
802
|
+
const url = queryString ? `/v2/payment_methods?${queryString}` : "/v2/payment_methods";
|
|
803
|
+
const response = await axiosInstance.get(config?.url ?? url, config);
|
|
804
|
+
return response.data;
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
807
|
+
handleAxiosError(error);
|
|
808
|
+
}
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const updatePaymentMethod = async (params, axiosInstance, config)=>{
|
|
813
|
+
try {
|
|
814
|
+
const validatedParams = validateInput(UpdatePaymentMethodParamsSchema, params, "update payment method params");
|
|
815
|
+
const response = await axiosInstance.patch(config?.url ?? `/v2/payment_methods/${validatedParams.id}`, validatedParams.payload, config);
|
|
816
|
+
return response.data;
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
819
|
+
handleAxiosError(error);
|
|
820
|
+
}
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// Invoice Status
|
|
826
|
+
const InvoiceStatusSchema = z.union([
|
|
827
|
+
z.literal("PENDING"),
|
|
828
|
+
z.literal("PAID"),
|
|
829
|
+
z.literal("SETTLED"),
|
|
830
|
+
z.literal("EXPIRED")
|
|
831
|
+
]);
|
|
832
|
+
// Payer Email
|
|
833
|
+
z.object({
|
|
834
|
+
email: z.string().email()
|
|
835
|
+
});
|
|
836
|
+
// Invoice Item
|
|
837
|
+
const InvoiceItemSchema = z.object({
|
|
838
|
+
name: z.string(),
|
|
839
|
+
quantity: z.number().positive(),
|
|
840
|
+
price: z.number().positive(),
|
|
841
|
+
category: z.string().optional(),
|
|
842
|
+
url: z.string().url().optional()
|
|
843
|
+
});
|
|
844
|
+
// Customer Notification Preference
|
|
845
|
+
const CustomerNotificationPreferenceSchema = z.object({
|
|
846
|
+
invoice_created: z.array(z.union([
|
|
847
|
+
z.literal("whatsapp"),
|
|
848
|
+
z.literal("sms"),
|
|
849
|
+
z.literal("email")
|
|
850
|
+
])).optional(),
|
|
851
|
+
invoice_reminder: z.array(z.union([
|
|
852
|
+
z.literal("whatsapp"),
|
|
853
|
+
z.literal("sms"),
|
|
854
|
+
z.literal("email")
|
|
855
|
+
])).optional(),
|
|
856
|
+
invoice_paid: z.array(z.union([
|
|
857
|
+
z.literal("whatsapp"),
|
|
858
|
+
z.literal("sms"),
|
|
859
|
+
z.literal("email")
|
|
860
|
+
])).optional(),
|
|
861
|
+
invoice_expired: z.array(z.union([
|
|
862
|
+
z.literal("whatsapp"),
|
|
863
|
+
z.literal("sms"),
|
|
864
|
+
z.literal("email")
|
|
865
|
+
])).optional()
|
|
866
|
+
});
|
|
867
|
+
// Customer Details
|
|
868
|
+
const CustomerDetailsSchema = z.object({
|
|
869
|
+
customer_name: z.string().optional(),
|
|
870
|
+
customer_email: z.string().email().optional(),
|
|
871
|
+
customer_phone: z.string().optional(),
|
|
872
|
+
billing_address: z.object({
|
|
873
|
+
first_name: z.string().optional(),
|
|
874
|
+
last_name: z.string().optional(),
|
|
875
|
+
address: z.string().optional(),
|
|
876
|
+
city: z.string().optional(),
|
|
877
|
+
postal_code: z.string().optional(),
|
|
878
|
+
phone: z.string().optional(),
|
|
879
|
+
country_code: CountrySchema.optional()
|
|
880
|
+
}).optional(),
|
|
881
|
+
shipping_address: z.object({
|
|
882
|
+
first_name: z.string().optional(),
|
|
883
|
+
last_name: z.string().optional(),
|
|
884
|
+
address: z.string().optional(),
|
|
885
|
+
city: z.string().optional(),
|
|
886
|
+
postal_code: z.string().optional(),
|
|
887
|
+
phone: z.string().optional(),
|
|
888
|
+
country_code: CountrySchema.optional()
|
|
889
|
+
}).optional()
|
|
890
|
+
});
|
|
891
|
+
// Fee Details
|
|
892
|
+
const FeeSchema = z.object({
|
|
893
|
+
type: z.string(),
|
|
894
|
+
value: z.number()
|
|
895
|
+
});
|
|
896
|
+
// Available Bank and E-wallet
|
|
897
|
+
const AvailableBankSchema = z.object({
|
|
898
|
+
bank_code: z.string(),
|
|
899
|
+
collection_type: z.string(),
|
|
900
|
+
bank_branch: z.string(),
|
|
901
|
+
transfer_amount: z.number(),
|
|
902
|
+
bank_account_number: z.string(),
|
|
903
|
+
account_holder_name: z.string(),
|
|
904
|
+
identity_amount: z.number().optional()
|
|
905
|
+
});
|
|
906
|
+
const AvailableEwalletSchema = z.object({
|
|
907
|
+
ewallet_type: z.string()
|
|
908
|
+
});
|
|
909
|
+
const AvailableRetailOutletSchema = z.object({
|
|
910
|
+
retail_outlet_name: z.string()
|
|
911
|
+
});
|
|
912
|
+
// Create Invoice Schema
|
|
913
|
+
const CreateInvoiceSchema = z.object({
|
|
914
|
+
external_id: z.string(),
|
|
915
|
+
payer_email: z.string().email(),
|
|
916
|
+
description: z.string(),
|
|
917
|
+
amount: z.number().positive(),
|
|
918
|
+
invoice_duration: z.number().positive().optional(),
|
|
919
|
+
callback_virtual_account_id: z.string().optional(),
|
|
920
|
+
should_exclude_credit_card: z.boolean().optional(),
|
|
921
|
+
should_send_email: z.boolean().optional(),
|
|
922
|
+
customer_name: z.string().optional(),
|
|
923
|
+
customer_email: z.string().email().optional(),
|
|
924
|
+
customer_phone: z.string().optional(),
|
|
925
|
+
customer: CustomerDetailsSchema.optional(),
|
|
926
|
+
customer_notification_preference: CustomerNotificationPreferenceSchema.optional(),
|
|
927
|
+
success_redirect_url: z.string().url().optional(),
|
|
928
|
+
failure_redirect_url: z.string().url().optional(),
|
|
929
|
+
payment_methods: z.array(z.string()).optional(),
|
|
930
|
+
mid_label: z.string().optional(),
|
|
931
|
+
should_authenticate_credit_card: z.boolean().optional(),
|
|
932
|
+
currency: CurrencySchema.optional(),
|
|
933
|
+
items: z.array(InvoiceItemSchema).optional(),
|
|
934
|
+
fixed_va: z.boolean().optional(),
|
|
935
|
+
reminder_time_unit: z.union([
|
|
936
|
+
z.literal("days"),
|
|
937
|
+
z.literal("hours"),
|
|
938
|
+
z.literal("minutes")
|
|
939
|
+
]).optional(),
|
|
940
|
+
reminder_time: z.number().optional(),
|
|
941
|
+
locale: z.string().optional(),
|
|
942
|
+
fees: z.array(FeeSchema).optional(),
|
|
943
|
+
metadata: z.record(z.unknown()).optional()
|
|
944
|
+
});
|
|
945
|
+
// Update Invoice Schema
|
|
946
|
+
const UpdateInvoiceSchema = z.object({
|
|
947
|
+
should_send_email: z.boolean().optional(),
|
|
948
|
+
customer_name: z.string().optional(),
|
|
949
|
+
customer_email: z.string().email().optional(),
|
|
950
|
+
customer_phone: z.string().optional(),
|
|
951
|
+
customer: CustomerDetailsSchema.optional(),
|
|
952
|
+
customer_notification_preference: CustomerNotificationPreferenceSchema.optional(),
|
|
953
|
+
success_redirect_url: z.string().url().optional(),
|
|
954
|
+
failure_redirect_url: z.string().url().optional(),
|
|
955
|
+
items: z.array(InvoiceItemSchema).optional(),
|
|
956
|
+
metadata: z.record(z.unknown()).optional()
|
|
957
|
+
});
|
|
958
|
+
// Invoice Resource
|
|
959
|
+
const InvoiceResourceSchema = z.object({
|
|
960
|
+
id: z.string(),
|
|
961
|
+
external_id: z.string(),
|
|
962
|
+
user_id: z.string(),
|
|
963
|
+
status: InvoiceStatusSchema,
|
|
964
|
+
merchant_name: z.string(),
|
|
965
|
+
merchant_profile_picture_url: z.string().url(),
|
|
966
|
+
amount: z.number(),
|
|
967
|
+
payer_email: z.string().email(),
|
|
968
|
+
description: z.string(),
|
|
969
|
+
expiry_date: z.string().datetime(),
|
|
970
|
+
invoice_url: z.string().url(),
|
|
971
|
+
should_exclude_credit_card: z.boolean(),
|
|
972
|
+
should_send_email: z.boolean(),
|
|
973
|
+
created: z.string().datetime(),
|
|
974
|
+
updated: z.string().datetime(),
|
|
975
|
+
currency: CurrencySchema,
|
|
976
|
+
paid_amount: z.number().optional(),
|
|
977
|
+
credit_card_charge_id: z.string().optional(),
|
|
978
|
+
payment_method: z.string().optional(),
|
|
979
|
+
payment_channel: z.string().optional(),
|
|
980
|
+
payment_destination: z.string().optional(),
|
|
981
|
+
payment_id: z.string().optional(),
|
|
982
|
+
paid_at: z.string().datetime().optional(),
|
|
983
|
+
bank_code: z.string().optional(),
|
|
984
|
+
ewallet_type: z.string().optional(),
|
|
985
|
+
on_demand_link: z.string().url().optional(),
|
|
986
|
+
recurring_payment_id: z.string().optional(),
|
|
987
|
+
// Customer information
|
|
988
|
+
customer_name: z.string().optional(),
|
|
989
|
+
customer_email: z.string().email().optional(),
|
|
990
|
+
customer_phone: z.string().optional(),
|
|
991
|
+
customer: CustomerDetailsSchema.optional(),
|
|
992
|
+
customer_notification_preference: CustomerNotificationPreferenceSchema.optional(),
|
|
993
|
+
// URLs
|
|
994
|
+
success_redirect_url: z.string().url().optional(),
|
|
995
|
+
failure_redirect_url: z.string().url().optional(),
|
|
996
|
+
// Items and fees
|
|
997
|
+
items: z.array(InvoiceItemSchema).optional(),
|
|
998
|
+
fees: z.array(FeeSchema).optional(),
|
|
999
|
+
// Available payment methods
|
|
1000
|
+
available_banks: z.array(AvailableBankSchema).optional(),
|
|
1001
|
+
available_ewallets: z.array(AvailableEwalletSchema).optional(),
|
|
1002
|
+
available_retail_outlets: z.array(AvailableRetailOutletSchema).optional(),
|
|
1003
|
+
available_paylaters: z.array(z.object({
|
|
1004
|
+
paylater_type: z.string()
|
|
1005
|
+
})).optional(),
|
|
1006
|
+
available_qr_codes: z.array(z.object({
|
|
1007
|
+
qr_code_type: z.string()
|
|
1008
|
+
})).optional(),
|
|
1009
|
+
available_direct_debits: z.array(z.object({
|
|
1010
|
+
direct_debit_type: z.string()
|
|
1011
|
+
})).optional(),
|
|
1012
|
+
should_authenticate_credit_card: z.boolean().optional(),
|
|
1013
|
+
metadata: z.record(z.unknown()).optional()
|
|
1014
|
+
});
|
|
1015
|
+
// Get Invoice Schema
|
|
1016
|
+
const GetInvoiceSchema = z.object({
|
|
1017
|
+
invoice_id: z.string()
|
|
1018
|
+
});
|
|
1019
|
+
// List Invoices Schema
|
|
1020
|
+
const ListInvoicesSchema = z.object({
|
|
1021
|
+
statuses: z.array(InvoiceStatusSchema).optional(),
|
|
1022
|
+
limit: z.number().min(1).max(100).optional(),
|
|
1023
|
+
created_after: z.string().datetime().optional(),
|
|
1024
|
+
created_before: z.string().datetime().optional(),
|
|
1025
|
+
paid_after: z.string().datetime().optional(),
|
|
1026
|
+
paid_before: z.string().datetime().optional(),
|
|
1027
|
+
expired_after: z.string().datetime().optional(),
|
|
1028
|
+
expired_before: z.string().datetime().optional(),
|
|
1029
|
+
last_invoice: z.string().optional(),
|
|
1030
|
+
client_types: z.array(z.string()).optional(),
|
|
1031
|
+
payment_channels: z.array(z.string()).optional(),
|
|
1032
|
+
on_demand_link: z.string().optional(),
|
|
1033
|
+
recurring_payment_id: z.string().optional()
|
|
1034
|
+
});
|
|
1035
|
+
// List Invoices Response
|
|
1036
|
+
z.object({
|
|
1037
|
+
has_more: z.boolean(),
|
|
1038
|
+
data: z.array(InvoiceResourceSchema)
|
|
1039
|
+
});
|
|
1040
|
+
// Update Invoice Params
|
|
1041
|
+
const UpdateInvoiceParamsSchema = z.object({
|
|
1042
|
+
invoice_id: z.string(),
|
|
1043
|
+
payload: UpdateInvoiceSchema
|
|
1044
|
+
});
|
|
1045
|
+
// Expire Invoice Schema
|
|
1046
|
+
const ExpireInvoiceSchema = z.object({
|
|
1047
|
+
invoice_id: z.string()
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
const createInvoice = async (params, axiosInstance, config)=>{
|
|
1051
|
+
try {
|
|
1052
|
+
const validatedParams = validateInput(CreateInvoiceSchema, params, "invoice params");
|
|
1053
|
+
const response = await axiosInstance.post(config?.url ?? "/v2/invoices", validatedParams, config);
|
|
1054
|
+
return response.data;
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
1057
|
+
handleAxiosError(error);
|
|
1058
|
+
}
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const getInvoice = async (params, axiosInstance, config)=>{
|
|
1063
|
+
try {
|
|
1064
|
+
const validatedParams = validateInput(GetInvoiceSchema, params, "get invoice params");
|
|
1065
|
+
const response = await axiosInstance.get(config?.url ?? `/v2/invoices/${validatedParams.invoice_id}`, config);
|
|
1066
|
+
return response.data;
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
1069
|
+
handleAxiosError(error);
|
|
1070
|
+
}
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const listInvoices = async (params, axiosInstance, config)=>{
|
|
1075
|
+
try {
|
|
1076
|
+
const validatedParams = params ? validateInput(ListInvoicesSchema, params, "list invoices params") : {};
|
|
1077
|
+
const queryParams = new URLSearchParams();
|
|
1078
|
+
if (validatedParams.statuses) {
|
|
1079
|
+
validatedParams.statuses.forEach((status)=>queryParams.append("statuses[]", status));
|
|
1080
|
+
}
|
|
1081
|
+
if (validatedParams.limit) {
|
|
1082
|
+
queryParams.append("limit", validatedParams.limit.toString());
|
|
1083
|
+
}
|
|
1084
|
+
if (validatedParams.created_after) {
|
|
1085
|
+
queryParams.append("created_after", validatedParams.created_after);
|
|
1086
|
+
}
|
|
1087
|
+
if (validatedParams.created_before) {
|
|
1088
|
+
queryParams.append("created_before", validatedParams.created_before);
|
|
1089
|
+
}
|
|
1090
|
+
if (validatedParams.paid_after) {
|
|
1091
|
+
queryParams.append("paid_after", validatedParams.paid_after);
|
|
1092
|
+
}
|
|
1093
|
+
if (validatedParams.paid_before) {
|
|
1094
|
+
queryParams.append("paid_before", validatedParams.paid_before);
|
|
1095
|
+
}
|
|
1096
|
+
if (validatedParams.expired_after) {
|
|
1097
|
+
queryParams.append("expired_after", validatedParams.expired_after);
|
|
1098
|
+
}
|
|
1099
|
+
if (validatedParams.expired_before) {
|
|
1100
|
+
queryParams.append("expired_before", validatedParams.expired_before);
|
|
1101
|
+
}
|
|
1102
|
+
if (validatedParams.last_invoice) {
|
|
1103
|
+
queryParams.append("last_invoice", validatedParams.last_invoice);
|
|
1104
|
+
}
|
|
1105
|
+
if (validatedParams.client_types) {
|
|
1106
|
+
validatedParams.client_types.forEach((type)=>queryParams.append("client_types[]", type));
|
|
1107
|
+
}
|
|
1108
|
+
if (validatedParams.payment_channels) {
|
|
1109
|
+
validatedParams.payment_channels.forEach((channel)=>queryParams.append("payment_channels[]", channel));
|
|
1110
|
+
}
|
|
1111
|
+
if (validatedParams.on_demand_link) {
|
|
1112
|
+
queryParams.append("on_demand_link", validatedParams.on_demand_link);
|
|
1113
|
+
}
|
|
1114
|
+
if (validatedParams.recurring_payment_id) {
|
|
1115
|
+
queryParams.append("recurring_payment_id", validatedParams.recurring_payment_id);
|
|
1116
|
+
}
|
|
1117
|
+
const queryString = queryParams.toString();
|
|
1118
|
+
const url = queryString ? `/v2/invoices?${queryString}` : "/v2/invoices";
|
|
1119
|
+
const response = await axiosInstance.get(config?.url ?? url, config);
|
|
1120
|
+
return response.data;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
1123
|
+
handleAxiosError(error);
|
|
1124
|
+
}
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const updateInvoice = async (params, axiosInstance, config)=>{
|
|
1129
|
+
try {
|
|
1130
|
+
const validatedParams = validateInput(UpdateInvoiceParamsSchema, params, "update invoice params");
|
|
1131
|
+
const response = await axiosInstance.patch(config?.url ?? `/v2/invoices/${validatedParams.invoice_id}`, validatedParams.payload, config);
|
|
1132
|
+
return response.data;
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
1135
|
+
handleAxiosError(error);
|
|
1136
|
+
}
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
const expireInvoice = async (params, axiosInstance, config)=>{
|
|
1141
|
+
try {
|
|
1142
|
+
const validatedParams = validateInput(ExpireInvoiceSchema, params, "expire invoice params");
|
|
1143
|
+
const response = await axiosInstance.post(config?.url ?? `/invoices/${validatedParams.invoice_id}/expire`, {}, config);
|
|
1144
|
+
return response.data;
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (error instanceof Error && error.name === "AxiosError") {
|
|
1147
|
+
handleAxiosError(error);
|
|
1148
|
+
}
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const btoa = (string)=>{
|
|
1154
|
+
if (typeof window === "undefined") {
|
|
1155
|
+
return Buffer.from(string).toString("base64");
|
|
1156
|
+
}
|
|
1157
|
+
return window.btoa(string);
|
|
1158
|
+
};
|
|
1159
|
+
const createFn = (fn, axiosInstance)=>{
|
|
1160
|
+
return (data)=>fn(data, axiosInstance);
|
|
1161
|
+
};
|
|
1162
|
+
const Xendit = (key, options = {})=>{
|
|
1163
|
+
const axiosInstance = createAxiosInstance({
|
|
1164
|
+
headers: {
|
|
1165
|
+
Authorization: `Basic ${btoa(key + ":")}`
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
// Apply rate limiting if configured
|
|
1169
|
+
if (options.rateLimit) {
|
|
1170
|
+
setupRateLimit(axiosInstance, options.rateLimit);
|
|
1171
|
+
}
|
|
1172
|
+
if (key.includes("development")) {
|
|
1173
|
+
console.log("👾 You are on → TEST MODE");
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
customer: {
|
|
1177
|
+
create: createFn(createCustomer, axiosInstance),
|
|
1178
|
+
getById: createFn(getCustomerId, axiosInstance),
|
|
1179
|
+
getByRefId: createFn(getCustomerRefId, axiosInstance),
|
|
1180
|
+
update: createFn(updateCustomer, axiosInstance)
|
|
1181
|
+
},
|
|
1182
|
+
ewallet: {
|
|
1183
|
+
charge: createFn(createEwalletCharge, axiosInstance),
|
|
1184
|
+
get: createFn(getEwalletCharge, axiosInstance)
|
|
1185
|
+
},
|
|
1186
|
+
paymentMethod: {
|
|
1187
|
+
create: createFn(createPaymentMethod, axiosInstance),
|
|
1188
|
+
get: createFn(getPaymentMethod, axiosInstance),
|
|
1189
|
+
list: (params)=>listPaymentMethods(params, axiosInstance),
|
|
1190
|
+
update: createFn(updatePaymentMethod, axiosInstance)
|
|
1191
|
+
},
|
|
1192
|
+
invoice: {
|
|
1193
|
+
create: createFn(createInvoice, axiosInstance),
|
|
1194
|
+
get: createFn(getInvoice, axiosInstance),
|
|
1195
|
+
list: (params)=>listInvoices(params, axiosInstance),
|
|
1196
|
+
update: createFn(updateInvoice, axiosInstance),
|
|
1197
|
+
expire: createFn(expireInvoice, axiosInstance)
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// Type guard for phone numbers
|
|
1203
|
+
function isValidPhone(value) {
|
|
1204
|
+
return typeof value === "string" && value.startsWith("+") && value.length >= 8 && value.length <= 15;
|
|
1205
|
+
}
|
|
1206
|
+
// Type guard for country codes
|
|
1207
|
+
function isValidCountry(value) {
|
|
1208
|
+
return [
|
|
1209
|
+
"PH",
|
|
1210
|
+
"ID",
|
|
1211
|
+
"MY",
|
|
1212
|
+
"TH",
|
|
1213
|
+
"VN"
|
|
1214
|
+
].includes(value);
|
|
1215
|
+
}
|
|
1216
|
+
// Type guard for currency codes
|
|
1217
|
+
function isValidCurrency(value) {
|
|
1218
|
+
return [
|
|
1219
|
+
"PHP",
|
|
1220
|
+
"IDR",
|
|
1221
|
+
"MYR",
|
|
1222
|
+
"THB",
|
|
1223
|
+
"VND"
|
|
1224
|
+
].includes(value);
|
|
1225
|
+
}
|
|
1226
|
+
// Type guard for customer type
|
|
1227
|
+
function isValidCustomerType(value) {
|
|
1228
|
+
return [
|
|
1229
|
+
"INDIVIDUAL",
|
|
1230
|
+
"BUSINESS"
|
|
1231
|
+
].includes(value);
|
|
1232
|
+
}
|
|
1233
|
+
// Type guard for checkout method
|
|
1234
|
+
function isValidCheckoutMethod(value) {
|
|
1235
|
+
return [
|
|
1236
|
+
"ONE_TIME_PAYMENT",
|
|
1237
|
+
"TOKENIZED_PAYMENT"
|
|
1238
|
+
].includes(value);
|
|
1239
|
+
}
|
|
1240
|
+
// Generic type guard for checking if value is not null or undefined
|
|
1241
|
+
function isNotNullOrUndefined(value) {
|
|
1242
|
+
return value !== null && value !== undefined;
|
|
1243
|
+
}
|
|
1244
|
+
// Type guard for checking if value is a valid URL
|
|
1245
|
+
function isValidUrl(value) {
|
|
1246
|
+
try {
|
|
1247
|
+
new URL(value);
|
|
1248
|
+
return true;
|
|
1249
|
+
} catch {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// Type guard for checking if value is a valid email
|
|
1254
|
+
function isValidEmail(value) {
|
|
1255
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1256
|
+
return emailRegex.test(value);
|
|
1257
|
+
}
|
|
1258
|
+
// Type guard for checking if value is a valid date string
|
|
1259
|
+
function isValidDateString(value) {
|
|
1260
|
+
const date = new Date(value);
|
|
1261
|
+
return !isNaN(date.getTime()) && value === date.toISOString();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Webhook Event Types
|
|
1265
|
+
const WebhookEventTypeSchema = z.union([
|
|
1266
|
+
z.literal("invoice.paid"),
|
|
1267
|
+
z.literal("invoice.expired"),
|
|
1268
|
+
z.literal("payment.succeeded"),
|
|
1269
|
+
z.literal("payment.failed"),
|
|
1270
|
+
z.literal("ewallet.charge.succeeded"),
|
|
1271
|
+
z.literal("ewallet.charge.pending"),
|
|
1272
|
+
z.literal("ewallet.charge.failed"),
|
|
1273
|
+
z.literal("payment_method.activate"),
|
|
1274
|
+
z.literal("payment_method.expire"),
|
|
1275
|
+
z.literal("customer.created"),
|
|
1276
|
+
z.literal("customer.updated")
|
|
1277
|
+
]);
|
|
1278
|
+
// Base Webhook Event Schema
|
|
1279
|
+
const WebhookEventSchema = z.object({
|
|
1280
|
+
id: z.string(),
|
|
1281
|
+
event: WebhookEventTypeSchema,
|
|
1282
|
+
api_version: z.string(),
|
|
1283
|
+
created: z.string().datetime(),
|
|
1284
|
+
business_id: z.string(),
|
|
1285
|
+
data: z.record(z.unknown())
|
|
1286
|
+
});
|
|
1287
|
+
/**
|
|
1288
|
+
* Verify webhook signature from Xendit
|
|
1289
|
+
* @param options Verification options
|
|
1290
|
+
* @returns true if signature is valid, false otherwise
|
|
1291
|
+
*/ function verifyWebhookSignature(options) {
|
|
1292
|
+
const { callbackToken, receivedToken } = options;
|
|
1293
|
+
// Simple token comparison for Xendit webhooks
|
|
1294
|
+
return callbackToken === receivedToken;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Advanced webhook signature verification using HMAC
|
|
1298
|
+
* @param options Verification options with HMAC
|
|
1299
|
+
* @returns true if signature is valid, false otherwise
|
|
1300
|
+
*/ function verifyWebhookHmac(options) {
|
|
1301
|
+
const { secret, requestBody, signature } = options;
|
|
1302
|
+
const body = typeof requestBody === "string" ? requestBody : requestBody.toString("utf8");
|
|
1303
|
+
const expectedSignature = createHmac("sha256", secret).update(body).digest("hex");
|
|
1304
|
+
return `sha256=${expectedSignature}` === signature;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Parse and validate webhook event
|
|
1308
|
+
* @param rawEvent Raw webhook event data
|
|
1309
|
+
* @returns Parsed and validated webhook event
|
|
1310
|
+
*/ function parseWebhookEvent(rawEvent) {
|
|
1311
|
+
const result = WebhookEventSchema.safeParse(rawEvent);
|
|
1312
|
+
if (!result.success) {
|
|
1313
|
+
throw new Error(`Invalid webhook event format: ${result.error.message}`);
|
|
1314
|
+
}
|
|
1315
|
+
return result.data;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Handle webhook events with type-safe handlers
|
|
1319
|
+
* @param event Webhook event
|
|
1320
|
+
* @param handlers Event handlers
|
|
1321
|
+
*/ async function handleWebhookEvent(event, handlers) {
|
|
1322
|
+
const handler = handlers[event.event];
|
|
1323
|
+
if (handler) {
|
|
1324
|
+
await handler(event);
|
|
1325
|
+
} else {
|
|
1326
|
+
console.warn(`No handler found for webhook event: ${event.event}`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Create a webhook processor with built-in verification
|
|
1331
|
+
*/ function createWebhookProcessor(options) {
|
|
1332
|
+
return {
|
|
1333
|
+
/**
|
|
1334
|
+
* Process a webhook request
|
|
1335
|
+
*/ async processWebhook (requestBody, headers, handlers) {
|
|
1336
|
+
try {
|
|
1337
|
+
// Verify signature
|
|
1338
|
+
if (options.callbackToken) {
|
|
1339
|
+
const receivedToken = headers["x-callback-token"] || headers["X-Callback-Token"];
|
|
1340
|
+
if (!receivedToken || !verifyWebhookSignature({
|
|
1341
|
+
callbackToken: options.callbackToken,
|
|
1342
|
+
requestBody,
|
|
1343
|
+
receivedToken
|
|
1344
|
+
})) {
|
|
1345
|
+
return {
|
|
1346
|
+
success: false,
|
|
1347
|
+
error: "Invalid webhook signature"
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (options.hmacSecret) {
|
|
1352
|
+
const signature = headers["x-xendit-signature"] || headers["X-Xendit-Signature"];
|
|
1353
|
+
if (!signature || !verifyWebhookHmac({
|
|
1354
|
+
secret: options.hmacSecret,
|
|
1355
|
+
requestBody,
|
|
1356
|
+
signature
|
|
1357
|
+
})) {
|
|
1358
|
+
return {
|
|
1359
|
+
success: false,
|
|
1360
|
+
error: "Invalid HMAC signature"
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
// Parse event
|
|
1365
|
+
const body = typeof requestBody === "string" ? requestBody : requestBody.toString("utf8");
|
|
1366
|
+
const rawEvent = JSON.parse(body);
|
|
1367
|
+
const event = parseWebhookEvent(rawEvent);
|
|
1368
|
+
// Handle event
|
|
1369
|
+
await handleWebhookEvent(event, handlers);
|
|
1370
|
+
return {
|
|
1371
|
+
success: true
|
|
1372
|
+
};
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
return {
|
|
1375
|
+
success: false,
|
|
1376
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Generic pagination response schema
|
|
1384
|
+
const PaginationMetaSchema = z.object({
|
|
1385
|
+
has_more: z.boolean(),
|
|
1386
|
+
after_id: z.string().optional(),
|
|
1387
|
+
before_id: z.string().optional(),
|
|
1388
|
+
total_count: z.number().optional()
|
|
1389
|
+
});
|
|
1390
|
+
const PaginatedResponseSchema = (itemSchema)=>z.object({
|
|
1391
|
+
data: z.array(itemSchema),
|
|
1392
|
+
has_more: z.boolean(),
|
|
1393
|
+
after_id: z.string().optional(),
|
|
1394
|
+
before_id: z.string().optional(),
|
|
1395
|
+
total_count: z.number().optional()
|
|
1396
|
+
});
|
|
1397
|
+
/**
|
|
1398
|
+
* Helper to build pagination query parameters
|
|
1399
|
+
*/ function buildPaginationParams(options) {
|
|
1400
|
+
const params = {};
|
|
1401
|
+
if (options.limit) {
|
|
1402
|
+
params.limit = options.limit.toString();
|
|
1403
|
+
}
|
|
1404
|
+
if (options.after_id) {
|
|
1405
|
+
params.after_id = options.after_id;
|
|
1406
|
+
}
|
|
1407
|
+
if (options.before_id) {
|
|
1408
|
+
params.before_id = options.before_id;
|
|
1409
|
+
}
|
|
1410
|
+
return params;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Generic paginated API fetcher
|
|
1414
|
+
*/ async function fetchPaginated(axiosInstance, endpoint, itemSchema, options = {}) {
|
|
1415
|
+
const params = buildPaginationParams(options);
|
|
1416
|
+
const response = await axiosInstance.get(endpoint, {
|
|
1417
|
+
params
|
|
1418
|
+
});
|
|
1419
|
+
const paginatedSchema = PaginatedResponseSchema(itemSchema);
|
|
1420
|
+
const result = paginatedSchema.parse(response.data);
|
|
1421
|
+
return result;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Auto-paginate through all pages and return all items
|
|
1425
|
+
*/ async function fetchAllPages(axiosInstance, endpoint, itemSchema, options = {}) {
|
|
1426
|
+
const { limit = 10, maxPages = 100, maxItems = Infinity, ...paginationOptions } = options;
|
|
1427
|
+
let allItems = [];
|
|
1428
|
+
let currentAfter = paginationOptions.after_id;
|
|
1429
|
+
let pageCount = 0;
|
|
1430
|
+
while(pageCount < maxPages && allItems.length < maxItems){
|
|
1431
|
+
const response = await fetchPaginated(axiosInstance, endpoint, itemSchema, {
|
|
1432
|
+
...paginationOptions,
|
|
1433
|
+
limit,
|
|
1434
|
+
after_id: currentAfter
|
|
1435
|
+
});
|
|
1436
|
+
allItems = allItems.concat(response.data);
|
|
1437
|
+
pageCount++;
|
|
1438
|
+
// Stop if we've reached the maxItems limit
|
|
1439
|
+
if (allItems.length >= maxItems) {
|
|
1440
|
+
allItems = allItems.slice(0, maxItems);
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
// Stop if there are no more pages
|
|
1444
|
+
if (!response.has_more) {
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
// Update cursor for next page
|
|
1448
|
+
currentAfter = response.after_id;
|
|
1449
|
+
}
|
|
1450
|
+
return allItems;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Create a paginator iterator for streaming through pages
|
|
1454
|
+
*/ function createPaginator(axiosInstance, endpoint, itemSchema, initialOptions = {}) {
|
|
1455
|
+
let currentOptions = {
|
|
1456
|
+
...initialOptions
|
|
1457
|
+
};
|
|
1458
|
+
let exhausted = false;
|
|
1459
|
+
return {
|
|
1460
|
+
/**
|
|
1461
|
+
* Get the next page
|
|
1462
|
+
*/ async next () {
|
|
1463
|
+
if (exhausted) {
|
|
1464
|
+
return {
|
|
1465
|
+
value: {},
|
|
1466
|
+
done: true
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
const response = await fetchPaginated(axiosInstance, endpoint, itemSchema, currentOptions);
|
|
1470
|
+
// Update options for next call
|
|
1471
|
+
if (response.has_more && response.after_id) {
|
|
1472
|
+
currentOptions.after_id = response.after_id;
|
|
1473
|
+
} else {
|
|
1474
|
+
exhausted = true;
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
value: response,
|
|
1478
|
+
done: !response.has_more
|
|
1479
|
+
};
|
|
1480
|
+
},
|
|
1481
|
+
/**
|
|
1482
|
+
* Reset the paginator to start from the beginning
|
|
1483
|
+
*/ reset (options = {}) {
|
|
1484
|
+
currentOptions = {
|
|
1485
|
+
...initialOptions,
|
|
1486
|
+
...options
|
|
1487
|
+
};
|
|
1488
|
+
exhausted = false;
|
|
1489
|
+
},
|
|
1490
|
+
/**
|
|
1491
|
+
* Check if there are more pages available
|
|
1492
|
+
*/ hasMore () {
|
|
1493
|
+
return !exhausted;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Async iterator for easy for-await-of usage
|
|
1499
|
+
*/ async function* iteratePages(axiosInstance, endpoint, itemSchema, options = {}) {
|
|
1500
|
+
const paginator = createPaginator(axiosInstance, endpoint, itemSchema, options);
|
|
1501
|
+
while(paginator.hasMore()){
|
|
1502
|
+
const { value, done } = await paginator.next();
|
|
1503
|
+
if (done) break;
|
|
1504
|
+
yield value;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Async iterator for individual items across all pages
|
|
1509
|
+
*/ async function* iterateItems(axiosInstance, endpoint, itemSchema, options = {}) {
|
|
1510
|
+
let itemCount = 0;
|
|
1511
|
+
const maxItems = options.maxItems || Infinity;
|
|
1512
|
+
for await (const page of iteratePages(axiosInstance, endpoint, itemSchema, options)){
|
|
1513
|
+
for (const item of page.data){
|
|
1514
|
+
if (itemCount >= maxItems) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
yield item;
|
|
1518
|
+
itemCount++;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function buildSearchParams(options) {
|
|
1523
|
+
const params = buildPaginationParams(options);
|
|
1524
|
+
if (options.query) {
|
|
1525
|
+
params.query = options.query;
|
|
1526
|
+
}
|
|
1527
|
+
if (options.sort_by) {
|
|
1528
|
+
params.sort_by = options.sort_by;
|
|
1529
|
+
}
|
|
1530
|
+
if (options.sort_direction) {
|
|
1531
|
+
params.sort_direction = options.sort_direction;
|
|
1532
|
+
}
|
|
1533
|
+
// Add filter parameters
|
|
1534
|
+
if (options.filters) {
|
|
1535
|
+
Object.entries(options.filters).forEach(([key, value])=>{
|
|
1536
|
+
if (value !== undefined && value !== null) {
|
|
1537
|
+
params[key] = String(value);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return params;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
export { AuthenticationError, NotFoundError, PaginatedResponseSchema, PaginationMetaSchema, RateLimitError, RateLimiter, ValidationError, WebhookEventSchema, WebhookEventTypeSchema, Xendit, XenditApiError, XenditErrorSchema, buildPaginationParams, buildSearchParams, createPaginator, createRateLimitInterceptor, createRateLimitedAxios, createRetryInterceptor, createWebhookProcessor, fetchAllPages, fetchPaginated, handleAxiosError, handleWebhookEvent, isNotNullOrUndefined, isValidCheckoutMethod, isValidCountry, isValidCurrency, isValidCustomerType, isValidDateString, isValidEmail, isValidPhone, isValidUrl, iterateItems, iteratePages, parseWebhookEvent, setupRateLimit, validateInput, verifyWebhookHmac, verifyWebhookSignature };
|