@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 syscli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # oneclickdz
2
+
3
+ A typed client for the [OneClickDZ](https://oneclickdz.com) API. It covers the
4
+ recharges every Algerian integration ends up writing by hand: mobile top-ups for
5
+ Mobilis, Djezzy, and Ooredoo, ADSL and 4G internet cards, gift cards, and Navio
6
+ payment links.
7
+
8
+ Two things set it apart. The recharge endpoints are asynchronous, so you send an
9
+ order and then poll until it settles; this client runs that loop for you and
10
+ hands back a fully typed, settled result. And the card codes a recharge delivers
11
+ come back wrapped so they stay out of your logs until you ask for them.
12
+
13
+ It is meant for servers. Your access token grants account and balance access, so
14
+ it must never reach the browser.
15
+
16
+ Published as `@syscli/oneclickdz`. It needs Node 18 or newer, uses the global
17
+ `fetch`, and ships no runtime dependencies.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @syscli/oneclickdz
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```ts
28
+ import { OneClickDZ } from "@syscli/oneclickdz";
29
+
30
+ const oneclick = new OneClickDZ({
31
+ key: process.env.ONECLICKDZ_API_KEY!,
32
+ });
33
+
34
+ // Confirm the key and see which environment it belongs to.
35
+ const { username, apiKey } = await oneclick.validate();
36
+ console.log(username, apiKey.type); // "boutique-batna", "SANDBOX"
37
+
38
+ // Send a Djezzy top-up and wait for it to settle.
39
+ const topup = await oneclick.mobile.sendAndWait({
40
+ plan_code: "PREPAID_DJEZZY",
41
+ MSSIDN: "0778037340",
42
+ amount: 500,
43
+ });
44
+
45
+ console.log(topup.status); // "FULFILLED"
46
+ ```
47
+
48
+ ## Keys and environments
49
+
50
+ Every key is either sandbox or production. Sandbox runs the full flow without
51
+ touching real balance, so build against it first. Read the environment off the
52
+ key with `validate()` before a deploy goes live:
53
+
54
+ ```ts
55
+ const { apiKey } = await oneclick.validate();
56
+ if (apiKey.type !== "PRODUCTION") {
57
+ throw new Error("Refusing to start with a sandbox key in production.");
58
+ }
59
+ ```
60
+
61
+ The token goes over the wire as the `X-Access-Token` header. After five failed
62
+ auth attempts the API blocks your IP for fifteen minutes, so cache a working key
63
+ rather than guessing.
64
+
65
+ ## Sending and waiting
66
+
67
+ A send returns an id and a reference; the order then moves from `PENDING` through
68
+ `HANDLING` to a final state. You can drive that yourself:
69
+
70
+ ```ts
71
+ const { topupRef } = await oneclick.mobile.send({
72
+ plan_code: "PREPAID_MOBILIS",
73
+ MSSIDN: "0661234567",
74
+ amount: 1000,
75
+ });
76
+
77
+ const status = await oneclick.mobile.checkByRef(topupRef);
78
+ ```
79
+
80
+ Or let the client poll for you. `sendAndWait` checks every five seconds and
81
+ resolves once the order is `FULFILLED`, `REFUNDED`, or `UNKNOWN_ERROR`:
82
+
83
+ ```ts
84
+ const topup = await oneclick.mobile.sendAndWait({
85
+ plan_code: "PREPAID_OOREDOO",
86
+ MSSIDN: "0551234567",
87
+ amount: 200,
88
+ });
89
+
90
+ if (topup.status === "REFUNDED") {
91
+ console.log(topup.refund_message, topup.suggested_offers);
92
+ }
93
+ ```
94
+
95
+ Pass an `AbortSignal` to stop waiting, and override the cadence when you need to:
96
+
97
+ ```ts
98
+ await oneclick.giftCards.placeOrderAndWait(order, {
99
+ intervalMs: 5000,
100
+ maxAttempts: 120,
101
+ signal: controller.signal,
102
+ });
103
+ ```
104
+
105
+ A wait that never settles inside its budget throws a `PollTimeoutError` that
106
+ carries the last status it saw on `error.last`.
107
+
108
+ ## References and safe resends
109
+
110
+ Every send takes an optional `ref`. Leave it out and the client generates one, so
111
+ a resend after a dropped connection reuses the same reference and the API rejects
112
+ the duplicate instead of charging twice. A duplicate is a `DuplicateRefError`, its
113
+ own class, so you can tell it apart from a real failure:
114
+
115
+ ```ts
116
+ import { DuplicateRefError } from "@syscli/oneclickdz";
117
+
118
+ try {
119
+ await oneclick.mobile.send({ plan_code, MSSIDN, amount, ref: "order-1043" });
120
+ } catch (error) {
121
+ if (error instanceof DuplicateRefError) {
122
+ const existing = await oneclick.mobile.checkByRef("order-1043");
123
+ // The first order stands. Read it rather than sending again.
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## Card codes are secrets
129
+
130
+ Internet recharges and gift cards return codes the customer keys in. Those are
131
+ bearer credentials, so the client wraps them. A wrapped value renders as
132
+ `[redacted]` in logs, JSON, and string interpolation, and gives up its contents
133
+ only through `reveal()`:
134
+
135
+ ```ts
136
+ const order = await oneclick.giftCards.checkOrder(orderId);
137
+
138
+ for (const card of order.cards) {
139
+ console.log(card.value); // [redacted]
140
+ sendToCustomer(card.value.reveal()); // the real code, read on purpose
141
+ }
142
+
143
+ JSON.stringify(order); // the codes do not appear
144
+ ```
145
+
146
+ The same applies to `card_code` on a settled internet top-up.
147
+
148
+ ## Gift cards
149
+
150
+ Load the catalog once and cache it; it changes rarely. Check a product for live
151
+ pricing and stock, place the order, then wait for the codes:
152
+
153
+ ```ts
154
+ const catalog = await oneclick.giftCards.catalog();
155
+ const details = await oneclick.giftCards.checkProduct("psn-us");
156
+
157
+ const order = await oneclick.giftCards.placeOrderAndWait({
158
+ productId: "psn-us",
159
+ typeId: details.types[0].id,
160
+ quantity: 2,
161
+ });
162
+
163
+ if (order.status === "PARTIALLY_FILLED") {
164
+ console.log(`Only ${order.fulfilled_quantity} of ${order.quantity} came through.`);
165
+ }
166
+ ```
167
+
168
+ ## Payments with Navio
169
+
170
+ Create a hosted payment link, send the customer to its URL, then poll the
171
+ reference. A link stays open for twenty minutes:
172
+
173
+ ```ts
174
+ const link = await oneclick.payments.createLink({
175
+ productInfo: { title: "Commande 1043", amount: 4500 },
176
+ feeMode: "CUSTOMER_FEE",
177
+ redirectUrl: "https://shop.example.dz/merci",
178
+ });
179
+
180
+ console.log(link.paymentUrl);
181
+
182
+ const payment = await oneclick.payments.waitForPayment(link.paymentRef);
183
+ console.log(payment.status); // "CONFIRMED" or "FAILED"
184
+ ```
185
+
186
+ ## Resale pricing
187
+
188
+ The API returns your wholesale `cost` on internet products and gift-card types,
189
+ and that figure must never reach a customer. Pick a markup and get back a clean,
190
+ customer-facing list with the cost gone:
191
+
192
+ ```ts
193
+ import { priceInternetProducts } from "@syscli/oneclickdz";
194
+
195
+ const products = await oneclick.internet.products("4G");
196
+ const forSale = priceInternetProducts(products.products, { percent: 10 });
197
+ // [{ value: 1000, price: 1078, available: true }, ...] and no cost field
198
+
199
+ import { sellPrice } from "@syscli/oneclickdz";
200
+ sellPrice(480, { flat: 100 }); // 580
201
+ ```
202
+
203
+ `priceGiftTypes` does the same for a gift product's denominations.
204
+
205
+ ## Validation
206
+
207
+ Inputs are checked before the request leaves. A wrong number for the service, an
208
+ amount that is not a whole dinar figure, a payment under the floor, or a quantity
209
+ below one throws a `ValidationError` with one issue per problem, and spends no
210
+ request:
211
+
212
+ ```ts
213
+ import { ValidationError } from "@syscli/oneclickdz";
214
+
215
+ try {
216
+ await oneclick.internet.send({ type: "4G", number: "033123456", value: 1500 });
217
+ } catch (error) {
218
+ if (error instanceof ValidationError) {
219
+ for (const issue of error.issues) console.warn(issue.field, issue.message);
220
+ }
221
+ }
222
+ ```
223
+
224
+ ## Errors
225
+
226
+ Every failure is a `OneClickError`, so you can catch one type and branch on it or
227
+ on its stable `code`:
228
+
229
+ ```ts
230
+ import {
231
+ AuthError,
232
+ InsufficientBalanceError,
233
+ DuplicateRefError,
234
+ RateLimitError,
235
+ ServiceError,
236
+ } from "@syscli/oneclickdz";
237
+ ```
238
+
239
+ `AuthError` (401), `InsufficientBalanceError` and `DuplicateRefError` (403),
240
+ `ValidationError` (400 or client-side), `NotFoundError` (404), `RateLimitError`
241
+ (429, with `retryAfter`), and `ServiceError` (5xx) all carry `status`, the API's
242
+ `code`, the `requestId`, and the parsed `body`.
243
+
244
+ ## Rate limits and retries
245
+
246
+ The API allows 60 requests a minute on sandbox and 120 on production. On a 429
247
+ the client waits the `Retry-After` and tries again, up to three attempts. A 5xx
248
+ or a dropped connection is retried too, but only for reads: a top-up that timed
249
+ out may still have gone through, so the client never blindly resends a charge.
250
+ That is also why a server error on a send should not be refunded right away; the
251
+ docs note it can still settle within a day.
252
+
253
+ ## Pagination
254
+
255
+ List endpoints return a `Page`. Step through it with `next()`, or let an iterator
256
+ walk every page:
257
+
258
+ ```ts
259
+ const page = await oneclick.account.transactions({ pageSize: 50 });
260
+ console.log(page.items, page.totalResults, page.hasMore);
261
+
262
+ for await (const tx of oneclick.account.paginateTransactions()) {
263
+ console.log(tx.type, tx.amount, tx.newBalance);
264
+ }
265
+ ```
266
+
267
+ ## Limitations
268
+
269
+ Worth knowing before you build on it:
270
+
271
+ - **Server only.** The token grants account and balance access. There is no
272
+ browser build, on purpose.
273
+ - **No webhooks yet.** The API does not offer them at the time of writing, so the
274
+ only way to learn an outcome is to poll. The `*AndWait` helpers and the
275
+ pollers are built around that. Webhook support can be added once it ships.
276
+ - **Navio needs merchant onboarding.** `createLink` returns a 403 until the
277
+ account completes merchant validation in the dashboard. The client surfaces
278
+ that as a `ForbiddenError`; it is an account step, not a code change.
279
+ - **A QUEUED internet order is slow.** It can take up to 48 hours, well past the
280
+ default polling window. Treat `QUEUED` as accepted and check back later rather
281
+ than waiting in process.
282
+ - **Wholesale prices are not for display.** Product `cost` and `price` are your
283
+ rates. Apply your own markup and do not show them to customers.
284
+
285
+ ## Development
286
+
287
+ ```sh
288
+ npm install
289
+ npm test # vitest
290
+ npm run build # tsup, dual ESM and CJS plus types
291
+ ```
292
+
293
+ A small set of live tests run against the real API when `ONECLICKDZ_API_KEY` is
294
+ set, and are skipped otherwise. Point them at a sandbox key.
295
+
296
+ ## License
297
+
298
+ MIT. See [LICENSE](./LICENSE).