npm package: @sonatel-os/juf
Description: The community SDK for Orange Money, SMS, Email & Sonatel APIs on the Orange Developer Platform.
Author: Mohamed Johnson
Latest version: 0.6.0

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