@sonatel-os/juf
Version:
The community SDK for Orange Money, SMS, Email & Sonatel APIs on the Orange Developer Platform.
350 lines (262 loc) • 11.3 kB
Markdown
<p align="center">
<img src="https://img.shields.io/badge/JUF.js-The_Community_SDK-ff6600?style=for-the-badge&labelColor=1a1a2e" alt="JUF.js" />
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@sonatel-os/juf"><img src="https://img.shields.io/npm/v/@sonatel-os/juf?style=flat-square&color=ff6600" alt="npm" /></a>
<img src="https://img.shields.io/badge/node-%3E%3D16-43853d?style=flat-square&logo=node.js&logoColor=white" alt="Node" />
<img src="https://img.shields.io/badge/ESM-native-blueviolet?style=flat-square" alt="ESM" />
<img src="https://img.shields.io/badge/CJS-supported-blue?style=flat-square" alt="CJS" />
<img src="https://img.shields.io/badge/TypeScript-declarations-3178c6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" />
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License" />
<img src="https://img.shields.io/badge/tests-174_passing-brightgreen?style=flat-square" alt="Tests" />
</p>
<p align="center">
<b>The community SDK for Orange Money, SMS, Email & Sonatel APIs.</b><br/>
<sub>Built on the <a href="https://developer.orange-sonatel.com">Orange Developer Platform</a> — open to all developers.</sub>
</p>
## Why JUF?
The [Orange Developer Platform](https://developer.orange-sonatel.com) exposes powerful APIs for payments, messaging, and more — but provides **no official SDK**. JUF fills that gap.
| What you get | Without JUF | With JUF |
|---|---|---|
| **OAuth2** | Manual token fetch, caching, refresh | Automatic — one call, cached 240s |
| **Payments** | Raw HTTP, manual payload construction | `payment.preparePaymentCheckout()` |
| **QR Codes** | Build payloads, format amounts, manage headers | `payment.createPaymentQRCode()` |
| **SMS / Email** | Auth + HTTP + error parsing | `communication.sendSMS()` |
| **Error handling** | Parse each endpoint's error shape | Consistent `JufError` hierarchy |
| **Validation** | Hope for the best | Superstruct schemas catch bad input before it hits the API |
## Getting Started
### 1. Get your API credentials
Sign up at **[developer.orange-sonatel.com](https://developer.orange-sonatel.com)** (free), create an application, and grab your `client_id` and `client_secret`. Sandbox access is immediate — no approval needed.
### 2. Install
```bash
yarn add @sonatel-os/juf
# or
npm install @sonatel-os/juf
```
### 3. Configure
```bash
cp .env.example .env
```
```bash
JUF_APIGEE_CLIENT_ID="<your-client-id>"
JUF_APIGEE_CLIENT_SECRET="<your-client-secret>"
```
> See [.env.example](.env.example) for all options (production, preprod, APM, logging).
### 4. Use
```javascript
import { authentication, communication, payment } from '@sonatel-os/juf';
// Authenticate (tokens are cached automatically)
const { access_token } = await authentication.debug();
// Accept a payment via QR code
const { qrCode, deepLinks } = await payment.createPaymentQRCode({
merchant: { code: 123456, sitename: 'CoolShop' },
bill: { amount: 2500, reference: 'ORDER-42' },
});
// Send a confirmation SMS
await communication.sendSMS({
body: 'Payment received! Thank you.',
to: '+221770000000',
senderName: 'CoolShop',
});
```
## Subpath Imports (recommended)
Import only what you need for smaller bundles and clearer dependency graphs:
```javascript
// Instead of importing everything:
import { authentication, payment } from '@sonatel-os/juf';
// Import only the domain you need:
import { Authentication } from '@sonatel-os/juf/auth';
import { Payment } from '@sonatel-os/juf/payment';
import { Communication } from '@sonatel-os/juf/communication';
import { ValidationError, AuthenticationError } from '@sonatel-os/juf/core';
// With DI, you control initialization:
const auth = new Authentication({ config, cache, client, logger });
const pay = new Payment({ authService: auth, client, config, logger });
```
| Subpath | Exports |
|---|---|
| `@sonatel-os/juf/auth` | `Authentication` class |
| `@sonatel-os/juf/communication` | `Communication` class, `EmailStructure`, `SmsStructure` |
| `@sonatel-os/juf/payment` | `Payment`, `QRCodeDecoder` classes, `CheckoutPaymentStructure`, `QRCodePaymentStructure`, `QRCodeDecodePaymentStructure` |
| `@sonatel-os/juf/core` | Errors, validation, logger, cache, constants, requester |
> The root import (`@sonatel-os/juf`) still works and will continue to work until v2.0.0.
## API Reference
### Authentication
```javascript
import { Authentication } from '@sonatel-os/juf/auth';
const auth = Authentication.init();
```
#### `auth.debug()`
Fetches a fresh OAuth2 token or returns the cached one (TTL: 240s).
```javascript
const { access_token, token_type, expires_in } = await auth.debug();
```
### Communication
```javascript
import { Communication } from '@sonatel-os/juf/communication';
const comm = Communication.init();
```
#### `comm.sendEmail({ subject, to, from, body, html? })`
```javascript
const { id, status } = await comm.sendEmail({
subject: 'Welcome!',
to: 'user@example.com',
from: 'hello@myapp.com',
body: '<h1>Welcome aboard!</h1>',
html: true,
});
```
| Param | Type | Required | Description |
|---|---|:---:|---|
| `subject` | string | Yes | Email subject line |
| `to` | string | Yes | Recipient address |
| `from` | string | Yes | Sender address |
| `body` | string | Yes | Email content |
| `html` | boolean | — | `true` if body is HTML |
#### `comm.sendSMS({ body, to, senderName, confidential?, scheduledFor? })`
```javascript
const { id, status } = await comm.sendSMS({
body: 'Your OTP is 4829',
to: '+221770000000',
senderName: 'MyApp',
});
```
| Param | Type | Required | Default | Description |
|---|---|:---:|:---:|---|
| `body` | string | Yes | — | Message content |
| `to` | string | Yes | — | Phone number |
| `senderName` | string | Yes | — | Sender display name |
| `confidential` | boolean | — | `true` | Mark as confidential |
| `scheduledFor` | string | — | — | ISO 8601 datetime |
### Payment
```javascript
import { Payment } from '@sonatel-os/juf/payment';
const pay = Payment.init();
```
#### `pay.preparePaymentCheckout({ merchant, bill, urls })`
Creates a payment session and returns a checkout link.
```javascript
const { link, secret } = await pay.preparePaymentCheckout({
merchant: { code: 123456, sitename: 'your-sitename' },
bill: { amount: 1000, reference: 'INV-2024-001' },
urls: {
success: 'https://my.site/success',
failed: 'https://my.site/failed',
cancel: 'https://my.site/canceled',
callback: 'https://my.site/webhook',
},
});
// Redirect your user to `link`
```
#### `pay.createPaymentQRCode({ merchant, bill, urls?, metadata?, validity? })`
Generates a QR code for mobile payment apps (Orange Money, MaxIt).
```javascript
const { qrId, qrCode, deepLinks, shortLink } = await pay.createPaymentQRCode({
merchant: { code: 123456, sitename: 'your-sitename' },
bill: { amount: 500, reference: 'TIP-007' },
metadata: { table: 12, waiter: 'Amadou' },
validity: 300,
});
```
<details>
<summary><b>Full response shape</b></summary>
| Field | Type | Description |
|---|---|---|
| `deepLink` | string | Universal deep link |
| `deepLinks.MAXIT` | string | MaxIt-specific link |
| `deepLinks.OM` | string | Orange Money link |
| `qrCode` | string | Base64 QR code image |
| `validity` | number | Seconds remaining |
| `metadata` | object | Your custom metadata |
| `shortLink` | string | Shortened payment URL |
| `qrId` | string | QR code identifier |
</details>
#### `pay.decodeQrCode({ id })`
Reads back the contents of a generated QR code. This is a **privileged operation** — only applications with explicit `decode_qr_sp_authorization` credentials can use it. It uses static SP authorization instead of the OAuth2 Bearer token.
```javascript
const { content } = await pay.decodeQrCode({ id: 'doyaT9sH3rGFph_ZuKIs' });
console.log(content.amount, content.reference);
```
You can also use the standalone `QRCodeDecoder` class directly:
```javascript
import { QRCodeDecoder } from '@sonatel-os/juf/payment';
const decoder = QRCodeDecoder.init();
const { content } = await decoder.decode({ id: 'doyaT9sH3rGFph_ZuKIs' });
```
## Error Handling
Every error thrown by JUF follows one consistent shape:
```javascript
import { ValidationError, AuthenticationError, ExternalServiceError } from '@sonatel-os/juf/core';
try {
await pay.preparePaymentCheckout({ /* bad data */ });
} catch (error) {
console.log(error.toJSON());
// {
// success: false,
// error: {
// code: 'JUF_VALIDATION_ERROR',
// message: 'Validation failed for preparePaymentCheckout: ...',
// details: [...]
// }
// }
}
```
| Error Class | Code | Status | When |
|---|---|:---:|---|
| `ValidationError` | `JUF_VALIDATION_ERROR` | 400 | Bad input (wrong types, missing fields, invalid URLs) |
| `AuthenticationError` | `JUF_AUTH_ERROR` | 401 | OAuth2 failure (bad credentials, expired) |
| `ExternalServiceError` | `JUF_EXTERNAL_SERVICE_ERROR` | varies | Upstream API error |
All errors extend `JufError` which extends native `Error` — `instanceof` checks work as expected.
## Project Structure
```
src/
auth/ # OAuth2 client credentials flow
communication/ # Email & SMS via Apigee
payment/ # Checkout, QR codes, decode
core/ # Errors, validation, logger, cache, HTTP client
config/ # Environment-based configuration loader
tests/ # 174 tests — unit, service, contract
```
## Bundled Logger
JUF ships with [`@sonatel-os/juf-xpress-logger`](http://git.tools.orange-sonatel.com/projects/ISC/repos/juf-xpress-logger-sdk/browse) included — no extra install needed. It's a structured Express-aware logger maintained in its own SDK repository.
```javascript
import { logger } from '@sonatel-os/juf';
logger.bootstrap({
appName: 'my-service',
logConsole: true,
});
```
> JUF also has its own **internal lightweight logger** (used for library diagnostics) separate from `juf-xpress-logger`. Set `JUF_LOG_LEVEL=debug` to see internal debug output.
## Notes
- Tokens are cached for **240 seconds** — no redundant auth calls
- All redirect URLs are validated (HTTP/HTTPS only) to prevent open redirect attacks
- Validation errors are **thrown**, not silently swallowed — always use try/catch
- If the QR code service is down during checkout, the flow gracefully falls back to USSD
- The internal logger sanitizes sensitive fields (tokens, secrets, passwords) before logging
- Services support **dependency injection** for full testability
## Contributing
```bash
git clone <repo-url> && cd juf-js
yarn install
yarn test # Run 174 tests
yarn lint # ESLint check
yarn build # Dual ESM/CJS build
```
Commits must follow [Conventional Commits](https://www.conventionalcommits.org/) (enforced by commitlint).
<p align="center">
<b>MIT License</b> — See <a href="./LICENCE">LICENCE</a><br/>
<sub>Made with care by <a href="https://github.com/lpix-11">Mohamed Johnson</a> at Sonatel</sub><br/>
<sub>Built on the <a href="https://developer.orange-sonatel.com">Orange Developer Platform</a></sub>
</p>