UNPKG

@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
<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>