blaaiz-nodejs-sdk 1.1.0 → 1.1.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/README.md CHANGED
@@ -94,6 +94,23 @@ const customers = await blaaiz.customers.list();
94
94
  console.log('Customers:', customers.data);
95
95
  ```
96
96
 
97
+ You can also pass optional filters and opt-in pagination. Supported filters
98
+ are `email`, `id_number`, `registration_number`, `verification_status`, and
99
+ `type`. Set `paginate: true` to receive a paginated response that includes
100
+ `links` and `meta` (with `current_page`, `total`, etc.) alongside `data`.
101
+
102
+ ```javascript
103
+ const verified = await blaaiz.customers.list({
104
+ email: 'john@example.com',
105
+ verification_status: 'VERIFIED',
106
+ type: 'individual',
107
+ paginate: true
108
+ });
109
+
110
+ console.log('Page:', verified.data.meta.current_page);
111
+ console.log('Customers:', verified.data.data);
112
+ ```
113
+
97
114
  #### Update Customer
98
115
 
99
116
  ```javascript
@@ -128,7 +145,7 @@ const result = await blaaiz.customers.uploadFileComplete('customer-id', {
128
145
  file: fileBuffer, // Buffer or Uint8Array
129
146
  file_category: 'identity', // identity, proof_of_address, liveness_check
130
147
  filename: 'passport.jpg', // Optional
131
- contentType: 'image/jpeg' // Optional
148
+ content_type: 'image/jpeg' // Optional
132
149
  });
133
150
 
134
151
  // Option B: Upload from Base64 string
package/index.d.ts CHANGED
@@ -35,7 +35,43 @@ export interface CustomerData {
35
35
  export interface Customer extends CustomerData {
36
36
  id: string;
37
37
  business_id: string;
38
- verification_status: 'PENDING' | 'VERIFIED' | 'REJECTED';
38
+ verification_status: 'PENDING' | 'PROCESSING' | 'VERIFIED' | 'REJECTED';
39
+ }
40
+
41
+ export interface CustomerListFilters {
42
+ email?: string;
43
+ id_number?: string;
44
+ registration_number?: string;
45
+ verification_status?: 'PENDING' | 'PROCESSING' | 'VERIFIED' | 'REJECTED';
46
+ type?: 'individual' | 'business';
47
+ paginate?: boolean;
48
+ page?: number;
49
+ per_page?: number;
50
+ [key: string]: string | number | boolean | undefined;
51
+ }
52
+
53
+ export interface PaginationLinks {
54
+ first?: string | null;
55
+ last?: string | null;
56
+ prev?: string | null;
57
+ next?: string | null;
58
+ }
59
+
60
+ export interface PaginationMeta {
61
+ current_page: number;
62
+ from?: number | null;
63
+ last_page?: number;
64
+ path?: string;
65
+ per_page?: number;
66
+ to?: number | null;
67
+ total: number;
68
+ }
69
+
70
+ export interface PaginatedCustomers {
71
+ data: Customer[];
72
+ links: PaginationLinks;
73
+ meta: PaginationMeta;
74
+ message?: string;
39
75
  }
40
76
 
41
77
  export interface CustomerKYCData {
@@ -52,6 +88,8 @@ export interface FileUploadOptions {
52
88
  file: Buffer | Uint8Array | string;
53
89
  file_category: 'identity' | 'proof_of_address' | 'liveness_check';
54
90
  filename?: string;
91
+ content_type?: string;
92
+ /** @deprecated Use content_type instead */
55
93
  contentType?: string;
56
94
  }
57
95
 
@@ -242,7 +280,7 @@ export interface WebhookEvent {
242
280
  export declare class CustomerService {
243
281
  constructor(client: any);
244
282
  create(customerData: CustomerData): Promise<BlaaizResponse<{ data: Customer }>>;
245
- list(): Promise<BlaaizResponse<Customer[]>>;
283
+ list(filters?: CustomerListFilters): Promise<BlaaizResponse<Customer[] | PaginatedCustomers>>;
246
284
  get(customerId: string): Promise<BlaaizResponse<Customer>>;
247
285
  update(customerId: string, updateData: Partial<CustomerData>): Promise<BlaaizResponse<Customer>>;
248
286
  addKYC(customerId: string, kycData: CustomerKYCData): Promise<BlaaizResponse<any>>;
@@ -331,7 +369,7 @@ export declare class Blaaiz {
331
369
 
332
370
  testConnection(): Promise<boolean>;
333
371
 
334
- createCompleteePayout(payoutConfig: {
372
+ createCompletePayout(payoutConfig: {
335
373
  customerData?: CustomerData;
336
374
  payoutData: PayoutData;
337
375
  }): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaaiz-nodejs-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Official Node.js SDK for Blaaiz RaaS (Remittance as a Service) API",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -40,10 +40,10 @@
40
40
  "license": "MIT",
41
41
  "repository": {
42
42
  "type": "git",
43
- "url": "https://github.com/blaaiz/nodejs-sdk.git"
43
+ "url": "git+https://github.com/Blaaiz/blaaiz-nodejs-sdk.git"
44
44
  },
45
45
  "bugs": {
46
- "url": "https://github.com/blaaiz/nodejs-sdk/issues"
46
+ "url": "https://github.com/Blaaiz/blaaiz-nodejs-sdk/issues"
47
47
  },
48
48
  "homepage": "https://docs.business.blaaiz.com",
49
49
  "engines": {
package/src/index.js CHANGED
@@ -39,7 +39,7 @@ class Blaaiz {
39
39
  }
40
40
  }
41
41
 
42
- async createCompleteePayout (payoutConfig) {
42
+ async createCompletePayout (payoutConfig) {
43
43
  const { customerData, payoutData } = payoutConfig
44
44
 
45
45
  try {
@@ -25,8 +25,15 @@ class CustomerService {
25
25
  return this.client.makeRequest('POST', '/api/external/customer', customerData)
26
26
  }
27
27
 
28
- async list () {
29
- return this.client.makeRequest('GET', '/api/external/customer')
28
+ async list (filters = {}) {
29
+ const params = new URLSearchParams()
30
+ for (const [key, value] of Object.entries(filters || {})) {
31
+ if (value === undefined || value === null) continue
32
+ params.append(key, typeof value === 'boolean' ? String(value) : value)
33
+ }
34
+ const query = params.toString()
35
+ const endpoint = query ? `/api/external/customer?${query}` : '/api/external/customer'
36
+ return this.client.makeRequest('GET', endpoint)
30
37
  }
31
38
 
32
39
  async get (customerId) {
@@ -84,7 +91,8 @@ class CustomerService {
84
91
  }
85
92
 
86
93
  const { file, file_category } = fileOptions // eslint-disable-line camelcase
87
- let { filename, contentType } = fileOptions
94
+ let { filename } = fileOptions
95
+ let contentType = fileOptions.content_type || fileOptions.contentType // eslint-disable-line camelcase
88
96
 
89
97
  if (!file) {
90
98
  throw new Error('File is required')
@@ -129,6 +137,15 @@ class CustomerService {
129
137
  if (file.startsWith('data:')) {
130
138
  // Handle data URL format: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=
131
139
  const base64Data = file.split(',')[1]
140
+ if (!base64Data || !base64Data.trim()) {
141
+ throw new Error('Invalid data URL: no base64 data found after the comma')
142
+ }
143
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data.trim())) {
144
+ throw new Error(
145
+ 'The base64 portion of the data URL does not appear to be valid base64. ' +
146
+ 'Ensure the string after the comma contains only valid base64 characters.'
147
+ )
148
+ }
132
149
  fileBuffer = Buffer.from(base64Data, 'base64')
133
150
 
134
151
  // Extract content type from data URL if not provided
@@ -154,6 +171,15 @@ class CustomerService {
154
171
  }
155
172
  } else {
156
173
  // Handle plain base64 string
174
+ if (!file.trim()) {
175
+ throw new Error('The file string is empty')
176
+ }
177
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(file.trim())) {
178
+ throw new Error(
179
+ 'The file string does not appear to be valid base64. ' +
180
+ 'If you meant to pass a file path, Buffer, or URL, use the appropriate format instead.'
181
+ )
182
+ }
157
183
  fileBuffer = Buffer.from(file, 'base64')
158
184
  }
159
185
  } else {
@@ -161,6 +187,19 @@ class CustomerService {
161
187
  fileBuffer = Buffer.from(file)
162
188
  }
163
189
 
190
+ // Auto-detect content type if not provided
191
+ if (!contentType) {
192
+ contentType = this._detectContentTypeFromBytes(fileBuffer)
193
+ }
194
+ if (!contentType && filename) {
195
+ contentType = this._getContentTypeFromFilename(filename)
196
+ }
197
+ if (!contentType) {
198
+ throw new Error(
199
+ 'Could not determine file content type. Please provide a content_type (e.g., "image/jpeg", "image/png", "application/pdf") in fileOptions.'
200
+ )
201
+ }
202
+
164
203
  await this._uploadToS3(presignedUrl, fileBuffer, contentType, filename)
165
204
 
166
205
  // Map file category to the correct field name expected by Laravel API
@@ -353,6 +392,66 @@ class CustomerService {
353
392
  })
354
393
  }
355
394
 
395
+ _detectContentTypeFromBytes (buffer) {
396
+ if (!buffer || buffer.length < 4) return null
397
+
398
+ // JPEG: FF D8 FF
399
+ if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) {
400
+ return 'image/jpeg'
401
+ }
402
+ // PNG: 89 50 4E 47
403
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) {
404
+ return 'image/png'
405
+ }
406
+ // GIF: 47 49 46 38
407
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
408
+ return 'image/gif'
409
+ }
410
+ // PDF: 25 50 44 46 (%PDF)
411
+ if (buffer[0] === 0x25 && buffer[1] === 0x50 && buffer[2] === 0x44 && buffer[3] === 0x46) {
412
+ return 'application/pdf'
413
+ }
414
+ // WEBP: starts with RIFF....WEBP
415
+ if (buffer.length >= 12 &&
416
+ buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
417
+ buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50) {
418
+ return 'image/webp'
419
+ }
420
+ // BMP: 42 4D
421
+ if (buffer[0] === 0x42 && buffer[1] === 0x4D) {
422
+ return 'image/bmp'
423
+ }
424
+ // TIFF: 49 49 2A 00 (little-endian) or 4D 4D 00 2A (big-endian)
425
+ if ((buffer[0] === 0x49 && buffer[1] === 0x49 && buffer[2] === 0x2A && buffer[3] === 0x00) ||
426
+ (buffer[0] === 0x4D && buffer[1] === 0x4D && buffer[2] === 0x00 && buffer[3] === 0x2A)) {
427
+ return 'image/tiff'
428
+ }
429
+
430
+ return null
431
+ }
432
+
433
+ _getContentTypeFromFilename (filename) {
434
+ if (!filename) return null
435
+
436
+ const ext = filename.toLowerCase().split('.').pop()
437
+ const extToMime = {
438
+ jpg: 'image/jpeg',
439
+ jpeg: 'image/jpeg',
440
+ png: 'image/png',
441
+ gif: 'image/gif',
442
+ webp: 'image/webp',
443
+ bmp: 'image/bmp',
444
+ tiff: 'image/tiff',
445
+ tif: 'image/tiff',
446
+ pdf: 'application/pdf',
447
+ txt: 'text/plain',
448
+ doc: 'application/msword',
449
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
450
+ }
451
+
452
+ return extToMime[ext] || null
453
+ }
454
+
356
455
  _getExtensionFromContentType (contentType) {
357
456
  const mimeToExt = {
358
457
  'image/jpeg': '.jpg',