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 +47 -3
- package/package.json +6 -2
- package/src/client.js +96 -0
- package/src/error.js +10 -0
- package/src/index.js +132 -0
- package/src/services/BankService.js +22 -0
- package/src/services/CollectionService.js +37 -0
- package/src/services/CurrencyService.js +11 -0
- package/src/services/CustomerService.js +308 -0
- package/src/services/FeesService.js +18 -0
- package/src/services/FileService.js +18 -0
- package/src/services/PayoutService.js +31 -0
- package/src/services/TransactionService.js +18 -0
- package/src/services/VirtualBankAccountService.js +30 -0
- package/src/services/WalletService.js +18 -0
- package/src/services/WebhookService.js +96 -0
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
|
|
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.
|
|
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
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,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
|