bazik-sdk 1.0.1 → 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.
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+
3
+ const {
4
+ DEFAULT_BASE_URL,
5
+ TOKEN_REFRESH_MARGIN_MS,
6
+ } = require("../constants");
7
+ const BazikError = require("../errors/BazikError");
8
+ const BazikAuthError = require("../errors/BazikAuthError");
9
+ const BazikValidationError = require("../errors/BazikValidationError");
10
+ const BazikInsufficientFundsError = require("../errors/BazikInsufficientFundsError");
11
+ const BazikRateLimitError = require("../errors/BazikRateLimitError");
12
+ const request = require("../http/request");
13
+ const Payments = require("./Payments");
14
+ const Transfers = require("./Transfers");
15
+ const Wallet = require("./Wallet");
16
+
17
+ // ─── Bazik Client ────────────────────────────────────────────────────────────
18
+
19
+ class Bazik {
20
+ #userID;
21
+ #secretKey;
22
+ #baseURL;
23
+ #token;
24
+ #tokenExpiresAt;
25
+ #autoRefresh;
26
+ #timeout;
27
+ #onTokenRefresh;
28
+
29
+ /**
30
+ * Create a new Bazik client.
31
+ *
32
+ * @param {Object} config
33
+ * @param {string} config.userID — Your Bazik user ID (e.g. "bzk_c5b754a0_1757383229")
34
+ * @param {string} config.secretKey — Your secret key (e.g. "sk_...")
35
+ * @param {string} [config.baseURL] — API base URL (default: https://api.bazik.io)
36
+ * @param {boolean} [config.autoRefresh] — Automatically refresh token before expiry (default: true)
37
+ * @param {number} [config.timeout] — Request timeout in ms (default: 30000)
38
+ * @param {(token: string) => void} [config.onTokenRefresh] — Callback when token is refreshed
39
+ *
40
+ * @example
41
+ * // CommonJS
42
+ * const { Bazik } = require("bazik-sdk");
43
+ *
44
+ * // ESM
45
+ * import { Bazik } from "bazik-sdk";
46
+ *
47
+ * const bazik = new Bazik({
48
+ * userID: "bzk_c5b754a0_1757383229",
49
+ * secretKey: "sk_5b0ff521b331c73db55313dc82f17cab",
50
+ * });
51
+ */
52
+ constructor(config) {
53
+ if (!config || !config.userID || !config.secretKey) {
54
+ throw new BazikValidationError(
55
+ "Both userID and secretKey are required to initialize the Bazik client."
56
+ );
57
+ }
58
+
59
+ this.#userID = config.userID;
60
+ this.#secretKey = config.secretKey;
61
+ this.#baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/+$/, "");
62
+ this.#autoRefresh = config.autoRefresh !== false;
63
+ this.#timeout = config.timeout || 30_000;
64
+ this.#onTokenRefresh = config.onTokenRefresh || null;
65
+ this.#token = null;
66
+ this.#tokenExpiresAt = 0;
67
+
68
+ // Bind sub-modules
69
+ this.payments = new Payments(this);
70
+ this.transfers = new Transfers(this);
71
+ this.wallet = new Wallet(this);
72
+ }
73
+
74
+ // ── Token management ────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Authenticate and obtain an access token.
78
+ * The token is cached internally and reused for subsequent requests.
79
+ *
80
+ * @returns {Promise<{ success: boolean, token: string, user_id: string, expires_at: number, message: string }>}
81
+ *
82
+ * @example
83
+ * const auth = await bazik.authenticate();
84
+ * console.log("Token expires at:", new Date(auth.expires_at));
85
+ */
86
+ async authenticate() {
87
+ const { status, data } = await request(`${this.#baseURL}/token`, {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json" },
90
+ timeout: this.#timeout,
91
+ body: JSON.stringify({
92
+ userID: this.#userID,
93
+ secretKey: this.#secretKey,
94
+ }),
95
+ });
96
+
97
+ if (status === 429) {
98
+ throw new BazikRateLimitError(
99
+ "Too many authentication attempts. Please wait before retrying.",
100
+ data
101
+ );
102
+ }
103
+
104
+ if (status === 401) {
105
+ throw new BazikAuthError(
106
+ data?.error?.message || "Invalid credentials.",
107
+ status,
108
+ data?.error?.code,
109
+ data?.error?.details
110
+ );
111
+ }
112
+
113
+ if (status !== 200 || !data?.token) {
114
+ throw new BazikError(
115
+ data?.error?.message || "Authentication failed.",
116
+ status,
117
+ data?.error?.code,
118
+ data
119
+ );
120
+ }
121
+
122
+ this.#token = data.token;
123
+ this.#tokenExpiresAt = data.expires_at;
124
+
125
+ if (this.#onTokenRefresh) {
126
+ this.#onTokenRefresh(data.token);
127
+ }
128
+
129
+ return data;
130
+ }
131
+
132
+ /**
133
+ * Returns true if the current token is still valid (with a safety margin).
134
+ * @returns {boolean}
135
+ */
136
+ isTokenValid() {
137
+ if (!this.#token) return false;
138
+ return Date.now() < this.#tokenExpiresAt - TOKEN_REFRESH_MARGIN_MS;
139
+ }
140
+
141
+ /**
142
+ * Get a valid token, refreshing if needed.
143
+ * @returns {Promise<string>}
144
+ */
145
+ async getToken() {
146
+ if (!this.#token || (this.#autoRefresh && !this.isTokenValid())) {
147
+ await this.authenticate();
148
+ }
149
+ return this.#token;
150
+ }
151
+
152
+ /**
153
+ * Internal: make an authenticated API request.
154
+ * @param {string} method
155
+ * @param {string} path
156
+ * @param {*} [body]
157
+ * @returns {Promise<*>}
158
+ */
159
+ async _request(method, path, body) {
160
+ const token = await this.getToken();
161
+
162
+ const headers = {
163
+ Authorization: `Bearer ${token}`,
164
+ "Content-Type": "application/json",
165
+ };
166
+
167
+ const opts = { method, headers, timeout: this.#timeout };
168
+ if (body !== undefined) {
169
+ opts.body = JSON.stringify(body);
170
+ }
171
+
172
+ const { status, data } = await request(
173
+ `${this.#baseURL}${path}`,
174
+ opts
175
+ );
176
+
177
+ // Handle common error statuses
178
+ if (status === 401) {
179
+ // Token may have expired — try one refresh
180
+ if (this.#autoRefresh) {
181
+ await this.authenticate();
182
+ const retryHeaders = {
183
+ Authorization: `Bearer ${this.#token}`,
184
+ "Content-Type": "application/json",
185
+ };
186
+ const retry = await request(`${this.#baseURL}${path}`, {
187
+ ...opts,
188
+ headers: retryHeaders,
189
+ });
190
+ if (retry.status === 401) {
191
+ throw new BazikAuthError(
192
+ "Authentication failed after token refresh.",
193
+ 401,
194
+ "unauthorized",
195
+ retry.data
196
+ );
197
+ }
198
+ return retry.data;
199
+ }
200
+ throw new BazikAuthError(
201
+ data?.error?.message || "Unauthorized.",
202
+ 401,
203
+ data?.error?.code,
204
+ data
205
+ );
206
+ }
207
+
208
+ if (status === 402) {
209
+ throw new BazikInsufficientFundsError(
210
+ data?.error?.message || "Insufficient funds.",
211
+ data
212
+ );
213
+ }
214
+
215
+ if (status === 429) {
216
+ throw new BazikRateLimitError(
217
+ data?.error?.message || "Rate limit exceeded.",
218
+ data
219
+ );
220
+ }
221
+
222
+ if (status >= 400) {
223
+ throw new BazikError(
224
+ data?.error?.message || data?.message || `Request failed with status ${status}`,
225
+ status,
226
+ data?.error?.code,
227
+ data
228
+ );
229
+ }
230
+
231
+ return data;
232
+ }
233
+ }
234
+
235
+ module.exports = Bazik;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ const validateRequired = require("../helpers/validateRequired");
4
+ const validateAmount = require("../helpers/validateAmount");
5
+ const validateWallet = require("../helpers/validateWallet");
6
+ const { MAX_MONCASH_AMOUNT } = require("../constants");
7
+ const BazikError = require("../errors/BazikError");
8
+ const BazikValidationError = require("../errors/BazikValidationError");
9
+
10
+ // ─── Payments sub-module ─────────────────────────────────────────────────────
11
+
12
+ class Payments {
13
+ #client;
14
+
15
+ constructor(client) {
16
+ this.#client = client;
17
+ }
18
+
19
+ /**
20
+ * Create a MonCash payment. The customer must be redirected to `redirectUrl`.
21
+ *
22
+ * @param {Object} params
23
+ * @param {number} params.gdes — Amount in Gourdes (max 75,000)
24
+ * @param {string} [params.successUrl] — Redirect URL on success
25
+ * @param {string} [params.errorUrl] — Redirect URL on error
26
+ * @param {string} [params.description] — Payment description
27
+ * @param {string} [params.referenceId] — Your reference ID
28
+ * @param {string} [params.customerFirstName]
29
+ * @param {string} [params.customerLastName]
30
+ * @param {string} [params.customerEmail]
31
+ * @param {string} [params.webhookUrl] — Webhook for status updates
32
+ * @param {Object} [params.metadata] — Arbitrary metadata
33
+ * @returns {Promise<Object>} — Contains orderId, redirectUrl, status, etc.
34
+ *
35
+ * @example
36
+ * const payment = await bazik.payments.create({
37
+ * gdes: 1284.00,
38
+ * successUrl: "https://mysite.com/success",
39
+ * errorUrl: "https://mysite.com/error",
40
+ * description: "iPhone Pro Max",
41
+ * referenceId: "ORDER-001",
42
+ * customerFirstName: "Franck",
43
+ * customerLastName: "Jean",
44
+ * customerEmail: "franck@example.com",
45
+ * webhookUrl: "https://mysite.com/webhook",
46
+ * });
47
+ *
48
+ * // Redirect the customer to complete the payment
49
+ * console.log("Redirect to:", payment.redirectUrl);
50
+ */
51
+ async create(params) {
52
+ validateRequired(params, ["gdes"]);
53
+ validateAmount(params.gdes, MAX_MONCASH_AMOUNT);
54
+
55
+ return this.#client._request("POST", "/moncash/token", params);
56
+ }
57
+
58
+ /**
59
+ * Verify a payment status by order ID.
60
+ *
61
+ * @param {string} orderId — The orderId returned by `create()`
62
+ * @returns {Promise<Object>} — Payment details with status, amount, metadata, etc.
63
+ *
64
+ * @example
65
+ * const status = await bazik.payments.verify("BZK_production_98630749_1760032902277_2ibu");
66
+ * if (status.status === "successful") {
67
+ * console.log("Payment confirmed!");
68
+ * }
69
+ */
70
+ async verify(orderId) {
71
+ if (!orderId) {
72
+ throw new BazikValidationError("orderId is required.");
73
+ }
74
+ return this.#client._request("GET", `/order/${encodeURIComponent(orderId)}`);
75
+ }
76
+
77
+ /**
78
+ * Poll payment status until it resolves or times out.
79
+ *
80
+ * @param {string} orderId
81
+ * @param {Object} [options]
82
+ * @param {number} [options.intervalMs=5000] — Polling interval
83
+ * @param {number} [options.timeoutMs=300000] — Max wait time (5 min)
84
+ * @returns {Promise<Object>} — Final payment status
85
+ *
86
+ * @example
87
+ * const result = await bazik.payments.waitForCompletion("BZK_...", {
88
+ * intervalMs: 5000,
89
+ * timeoutMs: 120000,
90
+ * });
91
+ */
92
+ async waitForCompletion(orderId, options = {}) {
93
+ const { intervalMs = 5000, timeoutMs = 300_000 } = options;
94
+ const deadline = Date.now() + timeoutMs;
95
+
96
+ while (Date.now() < deadline) {
97
+ const payment = await this.verify(orderId);
98
+ if (payment.status !== "pending") {
99
+ return payment;
100
+ }
101
+ await new Promise((r) => setTimeout(r, intervalMs));
102
+ }
103
+
104
+ throw new BazikError(
105
+ `Payment verification timed out after ${timeoutMs}ms.`,
106
+ null,
107
+ "timeout"
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Send money to a MonCash wallet (withdraw / payout).
113
+ *
114
+ * @param {Object} params
115
+ * @param {number} params.gdes — Amount in HTG
116
+ * @param {string} params.wallet — Recipient phone (8 or 11 digits)
117
+ * @param {string} params.customerFirstName — Recipient first name
118
+ * @param {string} params.customerLastName — Recipient last name
119
+ * @param {string} [params.description]
120
+ * @param {string} [params.referenceId]
121
+ * @param {string} [params.customerEmail]
122
+ * @param {string} [params.webhookUrl]
123
+ * @returns {Promise<Object>}
124
+ *
125
+ * @example
126
+ * const withdrawal = await bazik.payments.withdraw({
127
+ * gdes: 500,
128
+ * wallet: "47556677",
129
+ * customerFirstName: "Melissa",
130
+ * customerLastName: "Francois",
131
+ * description: "Weekly earnings",
132
+ * });
133
+ */
134
+ async withdraw(params) {
135
+ validateRequired(params, [
136
+ "gdes",
137
+ "wallet",
138
+ "customerFirstName",
139
+ "customerLastName",
140
+ ]);
141
+ validateAmount(params.gdes);
142
+ validateWallet(params.wallet);
143
+
144
+ return this.#client._request("POST", "/moncash/withdraw", params);
145
+ }
146
+
147
+ /**
148
+ * Get account balance (MonCash).
149
+ *
150
+ * @returns {Promise<{ available: number, reserved: number, currency: string, environment: string, last_updated: string }>}
151
+ *
152
+ * @example
153
+ * const balance = await bazik.payments.getBalance();
154
+ * console.log(`Available: ${balance.available} ${balance.currency}`);
155
+ */
156
+ async getBalance() {
157
+ return this.#client._request("GET", "/balance");
158
+ }
159
+ }
160
+
161
+ module.exports = Payments;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+
3
+ const validateRequired = require("../helpers/validateRequired");
4
+ const validateAmount = require("../helpers/validateAmount");
5
+ const validateWallet = require("../helpers/validateWallet");
6
+ const BazikValidationError = require("../errors/BazikValidationError");
7
+
8
+ // ─── Transfers sub-module ────────────────────────────────────────────────────
9
+
10
+ class Transfers {
11
+ #client;
12
+
13
+ constructor(client) {
14
+ this.#client = client;
15
+ }
16
+
17
+ /**
18
+ * Check MonCash customer/wallet status before sending a transfer.
19
+ *
20
+ * @param {string} wallet — MonCash phone number (8 digits)
21
+ * @returns {Promise<Object>} — Customer KYC level and status flags
22
+ *
23
+ * @example
24
+ * const status = await bazik.transfers.checkCustomer("37123456");
25
+ * console.log(status.customerStatus.type); // "fullkyc"
26
+ */
27
+ async checkCustomer(wallet) {
28
+ validateWallet(wallet);
29
+ return this.#client._request("POST", "/moncash/customers/status", {
30
+ wallet,
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Create a MonCash transfer (send money to a wallet).
36
+ *
37
+ * @param {Object} params
38
+ * @param {number} params.gdes — Amount in HTG
39
+ * @param {string} params.wallet — Recipient phone (8 digits)
40
+ * @param {string} params.customerFirstName
41
+ * @param {string} params.customerLastName
42
+ * @param {string} [params.description]
43
+ * @param {string} [params.referenceId]
44
+ * @param {string} [params.customerEmail]
45
+ * @param {string} [params.webhookUrl]
46
+ * @returns {Promise<Object>}
47
+ *
48
+ * @example
49
+ * const transfer = await bazik.transfers.moncash({
50
+ * gdes: 500,
51
+ * wallet: "47556677",
52
+ * customerFirstName: "Melissa",
53
+ * customerLastName: "Francois",
54
+ * description: "Pou ou peye lekol la",
55
+ * });
56
+ * console.log(transfer.transaction_id); // "TRF_..."
57
+ */
58
+ async moncash(params) {
59
+ validateRequired(params, [
60
+ "gdes",
61
+ "wallet",
62
+ "customerFirstName",
63
+ "customerLastName",
64
+ ]);
65
+ validateAmount(params.gdes);
66
+ validateWallet(params.wallet);
67
+
68
+ return this.#client._request("POST", "/moncash/transfers", params);
69
+ }
70
+
71
+ /**
72
+ * Create a NatCash transfer.
73
+ *
74
+ * @param {Object} params
75
+ * @param {number} params.gdes — Amount in HTG
76
+ * @param {string} params.wallet — Recipient phone (8 digits)
77
+ * @param {string} params.customerFirstName
78
+ * @param {string} params.customerLastName
79
+ * @param {string} [params.description]
80
+ * @param {string} [params.referenceId]
81
+ * @param {string} [params.customerEmail]
82
+ * @param {string} [params.webhookUrl]
83
+ * @returns {Promise<Object>}
84
+ *
85
+ * @example
86
+ * const transfer = await bazik.transfers.natcash({
87
+ * gdes: 50,
88
+ * wallet: "44556677",
89
+ * customerFirstName: "Marie",
90
+ * customerLastName: "Pierre",
91
+ * });
92
+ */
93
+ async natcash(params) {
94
+ validateRequired(params, [
95
+ "gdes",
96
+ "wallet",
97
+ "customerFirstName",
98
+ "customerLastName",
99
+ ]);
100
+ validateAmount(params.gdes);
101
+ validateWallet(params.wallet);
102
+
103
+ return this.#client._request("POST", "/natcash/transfers", params);
104
+ }
105
+
106
+ /**
107
+ * Get transfer status by transaction ID.
108
+ *
109
+ * @param {string} transactionId — e.g. "TRF_1761961466_eafd0ac3"
110
+ * @returns {Promise<Object>}
111
+ *
112
+ * @example
113
+ * const status = await bazik.transfers.getStatus("TRF_1761961466_eafd0ac3");
114
+ * if (status.status === "successful") {
115
+ * console.log("Transfer completed!");
116
+ * }
117
+ */
118
+ async getStatus(transactionId) {
119
+ if (!transactionId) {
120
+ throw new BazikValidationError("transactionId is required.");
121
+ }
122
+ return this.#client._request(
123
+ "GET",
124
+ `/transfers/${encodeURIComponent(transactionId)}`
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Get a fee quote before creating a transfer.
130
+ *
131
+ * @param {number} amount — Delivery amount in HTG
132
+ * @param {"moncash"|"natcash"} provider
133
+ * @returns {Promise<{ delivery_amount: number, fee: number, total_cost: number, currency: string, provider: string, fee_percentage: number }>}
134
+ *
135
+ * @example
136
+ * const quote = await bazik.transfers.getQuote(1000, "moncash");
137
+ * console.log(`Fee: ${quote.fee} HTG | Total: ${quote.total_cost} HTG`);
138
+ */
139
+ async getQuote(amount, provider) {
140
+ validateAmount(amount);
141
+ if (!["moncash", "natcash"].includes(provider)) {
142
+ throw new BazikValidationError(
143
+ `Invalid provider "${provider}". Must be "moncash" or "natcash".`
144
+ );
145
+ }
146
+ return this.#client._request("POST", "/transfers/quote", {
147
+ amount,
148
+ provider,
149
+ });
150
+ }
151
+ }
152
+
153
+ module.exports = Transfers;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ // ─── Wallet sub-module ───────────────────────────────────────────────────────
4
+
5
+ class Wallet {
6
+ #client;
7
+
8
+ constructor(client) {
9
+ this.#client = client;
10
+ }
11
+
12
+ /**
13
+ * Get wallet balance.
14
+ *
15
+ * @returns {Promise<{ available: number, reserved: number, currency: string, environment: string, last_updated: string }>}
16
+ *
17
+ * @example
18
+ * const wallet = await bazik.wallet.getBalance();
19
+ * console.log(`Available: ${wallet.available} HTG`);
20
+ */
21
+ async getBalance() {
22
+ return this.#client._request("GET", "/wallet");
23
+ }
24
+ }
25
+
26
+ module.exports = Wallet;