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.
package/src/index.js CHANGED
@@ -9,676 +9,12 @@
9
9
 
10
10
  "use strict";
11
11
 
12
- // ─── Constants ───────────────────────────────────────────────────────────────
13
-
14
- const DEFAULT_BASE_URL = "https://api.bazik.io";
15
- const TOKEN_REFRESH_MARGIN_MS = 60 * 60 * 1000; // 1 hour before expiry
16
- const MAX_MONCASH_AMOUNT = 75_000;
17
-
18
- // ─── Errors ──────────────────────────────────────────────────────────────────
19
-
20
- class BazikError extends Error {
21
- /**
22
- * @param {string} message
23
- * @param {number} [status]
24
- * @param {string} [code]
25
- * @param {*} [details]
26
- */
27
- constructor(message, status, code, details) {
28
- super(message);
29
- this.name = "BazikError";
30
- this.status = status ?? null;
31
- this.code = code ?? null;
32
- this.details = details ?? null;
33
- }
34
- }
35
-
36
- class BazikAuthError extends BazikError {
37
- constructor(message, status, code, details) {
38
- super(message, status, code, details);
39
- this.name = "BazikAuthError";
40
- }
41
- }
42
-
43
- class BazikValidationError extends BazikError {
44
- constructor(message, details) {
45
- super(message, 400, "validation_error", details);
46
- this.name = "BazikValidationError";
47
- }
48
- }
49
-
50
- class BazikInsufficientFundsError extends BazikError {
51
- constructor(message, details) {
52
- super(message, 402, "insufficient_funds", details);
53
- this.name = "BazikInsufficientFundsError";
54
- }
55
- }
56
-
57
- class BazikRateLimitError extends BazikError {
58
- constructor(message, details) {
59
- super(message, 429, "rate_limit_exceeded", details);
60
- this.name = "BazikRateLimitError";
61
- }
62
- }
63
-
64
- // ─── Helpers ─────────────────────────────────────────────────────────────────
65
-
66
- /**
67
- * Validate that a wallet number is 8 or 11 digits.
68
- * @param {string} wallet
69
- */
70
- function validateWallet(wallet) {
71
- if (typeof wallet !== "string" || !/^\d{8}(\d{3})?$/.test(wallet)) {
72
- throw new BazikValidationError(
73
- `Invalid wallet number "${wallet}". Must be 8 or 11 digits.`
74
- );
75
- }
76
- }
77
-
78
- /**
79
- * Validate a positive numeric amount.
80
- * @param {number} amount
81
- * @param {number} [max]
82
- */
83
- function validateAmount(amount, max) {
84
- if (typeof amount !== "number" || !isFinite(amount) || amount <= 0) {
85
- throw new BazikValidationError(
86
- `Invalid amount: ${amount}. Must be a positive number.`
87
- );
88
- }
89
- if (max !== undefined && amount > max) {
90
- throw new BazikValidationError(
91
- `Amount ${amount} exceeds maximum of ${max} HTG.`
92
- );
93
- }
94
- }
95
-
96
- /**
97
- * Validate that required fields are present in an object.
98
- * @param {Record<string, *>} obj
99
- * @param {string[]} fields
100
- */
101
- function validateRequired(obj, fields) {
102
- const missing = fields.filter(
103
- (f) => obj[f] === undefined || obj[f] === null || obj[f] === ""
104
- );
105
- if (missing.length > 0) {
106
- throw new BazikValidationError(
107
- `Missing required field(s): ${missing.join(", ")}`
108
- );
109
- }
110
- }
111
-
112
- // ─── HTTP Client (zero dependencies) ─────────────────────────────────────────
113
-
114
- /**
115
- * Minimal fetch wrapper.
116
- * @param {string} url
117
- * @param {RequestInit & { timeout?: number }} options
118
- * @returns {Promise<{ status: number, data: * }>}
119
- */
120
- async function request(url, options = {}) {
121
- const { timeout = 30_000, ...fetchOptions } = options;
122
-
123
- const controller = new AbortController();
124
- const timer = setTimeout(() => controller.abort(), timeout);
125
-
126
- try {
127
- const res = await fetch(url, {
128
- ...fetchOptions,
129
- signal: controller.signal,
130
- });
131
-
132
- let data;
133
- const contentType = res.headers.get("content-type") || "";
134
- if (contentType.includes("application/json")) {
135
- data = await res.json();
136
- } else {
137
- data = await res.text();
138
- }
139
-
140
- return { status: res.status, data };
141
- } finally {
142
- clearTimeout(timer);
143
- }
144
- }
145
-
146
- // ─── Bazik Client ────────────────────────────────────────────────────────────
147
-
148
- class Bazik {
149
- #userID;
150
- #secretKey;
151
- #baseURL;
152
- #token;
153
- #tokenExpiresAt;
154
- #autoRefresh;
155
- #timeout;
156
- #onTokenRefresh;
157
-
158
- /**
159
- * Create a new Bazik client.
160
- *
161
- * @param {Object} config
162
- * @param {string} config.userID — Your Bazik user ID (e.g. "bzk_c5b754a0_1757383229")
163
- * @param {string} config.secretKey — Your secret key (e.g. "sk_...")
164
- * @param {string} [config.baseURL] — API base URL (default: https://api.bazik.io)
165
- * @param {boolean} [config.autoRefresh] — Automatically refresh token before expiry (default: true)
166
- * @param {number} [config.timeout] — Request timeout in ms (default: 30000)
167
- * @param {(token: string) => void} [config.onTokenRefresh] — Callback when token is refreshed
168
- *
169
- * @example
170
- * // CommonJS
171
- * const { Bazik } = require("bazik-sdk");
172
- *
173
- * // ESM
174
- * import { Bazik } from "bazik-sdk";
175
- *
176
- * const bazik = new Bazik({
177
- * userID: "bzk_c5b754a0_1757383229",
178
- * secretKey: "sk_5b0ff521b331c73db55313dc82f17cab",
179
- * });
180
- */
181
- constructor(config) {
182
- if (!config || !config.userID || !config.secretKey) {
183
- throw new BazikValidationError(
184
- "Both userID and secretKey are required to initialize the Bazik client."
185
- );
186
- }
187
-
188
- this.#userID = config.userID;
189
- this.#secretKey = config.secretKey;
190
- this.#baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/+$/, "");
191
- this.#autoRefresh = config.autoRefresh !== false;
192
- this.#timeout = config.timeout || 30_000;
193
- this.#onTokenRefresh = config.onTokenRefresh || null;
194
- this.#token = null;
195
- this.#tokenExpiresAt = 0;
196
-
197
- // Bind sub-modules
198
- this.payments = new Payments(this);
199
- this.transfers = new Transfers(this);
200
- this.wallet = new Wallet(this);
201
- }
202
-
203
- // ── Token management ────────────────────────────────────────────────────
204
-
205
- /**
206
- * Authenticate and obtain an access token.
207
- * The token is cached internally and reused for subsequent requests.
208
- *
209
- * @returns {Promise<{ success: boolean, token: string, user_id: string, expires_at: number, message: string }>}
210
- *
211
- * @example
212
- * const auth = await bazik.authenticate();
213
- * console.log("Token expires at:", new Date(auth.expires_at));
214
- */
215
- async authenticate() {
216
- const { status, data } = await request(`${this.#baseURL}/token`, {
217
- method: "POST",
218
- headers: { "Content-Type": "application/json" },
219
- timeout: this.#timeout,
220
- body: JSON.stringify({
221
- userID: this.#userID,
222
- secretKey: this.#secretKey,
223
- }),
224
- });
225
-
226
- if (status === 429) {
227
- throw new BazikRateLimitError(
228
- "Too many authentication attempts. Please wait before retrying.",
229
- data
230
- );
231
- }
232
-
233
- if (status === 401) {
234
- throw new BazikAuthError(
235
- data?.error?.message || "Invalid credentials.",
236
- status,
237
- data?.error?.code,
238
- data?.error?.details
239
- );
240
- }
241
-
242
- if (status !== 200 || !data?.token) {
243
- throw new BazikError(
244
- data?.error?.message || "Authentication failed.",
245
- status,
246
- data?.error?.code,
247
- data
248
- );
249
- }
250
-
251
- this.#token = data.token;
252
- this.#tokenExpiresAt = data.expires_at;
253
-
254
- if (this.#onTokenRefresh) {
255
- this.#onTokenRefresh(data.token);
256
- }
257
-
258
- return data;
259
- }
260
-
261
- /**
262
- * Returns true if the current token is still valid (with a safety margin).
263
- * @returns {boolean}
264
- */
265
- isTokenValid() {
266
- if (!this.#token) return false;
267
- return Date.now() < this.#tokenExpiresAt - TOKEN_REFRESH_MARGIN_MS;
268
- }
269
-
270
- /**
271
- * Get a valid token, refreshing if needed.
272
- * @returns {Promise<string>}
273
- */
274
- async getToken() {
275
- if (!this.#token || (this.#autoRefresh && !this.isTokenValid())) {
276
- await this.authenticate();
277
- }
278
- return this.#token;
279
- }
280
-
281
- /**
282
- * Internal: make an authenticated API request.
283
- * @param {string} method
284
- * @param {string} path
285
- * @param {*} [body]
286
- * @returns {Promise<*>}
287
- */
288
- async _request(method, path, body) {
289
- const token = await this.getToken();
290
-
291
- const headers = {
292
- Authorization: `Bearer ${token}`,
293
- "Content-Type": "application/json",
294
- };
295
-
296
- const opts = { method, headers, timeout: this.#timeout };
297
- if (body !== undefined) {
298
- opts.body = JSON.stringify(body);
299
- }
300
-
301
- const { status, data } = await request(
302
- `${this.#baseURL}${path}`,
303
- opts
304
- );
305
-
306
- // Handle common error statuses
307
- if (status === 401) {
308
- // Token may have expired — try one refresh
309
- if (this.#autoRefresh) {
310
- await this.authenticate();
311
- const retryHeaders = {
312
- Authorization: `Bearer ${this.#token}`,
313
- "Content-Type": "application/json",
314
- };
315
- const retry = await request(`${this.#baseURL}${path}`, {
316
- ...opts,
317
- headers: retryHeaders,
318
- });
319
- if (retry.status === 401) {
320
- throw new BazikAuthError(
321
- "Authentication failed after token refresh.",
322
- 401,
323
- "unauthorized",
324
- retry.data
325
- );
326
- }
327
- return retry.data;
328
- }
329
- throw new BazikAuthError(
330
- data?.error?.message || "Unauthorized.",
331
- 401,
332
- data?.error?.code,
333
- data
334
- );
335
- }
336
-
337
- if (status === 402) {
338
- throw new BazikInsufficientFundsError(
339
- data?.error?.message || "Insufficient funds.",
340
- data
341
- );
342
- }
343
-
344
- if (status === 429) {
345
- throw new BazikRateLimitError(
346
- data?.error?.message || "Rate limit exceeded.",
347
- data
348
- );
349
- }
350
-
351
- if (status >= 400) {
352
- throw new BazikError(
353
- data?.error?.message || data?.message || `Request failed with status ${status}`,
354
- status,
355
- data?.error?.code,
356
- data
357
- );
358
- }
359
-
360
- return data;
361
- }
362
- }
363
-
364
- // ─── Payments sub-module ─────────────────────────────────────────────────────
365
-
366
- class Payments {
367
- #client;
368
-
369
- constructor(client) {
370
- this.#client = client;
371
- }
372
-
373
- /**
374
- * Create a MonCash payment. The customer must be redirected to `redirectUrl`.
375
- *
376
- * @param {Object} params
377
- * @param {number} params.gdes — Amount in Gourdes (max 75,000)
378
- * @param {string} [params.successUrl] — Redirect URL on success
379
- * @param {string} [params.errorUrl] — Redirect URL on error
380
- * @param {string} [params.description] — Payment description
381
- * @param {string} [params.referenceId] — Your reference ID
382
- * @param {string} [params.customerFirstName]
383
- * @param {string} [params.customerLastName]
384
- * @param {string} [params.customerEmail]
385
- * @param {string} [params.webhookUrl] — Webhook for status updates
386
- * @param {Object} [params.metadata] — Arbitrary metadata
387
- * @returns {Promise<Object>} — Contains orderId, redirectUrl, status, etc.
388
- *
389
- * @example
390
- * const payment = await bazik.payments.create({
391
- * gdes: 1284.00,
392
- * successUrl: "https://mysite.com/success",
393
- * errorUrl: "https://mysite.com/error",
394
- * description: "iPhone Pro Max",
395
- * referenceId: "ORDER-001",
396
- * customerFirstName: "Franck",
397
- * customerLastName: "Jean",
398
- * customerEmail: "franck@example.com",
399
- * webhookUrl: "https://mysite.com/webhook",
400
- * });
401
- *
402
- * // Redirect the customer to complete the payment
403
- * console.log("Redirect to:", payment.redirectUrl);
404
- */
405
- async create(params) {
406
- validateRequired(params, ["gdes"]);
407
- validateAmount(params.gdes, MAX_MONCASH_AMOUNT);
408
-
409
- return this.#client._request("POST", "/moncash/token", params);
410
- }
411
-
412
- /**
413
- * Verify a payment status by order ID.
414
- *
415
- * @param {string} orderId — The orderId returned by `create()`
416
- * @returns {Promise<Object>} — Payment details with status, amount, metadata, etc.
417
- *
418
- * @example
419
- * const status = await bazik.payments.verify("BZK_production_98630749_1760032902277_2ibu");
420
- * if (status.status === "successful") {
421
- * console.log("Payment confirmed!");
422
- * }
423
- */
424
- async verify(orderId) {
425
- if (!orderId) {
426
- throw new BazikValidationError("orderId is required.");
427
- }
428
- return this.#client._request("GET", `/order/${encodeURIComponent(orderId)}`);
429
- }
430
-
431
- /**
432
- * Poll payment status until it resolves or times out.
433
- *
434
- * @param {string} orderId
435
- * @param {Object} [options]
436
- * @param {number} [options.intervalMs=5000] — Polling interval
437
- * @param {number} [options.timeoutMs=300000] — Max wait time (5 min)
438
- * @returns {Promise<Object>} — Final payment status
439
- *
440
- * @example
441
- * const result = await bazik.payments.waitForCompletion("BZK_...", {
442
- * intervalMs: 5000,
443
- * timeoutMs: 120000,
444
- * });
445
- */
446
- async waitForCompletion(orderId, options = {}) {
447
- const { intervalMs = 5000, timeoutMs = 300_000 } = options;
448
- const deadline = Date.now() + timeoutMs;
449
-
450
- while (Date.now() < deadline) {
451
- const payment = await this.verify(orderId);
452
- if (payment.status !== "pending") {
453
- return payment;
454
- }
455
- await new Promise((r) => setTimeout(r, intervalMs));
456
- }
457
-
458
- throw new BazikError(
459
- `Payment verification timed out after ${timeoutMs}ms.`,
460
- null,
461
- "timeout"
462
- );
463
- }
464
-
465
- /**
466
- * Send money to a MonCash wallet (withdraw / payout).
467
- *
468
- * @param {Object} params
469
- * @param {number} params.gdes — Amount in HTG
470
- * @param {string} params.wallet — Recipient phone (8 or 11 digits)
471
- * @param {string} params.customerFirstName — Recipient first name
472
- * @param {string} params.customerLastName — Recipient last name
473
- * @param {string} [params.description]
474
- * @param {string} [params.referenceId]
475
- * @param {string} [params.customerEmail]
476
- * @param {string} [params.webhookUrl]
477
- * @returns {Promise<Object>}
478
- *
479
- * @example
480
- * const withdrawal = await bazik.payments.withdraw({
481
- * gdes: 500,
482
- * wallet: "47556677",
483
- * customerFirstName: "Melissa",
484
- * customerLastName: "Francois",
485
- * description: "Weekly earnings",
486
- * });
487
- */
488
- async withdraw(params) {
489
- validateRequired(params, [
490
- "gdes",
491
- "wallet",
492
- "customerFirstName",
493
- "customerLastName",
494
- ]);
495
- validateAmount(params.gdes);
496
- validateWallet(params.wallet);
497
-
498
- return this.#client._request("POST", "/moncash/withdraw", params);
499
- }
500
-
501
- /**
502
- * Get account balance (MonCash).
503
- *
504
- * @returns {Promise<{ available: number, reserved: number, currency: string, environment: string, last_updated: string }>}
505
- *
506
- * @example
507
- * const balance = await bazik.payments.getBalance();
508
- * console.log(`Available: ${balance.available} ${balance.currency}`);
509
- */
510
- async getBalance() {
511
- return this.#client._request("GET", "/balance");
512
- }
513
- }
514
-
515
- // ─── Transfers sub-module ────────────────────────────────────────────────────
516
-
517
- class Transfers {
518
- #client;
519
-
520
- constructor(client) {
521
- this.#client = client;
522
- }
523
-
524
- /**
525
- * Check MonCash customer/wallet status before sending a transfer.
526
- *
527
- * @param {string} wallet — MonCash phone number (8 digits)
528
- * @returns {Promise<Object>} — Customer KYC level and status flags
529
- *
530
- * @example
531
- * const status = await bazik.transfers.checkCustomer("37123456");
532
- * console.log(status.customerStatus.type); // "fullkyc"
533
- */
534
- async checkCustomer(wallet) {
535
- validateWallet(wallet);
536
- return this.#client._request("POST", "/moncash/customers/status", {
537
- wallet,
538
- });
539
- }
540
-
541
- /**
542
- * Create a MonCash transfer (send money to a wallet).
543
- *
544
- * @param {Object} params
545
- * @param {number} params.gdes — Amount in HTG
546
- * @param {string} params.wallet — Recipient phone (8 digits)
547
- * @param {string} params.customerFirstName
548
- * @param {string} params.customerLastName
549
- * @param {string} [params.description]
550
- * @param {string} [params.referenceId]
551
- * @param {string} [params.customerEmail]
552
- * @param {string} [params.webhookUrl]
553
- * @returns {Promise<Object>}
554
- *
555
- * @example
556
- * const transfer = await bazik.transfers.moncash({
557
- * gdes: 500,
558
- * wallet: "47556677",
559
- * customerFirstName: "Melissa",
560
- * customerLastName: "Francois",
561
- * description: "Pou ou peye lekol la",
562
- * });
563
- * console.log(transfer.transaction_id); // "TRF_..."
564
- */
565
- async moncash(params) {
566
- validateRequired(params, [
567
- "gdes",
568
- "wallet",
569
- "customerFirstName",
570
- "customerLastName",
571
- ]);
572
- validateAmount(params.gdes);
573
- validateWallet(params.wallet);
574
-
575
- return this.#client._request("POST", "/moncash/transfers", params);
576
- }
577
-
578
- /**
579
- * Create a NatCash transfer.
580
- *
581
- * @param {Object} params
582
- * @param {number} params.gdes — Amount in HTG
583
- * @param {string} params.wallet — Recipient phone (8 digits)
584
- * @param {string} params.customerFirstName
585
- * @param {string} params.customerLastName
586
- * @param {string} [params.description]
587
- * @param {string} [params.referenceId]
588
- * @param {string} [params.customerEmail]
589
- * @param {string} [params.webhookUrl]
590
- * @returns {Promise<Object>}
591
- *
592
- * @example
593
- * const transfer = await bazik.transfers.natcash({
594
- * gdes: 50,
595
- * wallet: "44556677",
596
- * customerFirstName: "Marie",
597
- * customerLastName: "Pierre",
598
- * });
599
- */
600
- async natcash(params) {
601
- validateRequired(params, [
602
- "gdes",
603
- "wallet",
604
- "customerFirstName",
605
- "customerLastName",
606
- ]);
607
- validateAmount(params.gdes);
608
- validateWallet(params.wallet);
609
-
610
- return this.#client._request("POST", "/natcash/transfers", params);
611
- }
612
-
613
- /**
614
- * Get transfer status by transaction ID.
615
- *
616
- * @param {string} transactionId — e.g. "TRF_1761961466_eafd0ac3"
617
- * @returns {Promise<Object>}
618
- *
619
- * @example
620
- * const status = await bazik.transfers.getStatus("TRF_1761961466_eafd0ac3");
621
- * if (status.status === "successful") {
622
- * console.log("Transfer completed!");
623
- * }
624
- */
625
- async getStatus(transactionId) {
626
- if (!transactionId) {
627
- throw new BazikValidationError("transactionId is required.");
628
- }
629
- return this.#client._request(
630
- "GET",
631
- `/transfers/${encodeURIComponent(transactionId)}`
632
- );
633
- }
634
-
635
- /**
636
- * Get a fee quote before creating a transfer.
637
- *
638
- * @param {number} amount — Delivery amount in HTG
639
- * @param {"moncash"|"natcash"} provider
640
- * @returns {Promise<{ delivery_amount: number, fee: number, total_cost: number, currency: string, provider: string, fee_percentage: number }>}
641
- *
642
- * @example
643
- * const quote = await bazik.transfers.getQuote(1000, "moncash");
644
- * console.log(`Fee: ${quote.fee} HTG | Total: ${quote.total_cost} HTG`);
645
- */
646
- async getQuote(amount, provider) {
647
- validateAmount(amount);
648
- if (!["moncash", "natcash"].includes(provider)) {
649
- throw new BazikValidationError(
650
- `Invalid provider "${provider}". Must be "moncash" or "natcash".`
651
- );
652
- }
653
- return this.#client._request("POST", "/transfers/quote", {
654
- amount,
655
- provider,
656
- });
657
- }
658
- }
659
-
660
- // ─── Wallet sub-module ───────────────────────────────────────────────────────
661
-
662
- class Wallet {
663
- #client;
664
-
665
- constructor(client) {
666
- this.#client = client;
667
- }
668
-
669
- /**
670
- * Get wallet balance.
671
- *
672
- * @returns {Promise<{ available: number, reserved: number, currency: string, environment: string, last_updated: string }>}
673
- *
674
- * @example
675
- * const wallet = await bazik.wallet.getBalance();
676
- * console.log(`Available: ${wallet.available} HTG`);
677
- */
678
- async getBalance() {
679
- return this.#client._request("GET", "/wallet");
680
- }
681
- }
12
+ const Bazik = require("./modules/Bazik");
13
+ const BazikError = require("./errors/BazikError");
14
+ const BazikAuthError = require("./errors/BazikAuthError");
15
+ const BazikValidationError = require("./errors/BazikValidationError");
16
+ const BazikInsufficientFundsError = require("./errors/BazikInsufficientFundsError");
17
+ const BazikRateLimitError = require("./errors/BazikRateLimitError");
682
18
 
683
19
  // ─── Exports ─────────────────────────────────────────────────────────────────
684
20