@syscli/oneclickdz 0.1.0

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/dist/index.cjs ADDED
@@ -0,0 +1,934 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var OneClickError = class extends Error {
5
+ /** HTTP status, or undefined for a client-side or network error. */
6
+ status;
7
+ code;
8
+ details;
9
+ requestId;
10
+ body;
11
+ constructor(message, code, options = {}) {
12
+ super(
13
+ message,
14
+ options.cause !== void 0 ? { cause: options.cause } : void 0
15
+ );
16
+ this.name = new.target.name;
17
+ this.code = code;
18
+ this.status = options.status;
19
+ this.details = options.details;
20
+ this.requestId = options.requestId;
21
+ this.body = options.body;
22
+ }
23
+ };
24
+ var AuthError = class extends OneClickError {
25
+ };
26
+ var ForbiddenError = class extends OneClickError {
27
+ };
28
+ var InsufficientBalanceError = class extends ForbiddenError {
29
+ };
30
+ var DuplicateRefError = class extends ForbiddenError {
31
+ };
32
+ var ValidationError = class extends OneClickError {
33
+ issues;
34
+ constructor(message, options = {}) {
35
+ super(message, options.code ?? "ERR_VALIDATION", options);
36
+ this.issues = options.issues ?? [];
37
+ }
38
+ };
39
+ var NotFoundError = class extends OneClickError {
40
+ };
41
+ var RateLimitError = class extends OneClickError {
42
+ retryAfter;
43
+ constructor(message, options = {}) {
44
+ super(message, "RATE_LIMIT_EXCEEDED", options);
45
+ this.retryAfter = options.retryAfter;
46
+ }
47
+ };
48
+ var ServiceError = class extends OneClickError {
49
+ };
50
+ var NetworkError = class extends OneClickError {
51
+ constructor(message, options = {}) {
52
+ super(message, "network", options);
53
+ }
54
+ };
55
+ var RequestError = class extends OneClickError {
56
+ };
57
+ function errorFromResponse(status, body, retryAfter) {
58
+ const apiError = readApiError(body);
59
+ const code = apiError.code;
60
+ const message = apiError.message ?? `Request failed with status ${status}.`;
61
+ const options = {
62
+ status,
63
+ body,
64
+ requestId: readRequestId(body),
65
+ details: apiError.details
66
+ };
67
+ switch (code) {
68
+ case "MISSING_ACCESS_TOKEN":
69
+ case "INVALID_ACCESS_TOKEN":
70
+ case "ERR_AUTH":
71
+ return new AuthError(message, code, options);
72
+ case "NO_BALANCE":
73
+ case "INSUFFICIENT_BALANCE":
74
+ return new InsufficientBalanceError(message, code, options);
75
+ case "DUPLICATED_REF":
76
+ return new DuplicateRefError(message, code, options);
77
+ case "IP_BLOCKED":
78
+ case "IP_NOT_ALLOWED":
79
+ return new ForbiddenError(message, code, options);
80
+ case "ERR_VALIDATION":
81
+ case "ERR_PHONE":
82
+ case "ERR_STOCK":
83
+ return new ValidationError(message, { ...options, code });
84
+ case "NOT_FOUND":
85
+ return new NotFoundError(message, code, options);
86
+ case "RATE_LIMIT_EXCEEDED":
87
+ return new RateLimitError(message, { ...options, retryAfter });
88
+ case "INTERNAL_SERVER_ERROR":
89
+ case "INTERNAL_ERROR":
90
+ case "ERR_SERVICE":
91
+ return new ServiceError(message, code, options);
92
+ }
93
+ return errorFromStatus(status, message, options, code, retryAfter);
94
+ }
95
+ function errorFromStatus(status, message, options, code, retryAfter) {
96
+ if (status === 401) return new AuthError(message, code ?? "ERR_AUTH", options);
97
+ if (status === 403)
98
+ return new ForbiddenError(message, code ?? "ERR_AUTH", options);
99
+ if (status === 404)
100
+ return new NotFoundError(message, code ?? "NOT_FOUND", options);
101
+ if (status === 429)
102
+ return new RateLimitError(message, { ...options, retryAfter });
103
+ if (status >= 500)
104
+ return new ServiceError(message, code ?? "INTERNAL_SERVER_ERROR", options);
105
+ return new RequestError(message, code ?? "ERR_VALIDATION", options);
106
+ }
107
+ function readApiError(body) {
108
+ if (body && typeof body === "object" && "error" in body) {
109
+ const inner = body.error;
110
+ if (inner && typeof inner === "object") {
111
+ const e = inner;
112
+ return {
113
+ code: typeof e.code === "string" ? e.code : void 0,
114
+ message: typeof e.message === "string" ? e.message : void 0,
115
+ details: e.details
116
+ };
117
+ }
118
+ }
119
+ return {};
120
+ }
121
+ function readRequestId(body) {
122
+ if (body && typeof body === "object" && "requestId" in body) {
123
+ const id = body.requestId;
124
+ if (typeof id === "string") return id;
125
+ }
126
+ return void 0;
127
+ }
128
+
129
+ // src/http.ts
130
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
131
+ function encodeQuery(params) {
132
+ if (!params) return "";
133
+ const parts = [];
134
+ for (const [key, value] of Object.entries(params)) {
135
+ if (value === void 0 || value === null) continue;
136
+ parts.push(
137
+ `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
138
+ );
139
+ }
140
+ return parts.length ? `?${parts.join("&")}` : "";
141
+ }
142
+ function buildHeaders(config, hasBody) {
143
+ const headers = {
144
+ "X-Access-Token": config.key,
145
+ Accept: "application/json"
146
+ };
147
+ if (hasBody) headers["Content-Type"] = "application/json";
148
+ return headers;
149
+ }
150
+ function parseRetryAfter(value) {
151
+ if (!value) return void 0;
152
+ const seconds = Number(value);
153
+ if (Number.isFinite(seconds)) return Math.max(0, seconds);
154
+ const at = Date.parse(value);
155
+ if (Number.isNaN(at)) return void 0;
156
+ return Math.max(0, Math.round((at - Date.now()) / 1e3));
157
+ }
158
+ var HttpClient = class {
159
+ config;
160
+ constructor(config) {
161
+ this.config = {
162
+ ...config,
163
+ baseUrl: config.baseUrl.endsWith("/") ? config.baseUrl : `${config.baseUrl}/`
164
+ };
165
+ }
166
+ /**
167
+ * Send a request and return the unwrapped envelope.
168
+ *
169
+ * Retry policy is deliberately narrow. A 429 is always safe to retry, since
170
+ * the request was rejected before processing. A 5xx or a dropped connection is
171
+ * retried only for GET: a send that timed out may still have gone through, and
172
+ * a blind retry could charge the account twice. Use a `ref` for safe resends.
173
+ */
174
+ async request(method, path, options = {}) {
175
+ const maxAttempts = Math.max(1, this.config.retry.attempts);
176
+ const idempotent = method === "GET";
177
+ for (let attempt = 1; ; attempt++) {
178
+ try {
179
+ return await this.send(method, path, options);
180
+ } catch (error) {
181
+ if (attempt >= maxAttempts || !this.retryable(error, idempotent)) {
182
+ throw error;
183
+ }
184
+ await sleep(this.backoffMs(error, attempt));
185
+ }
186
+ }
187
+ }
188
+ retryable(error, idempotent) {
189
+ if (error instanceof RateLimitError) return true;
190
+ if (error instanceof ServiceError) return idempotent;
191
+ if (error instanceof NetworkError) return idempotent;
192
+ return false;
193
+ }
194
+ /** Wait the API's Retry-After when offered, otherwise an exponential backoff. */
195
+ backoffMs(error, attempt) {
196
+ if (error instanceof RateLimitError && this.config.retry.respectRetryAfter && error.retryAfter !== void 0) {
197
+ return error.retryAfter * 1e3;
198
+ }
199
+ return Math.min(2 ** (attempt - 1) * 500, 3e4);
200
+ }
201
+ async send(method, path, options) {
202
+ const hasBody = options.body !== void 0;
203
+ const url = new URL(path, this.config.baseUrl);
204
+ const target = `${url.toString()}${encodeQuery(options.query)}`;
205
+ const headers = buildHeaders(this.config, hasBody);
206
+ const controller = new AbortController();
207
+ const timer = this.config.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.config.timeoutMs) : void 0;
208
+ if (options.signal) {
209
+ if (options.signal.aborted) controller.abort();
210
+ else
211
+ options.signal.addEventListener("abort", () => controller.abort(), {
212
+ once: true
213
+ });
214
+ }
215
+ let response;
216
+ try {
217
+ response = await this.config.fetch(target, {
218
+ method,
219
+ headers,
220
+ body: hasBody ? JSON.stringify(options.body) : void 0,
221
+ signal: controller.signal
222
+ });
223
+ } catch (cause) {
224
+ if (options.signal?.aborted) throw cause;
225
+ const reason = cause instanceof Error ? cause.message : "request failed";
226
+ throw new NetworkError(`Could not reach the API: ${reason}.`, { cause });
227
+ } finally {
228
+ if (timer) clearTimeout(timer);
229
+ }
230
+ const text = await response.text();
231
+ let body;
232
+ try {
233
+ body = text ? JSON.parse(text) : void 0;
234
+ } catch {
235
+ body = text;
236
+ }
237
+ if (!response.ok) {
238
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
239
+ throw errorFromResponse(response.status, body, retryAfter);
240
+ }
241
+ return this.toEnvelope(body, response.status);
242
+ }
243
+ /** Confirm the body is a success envelope before handing it back. */
244
+ toEnvelope(body, status) {
245
+ if (body && typeof body === "object" && "success" in body) {
246
+ if (body.success === false) {
247
+ throw errorFromResponse(status, body);
248
+ }
249
+ return body;
250
+ }
251
+ throw new OneClickError(
252
+ "The API returned a response in an unexpected shape.",
253
+ "ERR_SERVICE",
254
+ { status, body }
255
+ );
256
+ }
257
+ };
258
+
259
+ // src/resources/base.ts
260
+ var Resource = class {
261
+ constructor(http) {
262
+ this.http = http;
263
+ }
264
+ http;
265
+ /** GET a path and return its `data` payload. */
266
+ async getData(path, query, signal) {
267
+ const env = await this.http.request("GET", path, { query, signal });
268
+ return env.data;
269
+ }
270
+ /** POST a body and return the `data` payload. */
271
+ async postData(path, body, signal) {
272
+ const env = await this.http.request("POST", path, { body, signal });
273
+ return env.data;
274
+ }
275
+ /**
276
+ * Fetch one page of a list. `extract` pulls the items and the pagination block
277
+ * out of whatever shape the endpoint nests them in; it gets the whole envelope
278
+ * because some endpoints keep pagination in `data` and others in `meta`.
279
+ * `next()` reissues the same request with the page number bumped.
280
+ */
281
+ async fetchPage(path, query, extract) {
282
+ const env = await this.http.request("GET", path, { query });
283
+ const { items, pagination } = extract(env);
284
+ const hasMore = pagination.page < pagination.totalPages;
285
+ return {
286
+ items,
287
+ ...pagination,
288
+ hasMore,
289
+ next: hasMore ? () => this.fetchPage(
290
+ path,
291
+ { ...query, page: pagination.page + 1 },
292
+ extract
293
+ ) : async () => void 0
294
+ };
295
+ }
296
+ /** Walk every item across every page, starting from a first-page promise. */
297
+ async *iterate(first) {
298
+ let page = await first;
299
+ while (page) {
300
+ for (const item of page.items) yield item;
301
+ page = await page.next();
302
+ }
303
+ }
304
+ };
305
+
306
+ // src/resources/core.ts
307
+ var Core = class extends Resource {
308
+ /** Check the API and each operator's reachability. */
309
+ ping(signal) {
310
+ return this.getData("ping", void 0, signal);
311
+ }
312
+ /** Confirm the key and read its environment, scope, and IP allowlist. */
313
+ validate(signal) {
314
+ return this.getData("validate", void 0, signal);
315
+ }
316
+ };
317
+
318
+ // src/resources/account.ts
319
+ function extractTransactions(env) {
320
+ const d = env.data ?? {};
321
+ return {
322
+ items: d.items ?? [],
323
+ pagination: d.pagination ?? { page: 1, pageSize: 0, totalPages: 1, totalResults: 0 }
324
+ };
325
+ }
326
+ var Account = class extends Resource {
327
+ /** The current balance in dinars. */
328
+ balance(signal) {
329
+ return this.getData("account/balance", void 0, signal);
330
+ }
331
+ /** One page of the transaction ledger, newest first. */
332
+ transactions(params = {}) {
333
+ return this.fetchPage(
334
+ "account/transactions",
335
+ { ...params },
336
+ extractTransactions
337
+ );
338
+ }
339
+ /** Every transaction across every page, as an async iterator. */
340
+ paginateTransactions(params = {}) {
341
+ return this.iterate(this.transactions(params));
342
+ }
343
+ };
344
+
345
+ // src/types.ts
346
+ var TOPUP_TERMINAL = [
347
+ "FULFILLED",
348
+ "REFUNDED",
349
+ "UNKNOWN_ERROR"
350
+ ];
351
+ var GIFT_TERMINAL = [
352
+ "FULFILLED",
353
+ "PARTIALLY_FILLED",
354
+ "REFUNDED"
355
+ ];
356
+ var PAYMENT_TERMINAL = [
357
+ "CONFIRMED",
358
+ "FAILED"
359
+ ];
360
+
361
+ // src/poll.ts
362
+ var PollTimeoutError = class extends OneClickError {
363
+ /** The most recent value seen before time ran out. */
364
+ last;
365
+ constructor(message, last, attempts) {
366
+ super(message, "poll_timeout", { details: { attempts } });
367
+ this.last = last;
368
+ }
369
+ };
370
+ async function pollUntil(fetchOnce, isDone, options = {}) {
371
+ const intervalMs = options.intervalMs ?? 5e3;
372
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 60);
373
+ let last;
374
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
375
+ throwIfAborted(options.signal);
376
+ last = await fetchOnce();
377
+ if (isDone(last)) return last;
378
+ if (attempt < maxAttempts) await delay(intervalMs, options.signal);
379
+ }
380
+ throw new PollTimeoutError(
381
+ `Gave up after ${maxAttempts} checks without a final status.`,
382
+ last,
383
+ maxAttempts
384
+ );
385
+ }
386
+ function throwIfAborted(signal) {
387
+ if (signal?.aborted) throw abortReason(signal);
388
+ }
389
+ function abortReason(signal) {
390
+ const reason = signal.reason;
391
+ if (reason !== void 0) return reason;
392
+ const error = new Error("The wait was aborted.");
393
+ error.name = "AbortError";
394
+ return error;
395
+ }
396
+ function delay(ms, signal) {
397
+ return new Promise((resolve, reject) => {
398
+ const onAbort = () => {
399
+ clearTimeout(timer);
400
+ reject(abortReason(signal));
401
+ };
402
+ const timer = setTimeout(() => {
403
+ signal?.removeEventListener("abort", onAbort);
404
+ resolve();
405
+ }, ms);
406
+ signal?.addEventListener("abort", onAbort, { once: true });
407
+ });
408
+ }
409
+
410
+ // src/ref.ts
411
+ function generateRef(prefix = "ocd") {
412
+ const uuid = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
413
+ return `${prefix}-${uuid}`;
414
+ }
415
+
416
+ // src/validate.ts
417
+ var MOBILE_NUMBER = /^0[567]\d{8}$/;
418
+ var ADSL_NUMBER = /^0\d{8}$/;
419
+ var FOURG_NUMBER = /^213\d{9}$/;
420
+ function isMobileNumber(value) {
421
+ return MOBILE_NUMBER.test(value);
422
+ }
423
+ function isAdslNumber(value) {
424
+ return ADSL_NUMBER.test(value);
425
+ }
426
+ function isFourgNumber(value) {
427
+ return FOURG_NUMBER.test(value);
428
+ }
429
+ function isWholeAmount(value) {
430
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
431
+ }
432
+ var Issues = class {
433
+ list = [];
434
+ /** Record a problem. The ref ties an issue to one order when checking a batch. */
435
+ add(field, message, ref) {
436
+ this.list.push(ref ? { field, message, ref } : { field, message });
437
+ return this;
438
+ }
439
+ /** Record a problem only when the condition holds. */
440
+ addIf(condition, field, message, ref) {
441
+ if (condition) this.add(field, message, ref);
442
+ return this;
443
+ }
444
+ get any() {
445
+ return this.list.length > 0;
446
+ }
447
+ all() {
448
+ return this.list;
449
+ }
450
+ /** Throw if anything was collected; otherwise do nothing. */
451
+ throwIfAny(summary = "The request did not pass validation.") {
452
+ if (this.list.length > 0) {
453
+ throw new ValidationError(summary, { issues: this.list });
454
+ }
455
+ }
456
+ };
457
+ function requireText(issues, value, field, ref) {
458
+ if (typeof value !== "string" || value.trim() === "") {
459
+ issues.add(field, "is required", ref);
460
+ }
461
+ }
462
+
463
+ // src/resources/mobile.ts
464
+ function extractTopups(env) {
465
+ const d = env.data ?? {};
466
+ return {
467
+ items: d.items ?? [],
468
+ pagination: d.pagination ?? { page: 1, pageSize: 0, totalPages: 1, totalResults: 0 }
469
+ };
470
+ }
471
+ function validateSend(input) {
472
+ const issues = new Issues();
473
+ requireText(issues, input.plan_code, "plan_code");
474
+ if (typeof input.MSSIDN !== "string" || !isMobileNumber(input.MSSIDN)) {
475
+ issues.add("MSSIDN", "is not a valid mobile number");
476
+ }
477
+ if (input.amount !== void 0 && !isWholeAmount(input.amount)) {
478
+ issues.add("amount", "must be a whole number of dinars");
479
+ }
480
+ issues.throwIfAny("The top-up did not pass validation.");
481
+ }
482
+ var isTerminal = (t) => TOPUP_TERMINAL.includes(t.status);
483
+ var Mobile = class extends Resource {
484
+ /** The plans available to your account, split into dynamic and fixed. */
485
+ plans(signal) {
486
+ return this.getData("mobile/plans", void 0, signal);
487
+ }
488
+ /**
489
+ * Send a top-up. The number and amount are checked first, and a reference is
490
+ * generated when you do not pass one, so the returned `topupRef` is always set.
491
+ */
492
+ async send(input, signal) {
493
+ validateSend(input);
494
+ const body = { ...input, ref: input.ref ?? generateRef() };
495
+ return this.postData("mobile/send", body, signal);
496
+ }
497
+ /** Look a top-up up by your own reference. */
498
+ checkByRef(ref, signal) {
499
+ return this.getData(
500
+ `mobile/check-ref/${encodeURIComponent(ref)}`,
501
+ void 0,
502
+ signal
503
+ );
504
+ }
505
+ /** Look a top-up up by the id from `send`. */
506
+ checkById(id, signal) {
507
+ return this.getData(
508
+ `mobile/check-id/${encodeURIComponent(id)}`,
509
+ void 0,
510
+ signal
511
+ );
512
+ }
513
+ /**
514
+ * Send a top-up and wait for it to settle, polling by reference. Resolves with
515
+ * the final top-up once it is FULFILLED, REFUNDED, or UNKNOWN_ERROR. Defaults
516
+ * to a check every 5 seconds for up to 5 minutes, which the docs recommend.
517
+ */
518
+ async sendAndWait(input, options = {}) {
519
+ const ref = input.ref ?? generateRef();
520
+ await this.send({ ...input, ref }, options.signal);
521
+ return pollUntil(() => this.checkByRef(ref, options.signal), isTerminal, {
522
+ intervalMs: 5e3,
523
+ maxAttempts: 60,
524
+ ...options
525
+ });
526
+ }
527
+ /** One page of your past top-ups, newest first. */
528
+ list(params = {}) {
529
+ return this.fetchPage("mobile/list", { ...params }, extractTopups);
530
+ }
531
+ /** Every past top-up across every page, as an async iterator. */
532
+ paginate(params = {}) {
533
+ return this.iterate(this.list(params));
534
+ }
535
+ };
536
+
537
+ // src/secret.ts
538
+ var REDACTED = "[redacted]";
539
+ var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
540
+ var Secret = class _Secret {
541
+ // A private field is neither enumerable nor reachable by Object.keys or a
542
+ // spread, so the value cannot leak through ordinary object handling.
543
+ #value;
544
+ constructor(value) {
545
+ this.#value = value;
546
+ }
547
+ /** The real value. Calling this is the deliberate, searchable moment a secret is read. */
548
+ reveal() {
549
+ return this.#value;
550
+ }
551
+ /** Used by JSON.stringify, so a secret nested in an object serializes redacted. */
552
+ toJSON() {
553
+ return REDACTED;
554
+ }
555
+ toString() {
556
+ return REDACTED;
557
+ }
558
+ [INSPECT]() {
559
+ return `Secret(${REDACTED})`;
560
+ }
561
+ static of(value) {
562
+ return new _Secret(value);
563
+ }
564
+ };
565
+ function isSecret(value) {
566
+ return value instanceof Secret;
567
+ }
568
+ function reveal(value) {
569
+ return value instanceof Secret ? value.reveal() : value;
570
+ }
571
+
572
+ // src/resources/internet.ts
573
+ function toTopup(raw) {
574
+ const { card_code, ...rest } = raw;
575
+ return card_code ? { ...rest, card_code: Secret.of(card_code) } : { ...rest };
576
+ }
577
+ function extractInternetTopups(env) {
578
+ const data = env.data ?? {};
579
+ const page = env.meta?.pagination ?? {};
580
+ const items = (data.topups ?? []).map(toTopup);
581
+ return {
582
+ items,
583
+ pagination: {
584
+ page: page.currentPage ?? page.page ?? 1,
585
+ pageSize: items.length,
586
+ totalPages: page.totalPages ?? 1,
587
+ totalResults: page.totalItems ?? page.totalResults ?? items.length
588
+ }
589
+ };
590
+ }
591
+ function validateSend2(input) {
592
+ const issues = new Issues();
593
+ if (input.type !== "ADSL" && input.type !== "4G") {
594
+ issues.add("type", "must be ADSL or 4G");
595
+ } else if (input.type === "ADSL" && !isAdslNumber(input.number)) {
596
+ issues.add("number", "is not a valid ADSL number");
597
+ } else if (input.type === "4G" && !isFourgNumber(input.number)) {
598
+ issues.add("number", "is not a valid 4G number");
599
+ }
600
+ if (!isWholeAmount(input.value)) {
601
+ issues.add("value", "must be a whole number of dinars");
602
+ }
603
+ issues.throwIfAny("The internet top-up did not pass validation.");
604
+ }
605
+ var isTerminal2 = (t) => TOPUP_TERMINAL.includes(t.status);
606
+ var Internet = class extends Resource {
607
+ /** The cards on offer for a service type, with their cost and stock. */
608
+ products(type, signal) {
609
+ return this.getData("internet/products", { type }, signal);
610
+ }
611
+ /** Confirm a number is valid and active before sending to it. */
612
+ checkNumber(type, number, signal) {
613
+ return this.getData(
614
+ "internet/check-number",
615
+ { type, number },
616
+ signal
617
+ );
618
+ }
619
+ /** Send a recharge. The number and value are checked first; a ref is filled in. */
620
+ async send(input, signal) {
621
+ validateSend2(input);
622
+ const body = { ...input, ref: input.ref ?? generateRef() };
623
+ return this.postData("internet/send", body, signal);
624
+ }
625
+ async checkByRef(ref, signal) {
626
+ const raw = await this.getData(
627
+ `internet/check-ref/${encodeURIComponent(ref)}`,
628
+ void 0,
629
+ signal
630
+ );
631
+ return toTopup(raw);
632
+ }
633
+ async checkById(id, signal) {
634
+ const raw = await this.getData(
635
+ `internet/check-id/${encodeURIComponent(id)}`,
636
+ void 0,
637
+ signal
638
+ );
639
+ return toTopup(raw);
640
+ }
641
+ /**
642
+ * Send and wait for the recharge to settle. Resolves with the final order,
643
+ * its `card_code` ready to reveal once FULFILLED. A QUEUED order can take up to
644
+ * 48 hours, well past the default polling window, so expect a timeout there.
645
+ */
646
+ async sendAndWait(input, options = {}) {
647
+ const ref = input.ref ?? generateRef();
648
+ await this.send({ ...input, ref }, options.signal);
649
+ return pollUntil(() => this.checkByRef(ref, options.signal), isTerminal2, {
650
+ intervalMs: 5e3,
651
+ maxAttempts: 60,
652
+ ...options
653
+ });
654
+ }
655
+ list(params = {}) {
656
+ return this.fetchPage(
657
+ "internet/list",
658
+ { ...params },
659
+ extractInternetTopups
660
+ );
661
+ }
662
+ paginate(params = {}) {
663
+ return this.iterate(this.list(params));
664
+ }
665
+ };
666
+
667
+ // src/resources/giftcards.ts
668
+ function toOrder(raw) {
669
+ const cards = (raw.cards ?? []).map((card) => ({
670
+ value: Secret.of(card.value ?? ""),
671
+ serial: Secret.of(card.serial ?? "")
672
+ }));
673
+ return { ...raw, cards };
674
+ }
675
+ function extractOrders(env) {
676
+ const d = env.data ?? {};
677
+ return {
678
+ items: (d.items ?? []).map(toOrder),
679
+ pagination: d.pagination ?? { page: 1, pageSize: 0, totalPages: 1, totalResults: 0 }
680
+ };
681
+ }
682
+ function validateOrder(input) {
683
+ const issues = new Issues();
684
+ requireText(issues, input.productId, "productId");
685
+ requireText(issues, input.typeId, "typeId");
686
+ if (!Number.isInteger(input.quantity) || input.quantity < 1) {
687
+ issues.add("quantity", "must be a whole number of one or more");
688
+ }
689
+ issues.throwIfAny("The gift-card order did not pass validation.");
690
+ }
691
+ var isTerminal3 = (o) => GIFT_TERMINAL.includes(o.status);
692
+ var GiftCards = class extends Resource {
693
+ /** The full product catalog. Cache it; the docs say it changes rarely. */
694
+ async catalog(signal) {
695
+ const env = await this.http.request(
696
+ "GET",
697
+ "gift-cards/catalog",
698
+ { signal }
699
+ );
700
+ return {
701
+ categories: env.data.categories ?? [],
702
+ totalCategories: env.totalCategories,
703
+ totalProducts: env.totalProducts
704
+ };
705
+ }
706
+ /** A product's denominations, with live pricing and stock. */
707
+ checkProduct(productId, signal) {
708
+ return this.getData(
709
+ `gift-cards/checkProduct/${encodeURIComponent(productId)}`,
710
+ void 0,
711
+ signal
712
+ );
713
+ }
714
+ /** Place an order. The product, type, and quantity are checked first. */
715
+ async placeOrder(input, signal) {
716
+ validateOrder(input);
717
+ return this.postData(
718
+ "gift-cards/placeOrder",
719
+ input,
720
+ signal
721
+ );
722
+ }
723
+ /** Read an order's status and, once settled, its cards. */
724
+ async checkOrder(orderId, signal) {
725
+ const raw = await this.getData(
726
+ `gift-cards/checkOrder/${encodeURIComponent(orderId)}`,
727
+ void 0,
728
+ signal
729
+ );
730
+ return toOrder(raw);
731
+ }
732
+ /**
733
+ * Place an order and wait for it to settle. Resolves once the order is
734
+ * FULFILLED, PARTIALLY_FILLED, or REFUNDED. Gift cards can take longer than a
735
+ * top-up, so this checks every 5 seconds for up to 10 minutes by default.
736
+ */
737
+ async placeOrderAndWait(input, options = {}) {
738
+ const { orderId } = await this.placeOrder(input, options.signal);
739
+ return pollUntil(
740
+ () => this.checkOrder(orderId, options.signal),
741
+ isTerminal3,
742
+ { intervalMs: 5e3, maxAttempts: 120, ...options }
743
+ );
744
+ }
745
+ /** One page of past orders, newest first. */
746
+ listOrders(params = {}) {
747
+ return this.fetchPage(
748
+ "gift-cards/list",
749
+ { ...params },
750
+ extractOrders
751
+ );
752
+ }
753
+ /** Every past order across every page, as an async iterator. */
754
+ paginateOrders(params = {}) {
755
+ return this.iterate(this.listOrders(params));
756
+ }
757
+ };
758
+
759
+ // src/resources/payments.ts
760
+ var MIN_PAYMENT_AMOUNT = 500;
761
+ var MAX_PAYMENT_AMOUNT = 5e5;
762
+ function validateCreateLink(input) {
763
+ const issues = new Issues();
764
+ const info = input.productInfo ?? {};
765
+ requireText(issues, info.title, "productInfo.title");
766
+ if (typeof info.title === "string" && info.title.length > 200) {
767
+ issues.add("productInfo.title", "must be at most 200 characters");
768
+ }
769
+ if (!isWholeAmount(info.amount)) {
770
+ issues.add("productInfo.amount", "must be a whole number of dinars");
771
+ } else if (info.amount < MIN_PAYMENT_AMOUNT || info.amount > MAX_PAYMENT_AMOUNT) {
772
+ issues.add(
773
+ "productInfo.amount",
774
+ `must be between ${MIN_PAYMENT_AMOUNT} and ${MAX_PAYMENT_AMOUNT}`
775
+ );
776
+ }
777
+ if (info.description !== void 0 && info.description.length > 1e3) {
778
+ issues.add("productInfo.description", "must be at most 1000 characters");
779
+ }
780
+ if (input.successMessage !== void 0 && input.successMessage.length > 500) {
781
+ issues.add("successMessage", "must be at most 500 characters");
782
+ }
783
+ if (input.feeMode !== void 0 && input.feeMode !== "NO_FEE" && input.feeMode !== "SPLIT_FEE" && input.feeMode !== "CUSTOMER_FEE") {
784
+ issues.add("feeMode", "must be NO_FEE, SPLIT_FEE, or CUSTOMER_FEE");
785
+ }
786
+ if (input.redirectUrl !== void 0 && !/^https?:\/\//.test(input.redirectUrl)) {
787
+ issues.add("redirectUrl", "must start with http:// or https://");
788
+ }
789
+ issues.throwIfAny("The payment link did not pass validation.");
790
+ }
791
+ var isTerminal4 = (p) => PAYMENT_TERMINAL.includes(p.status);
792
+ var Payments = class extends Resource {
793
+ /** Create a payment link. Returns the URL to send the customer to. */
794
+ async createLink(input, signal) {
795
+ validateCreateLink(input);
796
+ return this.postData("ocpay/createLink", input, signal);
797
+ }
798
+ /** Check a payment's status by its reference. */
799
+ checkPayment(ref, signal) {
800
+ return this.getData(
801
+ `ocpay/checkPayment/${encodeURIComponent(ref)}`,
802
+ void 0,
803
+ signal
804
+ );
805
+ }
806
+ /**
807
+ * Poll a payment until it is CONFIRMED or FAILED. The default window is twenty
808
+ * minutes, matching how long an unpaid link stays open; a payment that is
809
+ * never started times out with a PollTimeoutError. Pass a signal to stop early.
810
+ */
811
+ waitForPayment(ref, options = {}) {
812
+ return pollUntil(
813
+ () => this.checkPayment(ref, options.signal),
814
+ isTerminal4,
815
+ { intervalMs: 5e3, maxAttempts: 240, ...options }
816
+ );
817
+ }
818
+ };
819
+
820
+ // src/client.ts
821
+ var DEFAULT_BASE_URL = "https://api.oneclickdz.com/v3";
822
+ var DEFAULT_TIMEOUT_MS = 3e4;
823
+ var DEFAULT_RETRY = { attempts: 3, respectRetryAfter: true };
824
+ var OneClickDZ = class {
825
+ core;
826
+ account;
827
+ mobile;
828
+ internet;
829
+ giftCards;
830
+ payments;
831
+ http;
832
+ constructor(options) {
833
+ if (!options.key) {
834
+ throw new Error("OneClickDZ needs an access token in options.key.");
835
+ }
836
+ const fetchImpl = options.fetch ?? globalThis.fetch;
837
+ if (typeof fetchImpl !== "function") {
838
+ throw new Error(
839
+ "No fetch is available. Run on Node 18+ or pass one in options.fetch."
840
+ );
841
+ }
842
+ this.http = new HttpClient({
843
+ key: options.key,
844
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
845
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
846
+ retry: { ...DEFAULT_RETRY, ...options.retry },
847
+ fetch: fetchImpl
848
+ });
849
+ this.core = new Core(this.http);
850
+ this.account = new Account(this.http);
851
+ this.mobile = new Mobile(this.http);
852
+ this.internet = new Internet(this.http);
853
+ this.giftCards = new GiftCards(this.http);
854
+ this.payments = new Payments(this.http);
855
+ }
856
+ /** Shortcut for `core.ping()`: the API and operator status. */
857
+ ping(signal) {
858
+ return this.core.ping(signal);
859
+ }
860
+ /** Shortcut for `core.validate()`: confirm the key and read its environment. */
861
+ validate(signal) {
862
+ return this.core.validate(signal);
863
+ }
864
+ };
865
+
866
+ // src/pricing.ts
867
+ function sellPrice(cost, markup) {
868
+ const raw = "percent" in markup ? cost * (1 + markup.percent / 100) : cost + markup.flat;
869
+ return Math.round(raw);
870
+ }
871
+ function priceInternetProducts(products, markup) {
872
+ return products.map((p) => ({
873
+ value: p.value,
874
+ price: sellPrice(p.cost, markup),
875
+ available: p.available
876
+ }));
877
+ }
878
+ function priceGiftTypes(types, markup) {
879
+ return types.map((t) => ({
880
+ id: t.id,
881
+ name: t.name,
882
+ price: sellPrice(t.price, markup),
883
+ inStock: t.quantity > 0
884
+ }));
885
+ }
886
+
887
+ // src/index.ts
888
+ var VERSION = "0.1.0";
889
+
890
+ exports.ADSL_NUMBER = ADSL_NUMBER;
891
+ exports.Account = Account;
892
+ exports.AuthError = AuthError;
893
+ exports.Core = Core;
894
+ exports.DuplicateRefError = DuplicateRefError;
895
+ exports.FOURG_NUMBER = FOURG_NUMBER;
896
+ exports.ForbiddenError = ForbiddenError;
897
+ exports.GIFT_TERMINAL = GIFT_TERMINAL;
898
+ exports.GiftCards = GiftCards;
899
+ exports.InsufficientBalanceError = InsufficientBalanceError;
900
+ exports.Internet = Internet;
901
+ exports.Issues = Issues;
902
+ exports.MAX_PAYMENT_AMOUNT = MAX_PAYMENT_AMOUNT;
903
+ exports.MIN_PAYMENT_AMOUNT = MIN_PAYMENT_AMOUNT;
904
+ exports.MOBILE_NUMBER = MOBILE_NUMBER;
905
+ exports.Mobile = Mobile;
906
+ exports.NetworkError = NetworkError;
907
+ exports.NotFoundError = NotFoundError;
908
+ exports.OneClickDZ = OneClickDZ;
909
+ exports.OneClickError = OneClickError;
910
+ exports.PAYMENT_TERMINAL = PAYMENT_TERMINAL;
911
+ exports.Payments = Payments;
912
+ exports.PollTimeoutError = PollTimeoutError;
913
+ exports.RateLimitError = RateLimitError;
914
+ exports.RequestError = RequestError;
915
+ exports.Secret = Secret;
916
+ exports.ServiceError = ServiceError;
917
+ exports.TOPUP_TERMINAL = TOPUP_TERMINAL;
918
+ exports.VERSION = VERSION;
919
+ exports.ValidationError = ValidationError;
920
+ exports.errorFromResponse = errorFromResponse;
921
+ exports.generateRef = generateRef;
922
+ exports.isAdslNumber = isAdslNumber;
923
+ exports.isFourgNumber = isFourgNumber;
924
+ exports.isMobileNumber = isMobileNumber;
925
+ exports.isSecret = isSecret;
926
+ exports.isWholeAmount = isWholeAmount;
927
+ exports.pollUntil = pollUntil;
928
+ exports.priceGiftTypes = priceGiftTypes;
929
+ exports.priceInternetProducts = priceInternetProducts;
930
+ exports.requireText = requireText;
931
+ exports.reveal = reveal;
932
+ exports.sellPrice = sellPrice;
933
+ //# sourceMappingURL=index.cjs.map
934
+ //# sourceMappingURL=index.cjs.map