blaaiz-nodejs-sdk 1.0.0 → 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/README.md CHANGED
@@ -13,12 +13,17 @@ npm install blaaiz-nodejs-sdk
13
13
  ```javascript
14
14
  const { Blaaiz } = require('blaaiz-nodejs-sdk');
15
15
 
16
- // Initialize the SDK
16
+ // Initialize the SDK (defaults to development environment)
17
17
  const blaaiz = new Blaaiz('your-api-key-here', {
18
18
  baseURL: 'https://api-dev.blaaiz.com', // Optional: defaults to dev environment
19
19
  timeout: 30000 // Optional: request timeout in milliseconds
20
20
  });
21
21
 
22
+ // For production, change the baseURL:
23
+ // const blaaiz = new Blaaiz('your-prod-api-key', {
24
+ // baseURL: 'https://api.blaaiz.com'
25
+ // });
26
+
22
27
  // Test the connection
23
28
  const isConnected = await blaaiz.testConnection();
24
29
  console.log('API Connected:', isConnected);
@@ -594,16 +599,55 @@ app.post('/webhooks/collection', (req, res) => {
594
599
 
595
600
  ## Environment Configuration
596
601
 
602
+ The SDK defaults to the development environment. To use different environments:
603
+
597
604
  ```javascript
598
- // Development
605
+ // Development (default)
606
+ const blaaizDev = new Blaaiz('dev-api-key');
607
+ // OR explicitly specify dev URL
599
608
  const blaaizDev = new Blaaiz('dev-api-key', {
600
609
  baseURL: 'https://api-dev.blaaiz.com'
601
610
  });
602
611
 
603
- // Production (when available)
612
+ // Production
604
613
  const blaaizProd = new Blaaiz('prod-api-key', {
605
614
  baseURL: 'https://api.blaaiz.com'
606
615
  });
616
+
617
+ // Staging (if available)
618
+ const blaaizStaging = new Blaaiz('staging-api-key', {
619
+ baseURL: 'https://api-staging.blaaiz.com'
620
+ });
621
+ ```
622
+
623
+ ### Environment Variables Approach (Recommended)
624
+
625
+ ```javascript
626
+ const { Blaaiz } = require('blaaiz-nodejs-sdk');
627
+
628
+ const blaaiz = new Blaaiz(process.env.BLAAIZ_API_KEY, {
629
+ baseURL: process.env.BLAAIZ_API_URL || 'https://api-dev.blaaiz.com'
630
+ });
631
+ ```
632
+
633
+ **Environment Variables:**
634
+ - `BLAAIZ_API_KEY` - Your API key
635
+ - `BLAAIZ_API_URL` - API base URL (optional, defaults to dev)
636
+ - `BLAAIZ_WEBHOOK_SECRET` - Webhook secret for signature verification
637
+ - `BLAAIZ_TEST_WALLET_ID` - Test wallet ID for integration tests (optional)
638
+
639
+ **.env file example:**
640
+ ```bash
641
+ # Development
642
+ BLAAIZ_API_KEY=your-dev-api-key
643
+ BLAAIZ_API_URL=https://api-dev.blaaiz.com
644
+ BLAAIZ_WEBHOOK_SECRET=your-webhook-secret
645
+ BLAAIZ_TEST_WALLET_ID=your-test-wallet-id
646
+
647
+ # Production
648
+ # BLAAIZ_API_KEY=your-prod-api-key
649
+ # BLAAIZ_API_URL=https://api.blaaiz.com
650
+ # BLAAIZ_WEBHOOK_SECRET=your-prod-webhook-secret
607
651
  ```
608
652
 
609
653
  ## Best Practices
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaaiz-nodejs-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
@@ -8,8 +8,10 @@
8
8
  "test": "jest",
9
9
  "test:watch": "jest --watch",
10
10
  "test:coverage": "jest --coverage",
11
+ "test:integration": "jest --testPathPattern=integration",
11
12
  "lint": "eslint src/**/*.js",
12
13
  "lint:fix": "eslint src/**/*.js --fix",
14
+ "audit": "npm audit --audit-level=moderate",
13
15
  "docs": "jsdoc -d docs src/**/*.js",
14
16
  "build": "npm run lint && npm test",
15
17
  "prepublishOnly": "npm run build"
@@ -55,11 +57,13 @@
55
57
  "eslint-plugin-n": "^15.7.0",
56
58
  "eslint-plugin-promise": "^6.1.1",
57
59
  "jest": "^29.5.0",
58
- "jsdoc": "^4.0.2"
60
+ "jsdoc": "^4.0.2",
61
+ "audit-ci": "^7.0.1"
59
62
  },
60
63
  "files": [
61
64
  "index.js",
62
65
  "index.d.ts",
66
+ "src/",
63
67
  "README.md",
64
68
  "LICENSE"
65
69
  ],
package/src/client.js ADDED
@@ -0,0 +1,96 @@
1
+ const https = require('https')
2
+ const http = require('http')
3
+ const { URL } = require('url')
4
+ const BlaaizError = require('./error')
5
+
6
+ class BlaaizAPIClient {
7
+ constructor (apiKey, options = {}) {
8
+ if (!apiKey) {
9
+ throw new Error('API key is required')
10
+ }
11
+
12
+ this.apiKey = apiKey
13
+ this.baseURL = options.baseURL || 'https://api-dev.blaaiz.com'
14
+ this.timeout = options.timeout || 30000
15
+ this.defaultHeaders = {
16
+ 'x-blaaiz-api-key': this.apiKey,
17
+ Accept: 'application/json',
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': 'Blaaiz-NodeJS-SDK/1.0.0'
20
+ }
21
+ }
22
+
23
+ async makeRequest (method, endpoint, data = null, headers = {}) {
24
+ return new Promise((resolve, reject) => {
25
+ const url = new URL(endpoint, this.baseURL)
26
+ const options = {
27
+ hostname: url.hostname,
28
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
+ path: url.pathname + url.search,
30
+ method: method.toUpperCase(),
31
+ headers: { ...this.defaultHeaders, ...headers },
32
+ timeout: this.timeout
33
+ }
34
+
35
+ const client = url.protocol === 'https:' ? https : http
36
+ const req = client.request(options, (res) => {
37
+ let responseData = ''
38
+
39
+ res.on('data', (chunk) => {
40
+ responseData += chunk
41
+ })
42
+
43
+ res.on('end', () => {
44
+ try {
45
+ const parsedData = JSON.parse(responseData)
46
+
47
+ if (res.statusCode >= 200 && res.statusCode < 300) {
48
+ resolve({
49
+ data: parsedData,
50
+ status: res.statusCode,
51
+ headers: res.headers
52
+ })
53
+ } else {
54
+ reject(new BlaaizError(
55
+ parsedData.message || 'API request failed',
56
+ res.statusCode,
57
+ parsedData.code
58
+ ))
59
+ }
60
+ } catch (error) {
61
+ reject(new BlaaizError(
62
+ 'Failed to parse API response',
63
+ res.statusCode,
64
+ 'PARSE_ERROR'
65
+ ))
66
+ }
67
+ })
68
+ })
69
+
70
+ req.on('error', (error) => {
71
+ reject(new BlaaizError(
72
+ `Request failed: ${error.message}`,
73
+ null,
74
+ 'REQUEST_ERROR'
75
+ ))
76
+ })
77
+
78
+ req.on('timeout', () => {
79
+ req.destroy()
80
+ reject(new BlaaizError(
81
+ 'Request timeout',
82
+ null,
83
+ 'TIMEOUT_ERROR'
84
+ ))
85
+ })
86
+
87
+ if (data && method.toUpperCase() !== 'GET') {
88
+ req.write(JSON.stringify(data))
89
+ }
90
+
91
+ req.end()
92
+ })
93
+ }
94
+ }
95
+
96
+ module.exports = BlaaizAPIClient
package/src/error.js ADDED
@@ -0,0 +1,10 @@
1
+ class BlaaizError extends Error {
2
+ constructor(message, status, code) {
3
+ super(message)
4
+ this.name = 'BlaaizError'
5
+ this.status = status
6
+ this.code = code
7
+ }
8
+ }
9
+
10
+ module.exports = BlaaizError
package/src/index.js ADDED
@@ -0,0 +1,132 @@
1
+ const BlaaizError = require('./error')
2
+ const BlaaizAPIClient = require('./client')
3
+
4
+ const CustomerService = require('./services/CustomerService')
5
+ const CollectionService = require('./services/CollectionService')
6
+ const PayoutService = require('./services/PayoutService')
7
+ const WalletService = require('./services/WalletService')
8
+ const VirtualBankAccountService = require('./services/VirtualBankAccountService')
9
+ const TransactionService = require('./services/TransactionService')
10
+ const BankService = require('./services/BankService')
11
+ const CurrencyService = require('./services/CurrencyService')
12
+ const FeesService = require('./services/FeesService')
13
+ const FileService = require('./services/FileService')
14
+ const WebhookService = require('./services/WebhookService')
15
+
16
+ class Blaaiz {
17
+ constructor (apiKey, options = {}) {
18
+ this.client = new BlaaizAPIClient(apiKey, options)
19
+
20
+ this.customers = new CustomerService(this.client)
21
+ this.collections = new CollectionService(this.client)
22
+ this.payouts = new PayoutService(this.client)
23
+ this.wallets = new WalletService(this.client)
24
+ this.virtualBankAccounts = new VirtualBankAccountService(this.client)
25
+ this.transactions = new TransactionService(this.client)
26
+ this.banks = new BankService(this.client)
27
+ this.currencies = new CurrencyService(this.client)
28
+ this.fees = new FeesService(this.client)
29
+ this.files = new FileService(this.client)
30
+ this.webhooks = new WebhookService(this.client)
31
+ }
32
+
33
+ async testConnection () {
34
+ try {
35
+ await this.currencies.list()
36
+ return true
37
+ } catch (error) {
38
+ return false
39
+ }
40
+ }
41
+
42
+ async createCompleteePayout (payoutConfig) {
43
+ const { customerData, payoutData } = payoutConfig
44
+
45
+ try {
46
+ let customerId = payoutData.customer_id
47
+ if (!customerId && customerData) {
48
+ const customerResult = await this.customers.create(customerData)
49
+ customerId = customerResult.data.data.id
50
+ }
51
+
52
+ const feeBreakdown = await this.fees.getBreakdown({
53
+ from_currency_id: payoutData.from_currency_id,
54
+ to_currency_id: payoutData.to_currency_id,
55
+ from_amount: payoutData.from_amount
56
+ })
57
+
58
+ const payoutResult = await this.payouts.initiate({
59
+ ...payoutData,
60
+ customer_id: customerId
61
+ })
62
+
63
+ return {
64
+ customer_id: customerId,
65
+ payout: payoutResult.data,
66
+ fees: feeBreakdown.data
67
+ }
68
+ } catch (error) {
69
+ throw new BlaaizError(
70
+ `Complete payout failed: ${error.message}`,
71
+ error.status,
72
+ error.code
73
+ )
74
+ }
75
+ }
76
+
77
+ async createCompleteCollection (collectionConfig) {
78
+ const { customerData, collectionData, createVBA = false } = collectionConfig
79
+
80
+ try {
81
+ let customerId = collectionData.customer_id
82
+ if (!customerId && customerData) {
83
+ const customerResult = await this.customers.create(customerData)
84
+ customerId = customerResult.data.data.id
85
+ }
86
+
87
+ let vbaData = null
88
+ if (createVBA) {
89
+ const vbaResult = await this.virtualBankAccounts.create({
90
+ wallet_id: collectionData.wallet_id,
91
+ account_name: customerData ? `${customerData.first_name} ${customerData.last_name}` : 'Customer Account'
92
+ })
93
+ vbaData = vbaResult.data
94
+ }
95
+
96
+ const collectionResult = await this.collections.initiate({
97
+ ...collectionData,
98
+ customer_id: customerId
99
+ })
100
+
101
+ return {
102
+ customer_id: customerId,
103
+ collection: collectionResult.data,
104
+ virtual_account: vbaData
105
+ }
106
+ } catch (error) {
107
+ throw new BlaaizError(
108
+ `Complete collection failed: ${error.message}`,
109
+ error.status,
110
+ error.code
111
+ )
112
+ }
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ Blaaiz,
118
+ BlaaizError,
119
+ CustomerService,
120
+ CollectionService,
121
+ PayoutService,
122
+ WalletService,
123
+ VirtualBankAccountService,
124
+ TransactionService,
125
+ BankService,
126
+ CurrencyService,
127
+ FeesService,
128
+ FileService,
129
+ WebhookService
130
+ }
131
+
132
+ module.exports.default = Blaaiz
@@ -0,0 +1,22 @@
1
+ class BankService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async list () {
7
+ return this.client.makeRequest('GET', '/api/external/bank')
8
+ }
9
+
10
+ async lookupAccount (lookupData) {
11
+ const requiredFields = ['account_number', 'bank_id']
12
+ for (const field of requiredFields) {
13
+ if (!lookupData[field]) {
14
+ throw new Error(`${field} is required`)
15
+ }
16
+ }
17
+
18
+ return this.client.makeRequest('POST', '/api/external/bank/account-lookup', lookupData)
19
+ }
20
+ }
21
+
22
+ module.exports = BankService
@@ -0,0 +1,37 @@
1
+ class CollectionService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async initiate (collectionData) {
7
+ const requiredFields = ['method', 'amount', 'wallet_id']
8
+ for (const field of requiredFields) {
9
+ if (!collectionData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ return this.client.makeRequest('POST', '/api/external/collection', collectionData)
15
+ }
16
+
17
+ async initiateCrypto (cryptoData) {
18
+ return this.client.makeRequest('POST', '/api/external/collection/crypto', cryptoData)
19
+ }
20
+
21
+ async attachCustomer (attachData) {
22
+ const requiredFields = ['customer_id', 'transaction_id']
23
+ for (const field of requiredFields) {
24
+ if (!attachData[field]) {
25
+ throw new Error(`${field} is required`)
26
+ }
27
+ }
28
+
29
+ return this.client.makeRequest('POST', '/api/external/collection/attach-customer', attachData)
30
+ }
31
+
32
+ async getCryptoNetworks () {
33
+ return this.client.makeRequest('GET', '/api/external/collection/crypto/networks')
34
+ }
35
+ }
36
+
37
+ module.exports = CollectionService
@@ -0,0 +1,11 @@
1
+ class CurrencyService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async list () {
7
+ return this.client.makeRequest('GET', '/api/external/currency')
8
+ }
9
+ }
10
+
11
+ module.exports = CurrencyService
@@ -0,0 +1,308 @@
1
+ class CustomerService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async create (customerData) {
7
+ const requiredFields = ['first_name', 'last_name', 'type', 'email', 'country', 'id_type', 'id_number']
8
+ for (const field of requiredFields) {
9
+ if (!customerData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ if (customerData.type === 'business' && !customerData.business_name) {
15
+ throw new Error('business_name is required when type is business')
16
+ }
17
+
18
+ return this.client.makeRequest('POST', '/api/external/customer', customerData)
19
+ }
20
+
21
+ async list () {
22
+ return this.client.makeRequest('GET', '/api/external/customer')
23
+ }
24
+
25
+ async get (customerId) {
26
+ if (!customerId) {
27
+ throw new Error('Customer ID is required')
28
+ }
29
+ return this.client.makeRequest('GET', `/api/external/customer/${customerId}`)
30
+ }
31
+
32
+ async update (customerId, updateData) {
33
+ if (!customerId) {
34
+ throw new Error('Customer ID is required')
35
+ }
36
+ return this.client.makeRequest('PUT', `/api/external/customer/${customerId}`, updateData)
37
+ }
38
+
39
+ async addKYC (customerId, kycData) {
40
+ if (!customerId) {
41
+ throw new Error('Customer ID is required')
42
+ }
43
+ return this.client.makeRequest('POST', `/api/external/customer/${customerId}/kyc-data`, kycData)
44
+ }
45
+
46
+ async uploadFiles (customerId, fileData) {
47
+ if (!customerId) {
48
+ throw new Error('Customer ID is required')
49
+ }
50
+ return this.client.makeRequest('PUT', `/api/external/customer/${customerId}/files`, fileData)
51
+ }
52
+
53
+ async uploadFileComplete (customerId, fileOptions) {
54
+ if (!customerId) {
55
+ throw new Error('Customer ID is required')
56
+ }
57
+
58
+ if (!fileOptions) {
59
+ throw new Error('File options are required')
60
+ }
61
+
62
+ const { file, file_category } = fileOptions // eslint-disable-line camelcase
63
+ let { filename, contentType } = fileOptions
64
+
65
+ if (!file) {
66
+ throw new Error('File is required')
67
+ }
68
+
69
+ if (!file_category) { // eslint-disable-line camelcase
70
+ throw new Error('file_category is required')
71
+ }
72
+
73
+ if (!['identity', 'proof_of_address', 'liveness_check'].includes(file_category)) { // eslint-disable-line camelcase
74
+ throw new Error('file_category must be one of: identity, proof_of_address, liveness_check')
75
+ }
76
+
77
+ try {
78
+ const presignedResponse = await this.client.makeRequest('POST', '/api/external/file/get-presigned-url', {
79
+ customer_id: customerId, // eslint-disable-line camelcase
80
+ file_category // eslint-disable-line camelcase
81
+ })
82
+
83
+ const { url: presignedUrl, file_id } = presignedResponse.data.data // eslint-disable-line camelcase
84
+
85
+ let fileBuffer
86
+ if (Buffer.isBuffer(file)) {
87
+ fileBuffer = file
88
+ } else if (typeof file === 'string') {
89
+ if (file.startsWith('data:')) {
90
+ // Handle data URL format: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=
91
+ const base64Data = file.split(',')[1]
92
+ fileBuffer = Buffer.from(base64Data, 'base64')
93
+
94
+ // Extract content type from data URL if not provided
95
+ if (!contentType) {
96
+ const mimeMatch = file.match(/data:([^;]+)/)
97
+ if (mimeMatch) {
98
+ contentType = mimeMatch[1]
99
+ }
100
+ }
101
+ } else if (file.startsWith('http://') || file.startsWith('https://')) {
102
+ // Handle URL - download the file
103
+ const downloadResult = await this._downloadFile(file)
104
+ fileBuffer = downloadResult.buffer
105
+
106
+ // Use detected content type if not provided
107
+ if (!contentType && downloadResult.contentType) {
108
+ contentType = downloadResult.contentType
109
+ }
110
+
111
+ // Use filename from URL if not provided
112
+ if (!filename && downloadResult.filename) {
113
+ filename = downloadResult.filename
114
+ }
115
+ } else {
116
+ // Handle plain base64 string
117
+ fileBuffer = Buffer.from(file, 'base64')
118
+ }
119
+ } else {
120
+ // Handle Uint8Array or other array-like structures
121
+ fileBuffer = Buffer.from(file)
122
+ }
123
+
124
+ await this._uploadToS3(presignedUrl, fileBuffer, contentType, filename)
125
+
126
+ const fileAssociation = await this.client.makeRequest('PUT', `/api/external/customer/${customerId}/files`, {
127
+ id_file: file_id // eslint-disable-line camelcase
128
+ })
129
+
130
+ return {
131
+ ...fileAssociation,
132
+ file_id, // eslint-disable-line camelcase
133
+ presigned_url: presignedUrl
134
+ }
135
+ } catch (error) {
136
+ throw new Error(`File upload failed: ${error.message}`)
137
+ }
138
+ }
139
+
140
+ async _uploadToS3 (presignedUrl, fileBuffer, contentType, filename) {
141
+ const https = require('https')
142
+ const { URL } = require('url')
143
+
144
+ return new Promise((resolve, reject) => {
145
+ const url = new URL(presignedUrl)
146
+
147
+ const headers = {
148
+ 'Content-Length': fileBuffer.length
149
+ }
150
+
151
+ if (contentType) {
152
+ headers['Content-Type'] = contentType
153
+ }
154
+
155
+ if (filename) {
156
+ headers['Content-Disposition'] = `attachment; filename="${filename}"`
157
+ }
158
+
159
+ const options = {
160
+ hostname: url.hostname,
161
+ port: url.port || 443,
162
+ path: url.pathname + url.search,
163
+ method: 'PUT',
164
+ headers
165
+ }
166
+
167
+ const req = https.request(options, (res) => {
168
+ let responseData = ''
169
+
170
+ res.on('data', (chunk) => {
171
+ responseData += chunk
172
+ })
173
+
174
+ res.on('end', () => {
175
+ if (res.statusCode >= 200 && res.statusCode < 300) {
176
+ resolve({
177
+ status: res.statusCode,
178
+ data: responseData
179
+ })
180
+ } else {
181
+ reject(new Error(`S3 upload failed with status ${res.statusCode}: ${responseData}`))
182
+ }
183
+ })
184
+ })
185
+
186
+ req.on('error', (error) => {
187
+ reject(new Error(`S3 upload request failed: ${error.message}`))
188
+ })
189
+
190
+ req.write(fileBuffer)
191
+ req.end()
192
+ })
193
+ }
194
+
195
+ async _downloadFile (url) {
196
+ const https = require('https')
197
+ const http = require('http')
198
+ const { URL } = require('url')
199
+ const path = require('path')
200
+
201
+ return new Promise((resolve, reject) => {
202
+ const urlObj = new URL(url)
203
+ const client = urlObj.protocol === 'https:' ? https : http
204
+
205
+ const options = {
206
+ hostname: urlObj.hostname,
207
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
208
+ path: urlObj.pathname + urlObj.search,
209
+ method: 'GET',
210
+ headers: {
211
+ 'User-Agent': 'Blaaiz-NodeJS-SDK/1.0.0'
212
+ }
213
+ }
214
+
215
+ const req = client.request(options, (res) => {
216
+ // Handle redirects
217
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
218
+ return this._downloadFile(res.headers.location)
219
+ .then(resolve)
220
+ .catch(reject)
221
+ }
222
+
223
+ if (res.statusCode < 200 || res.statusCode >= 300) {
224
+ reject(new Error(`Failed to download file: HTTP ${res.statusCode}`))
225
+ return
226
+ }
227
+
228
+ const chunks = []
229
+ let totalLength = 0
230
+
231
+ res.on('data', (chunk) => {
232
+ chunks.push(chunk)
233
+ totalLength += chunk.length
234
+ })
235
+
236
+ res.on('end', () => {
237
+ const buffer = Buffer.concat(chunks, totalLength)
238
+
239
+ // Extract content type from response headers
240
+ const contentType = res.headers['content-type']
241
+
242
+ // Extract filename from URL path or Content-Disposition header
243
+ let filename = null
244
+
245
+ // Try Content-Disposition header first
246
+ const contentDisposition = res.headers['content-disposition']
247
+ if (contentDisposition) {
248
+ const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
249
+ if (filenameMatch) {
250
+ filename = filenameMatch[1].replace(/['"]/g, '')
251
+ }
252
+ }
253
+
254
+ // Fallback to URL path
255
+ if (!filename) {
256
+ const urlPath = urlObj.pathname
257
+ filename = path.basename(urlPath)
258
+
259
+ // If no extension, try to add one based on content type
260
+ if (!path.extname(filename) && contentType) {
261
+ const ext = this._getExtensionFromContentType(contentType)
262
+ if (ext) {
263
+ filename += ext
264
+ }
265
+ }
266
+ }
267
+
268
+ resolve({
269
+ buffer,
270
+ contentType,
271
+ filename
272
+ })
273
+ })
274
+ })
275
+
276
+ req.on('error', (error) => {
277
+ reject(new Error(`File download failed: ${error.message}`))
278
+ })
279
+
280
+ req.setTimeout(30000, () => {
281
+ req.destroy()
282
+ reject(new Error('File download timeout'))
283
+ })
284
+
285
+ req.end()
286
+ })
287
+ }
288
+
289
+ _getExtensionFromContentType (contentType) {
290
+ const mimeToExt = {
291
+ 'image/jpeg': '.jpg',
292
+ 'image/jpg': '.jpg',
293
+ 'image/png': '.png',
294
+ 'image/gif': '.gif',
295
+ 'image/webp': '.webp',
296
+ 'image/bmp': '.bmp',
297
+ 'image/tiff': '.tiff',
298
+ 'application/pdf': '.pdf',
299
+ 'text/plain': '.txt',
300
+ 'application/msword': '.doc',
301
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx'
302
+ }
303
+
304
+ return mimeToExt[contentType.split(';')[0]] || null
305
+ }
306
+ }
307
+
308
+ module.exports = CustomerService
@@ -0,0 +1,18 @@
1
+ class FeesService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async getBreakdown (feeData) {
7
+ const requiredFields = ['from_currency_id', 'to_currency_id', 'from_amount']
8
+ for (const field of requiredFields) {
9
+ if (!feeData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ return this.client.makeRequest('POST', '/api/external/fees/breakdown', feeData)
15
+ }
16
+ }
17
+
18
+ module.exports = FeesService
@@ -0,0 +1,18 @@
1
+ class FileService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async getPresignedUrl (fileData) {
7
+ const requiredFields = ['customer_id', 'file_category']
8
+ for (const field of requiredFields) {
9
+ if (!fileData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ return this.client.makeRequest('POST', '/api/external/file/get-presigned-url', fileData)
15
+ }
16
+ }
17
+
18
+ module.exports = FileService
@@ -0,0 +1,31 @@
1
+ class PayoutService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async initiate (payoutData) {
7
+ const requiredFields = ['wallet_id', 'method', 'from_amount', 'from_currency_id', 'to_currency_id']
8
+ for (const field of requiredFields) {
9
+ if (!payoutData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ if (payoutData.method === 'bank_transfer' && !payoutData.account_number) {
15
+ throw new Error('account_number is required for bank_transfer method')
16
+ }
17
+
18
+ if (payoutData.method === 'interac') {
19
+ const interacFields = ['email', 'interac_first_name', 'interac_last_name']
20
+ for (const field of interacFields) {
21
+ if (!payoutData[field]) {
22
+ throw new Error(`${field} is required for interac method`)
23
+ }
24
+ }
25
+ }
26
+
27
+ return this.client.makeRequest('POST', '/api/external/payout', payoutData)
28
+ }
29
+ }
30
+
31
+ module.exports = PayoutService
@@ -0,0 +1,18 @@
1
+ class TransactionService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async list (filters = {}) {
7
+ return this.client.makeRequest('POST', '/api/external/transaction', filters)
8
+ }
9
+
10
+ async get (transactionId) {
11
+ if (!transactionId) {
12
+ throw new Error('Transaction ID is required')
13
+ }
14
+ return this.client.makeRequest('GET', `/api/external/transaction/${transactionId}`)
15
+ }
16
+ }
17
+
18
+ module.exports = TransactionService
@@ -0,0 +1,30 @@
1
+ class VirtualBankAccountService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async create (vbaData) {
7
+ const requiredFields = ['wallet_id']
8
+ for (const field of requiredFields) {
9
+ if (!vbaData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ return this.client.makeRequest('POST', '/api/external/virtual-bank-account', vbaData)
15
+ }
16
+
17
+ async list (walletId) {
18
+ const params = walletId ? `?wallet_id=${walletId}` : ''
19
+ return this.client.makeRequest('GET', `/api/external/virtual-bank-account${params}`)
20
+ }
21
+
22
+ async get (vbaId) {
23
+ if (!vbaId) {
24
+ throw new Error('Virtual bank account ID is required')
25
+ }
26
+ return this.client.makeRequest('GET', `/api/external/virtual-bank-account/${vbaId}`)
27
+ }
28
+ }
29
+
30
+ module.exports = VirtualBankAccountService
@@ -0,0 +1,18 @@
1
+ class WalletService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async list () {
7
+ return this.client.makeRequest('GET', '/api/external/wallet')
8
+ }
9
+
10
+ async get (walletId) {
11
+ if (!walletId) {
12
+ throw new Error('Wallet ID is required')
13
+ }
14
+ return this.client.makeRequest('GET', `/api/external/wallet/${walletId}`)
15
+ }
16
+ }
17
+
18
+ module.exports = WalletService
@@ -0,0 +1,96 @@
1
+ class WebhookService {
2
+ constructor (client) {
3
+ this.client = client
4
+ }
5
+
6
+ async register (webhookData) {
7
+ const requiredFields = ['collection_url', 'payout_url']
8
+ for (const field of requiredFields) {
9
+ if (!webhookData[field]) {
10
+ throw new Error(`${field} is required`)
11
+ }
12
+ }
13
+
14
+ return this.client.makeRequest('POST', '/api/external/webhook', webhookData)
15
+ }
16
+
17
+ async get () {
18
+ return this.client.makeRequest('GET', '/api/external/webhook')
19
+ }
20
+
21
+ async update (webhookData) {
22
+ return this.client.makeRequest('PUT', '/api/external/webhook', webhookData)
23
+ }
24
+
25
+ async replay (replayData) {
26
+ const requiredFields = ['transaction_id']
27
+ for (const field of requiredFields) {
28
+ if (!replayData[field]) {
29
+ throw new Error(`${field} is required`)
30
+ }
31
+ }
32
+
33
+ return this.client.makeRequest('POST', '/api/external/webhook/replay', replayData)
34
+ }
35
+
36
+ verifySignature (payload, signature, secret) {
37
+ if (!payload) {
38
+ throw new Error('Payload is required for signature verification')
39
+ }
40
+
41
+ if (!signature) {
42
+ throw new Error('Signature is required for signature verification')
43
+ }
44
+
45
+ if (!secret) {
46
+ throw new Error('Webhook secret is required for signature verification')
47
+ }
48
+
49
+ const crypto = require('crypto')
50
+
51
+ // Convert payload to string if it's an object
52
+ const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload)
53
+
54
+ // Remove 'sha256=' prefix if present
55
+ const cleanSignature = signature.replace(/^sha256=/, '')
56
+
57
+ // Create HMAC signature
58
+ const expectedSignature = crypto
59
+ .createHmac('sha256', secret)
60
+ .update(payloadString, 'utf8')
61
+ .digest('hex')
62
+
63
+ // Use timingSafeEqual to prevent timing attacks
64
+ try {
65
+ const signatureBuffer = Buffer.from(cleanSignature, 'hex')
66
+ const expectedBuffer = Buffer.from(expectedSignature, 'hex')
67
+
68
+ if (signatureBuffer.length !== expectedBuffer.length) {
69
+ return false
70
+ }
71
+
72
+ return crypto.timingSafeEqual(signatureBuffer, expectedBuffer)
73
+ } catch (error) {
74
+ return false
75
+ }
76
+ }
77
+
78
+ constructEvent (payload, signature, secret) {
79
+ if (!this.verifySignature(payload, signature, secret)) {
80
+ throw new Error('Invalid webhook signature')
81
+ }
82
+
83
+ try {
84
+ const event = typeof payload === 'string' ? JSON.parse(payload) : payload
85
+ return {
86
+ ...event,
87
+ verified: true,
88
+ timestamp: new Date().toISOString()
89
+ }
90
+ } catch (error) {
91
+ throw new Error('Invalid webhook payload: unable to parse JSON')
92
+ }
93
+ }
94
+ }
95
+
96
+ module.exports = WebhookService