@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 +21 -0
- package/README.md +298 -0
- package/dist/index.cjs +934 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +730 -0
- package/dist/index.d.ts +730 -0
- package/dist/index.js +890 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
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).
|