npm package: @fiscapi-node/sdk
Description: SDK officiel Node.js/TypeScript pour l'API FiscAPI - Facturation e-MECeF au Bénin
Author: FiscAPI Team
Latest version: 1.3.0

# FiscAPI SDK pour Node.js

SDK officiel pour l'intégration de la facturation e-MECeF (DGI Bénin) avec FiscAPI.

[![npm version](https://badge.fury.io/js/%40fiscapi-node%2Fsdk.svg)](https://www.npmjs.com/package/@fiscapi-node/sdk)

## ✨ Nouveautés v1.3.0

- ✅ Sélection explicite de la cible `cloud` ou `local`
- ✅ Support runtime de `FISCAPI_BASE_URL` et `FISCAPI_LOCAL_BASE_URL`
- ✅ `getPdfUrl(id, { absolute: true })` pour obtenir une URL absolue correcte
- ✅ Chaîne de tests mise à niveau pour corriger les vulnérabilités connues

## ✨ Nouveautés v1.2.0

- ✅ Support complet des avoirs (FA/EA)
- ✅ Endpoint de normalisation des brouillons
- ✅ Authentification via header `x-api-key`
- ✅ Support des réponses paginées
- ✅ Calcul TVA côté client
- ✅ API v2 (compta) : vendor rules, relevés bancaires, uploads multipart

## 📦 Installation

```bash
npm install @fiscapi-node/sdk
# ou
yarn add @fiscapi-node/sdk
```

## 🔐 Sécurité - IMPORTANT

### ⚠️ NE JAMAIS exposer votre clé API côté client

```typescript
// ❌ DANGEREUX - La clé est visible dans le code source
const fiscapi = new FiscApi('sk_live_xxx'); // Dans le navigateur = FUITE
```

### ✅ Utilisez TOUJOURS le SDK côté serveur

| Architecture | Sécurité | Recommandation |
|--------------|----------|----------------|
| **Node.js / Backend** | ✅ Sécurisé | ✅ Recommandé |
| **Serverless (Vercel, Netlify)** | ✅ Sécurisé | ✅ Recommandé |
| **Frontend (React, Vue, etc.)** | ❌ Dangereux | ❌ À éviter |
| **Mobile (React Native, Flutter)** | ❌ Dangereux | ❌ À éviter |

### Architecture recommandée

```
┌─────────────────┐     ┌─────────────────┐     ┌────────────────┐
│  Votre Frontend │ --> │  Votre Backend  │ --> │   FiscAPI v1   │
│  (React, Vue)   │     │  (Node, Python) │     │ (clé API ici)  │
└─────────────────┘     └─────────────────┘     └────────────────┘
        ▲                       │
        │                       │
    Sans clé              Clé API en
    (session)             variable env
```

### Variables d'environnement

```bash
# .env (jamais commité)
FISCAPI_API_KEY=sk_live_xxx
FISCAPI_BASE_URL=https://www.fiscapi.com
FISCAPI_LOCAL_BASE_URL=http://127.0.0.1:3001

# .env.example (à committer)
FISCAPI_API_KEY=
```

```typescript
// Charger la clé depuis l'environnement
const fiscapi = new FiscApi(process.env.FISCAPI_API_KEY!);
```

### Cloud ou local

```typescript
const cloudClient = new FiscApi({
  apiKey: process.env.FISCAPI_API_KEY!,
  target: 'cloud',
});

const localClient = new FiscApi({
  apiKey: process.env.FISCAPI_API_KEY!,
  target: 'local',
});
```

Priorité de résolution:

- `baseUrl` explicite
- `target: 'local'` avec `FISCAPI_LOCAL_BASE_URL` ou `http://127.0.0.1:3001`
- sinon `FISCAPI_BASE_URL` ou `https://www.fiscapi.com`

---

## 🚀 Démarrage rapide

```typescript
import { FiscApi } from '@fiscapi-node/sdk';

// Initialiser avec votre clé API (côté serveur uniquement)
const fiscapi = new FiscApi(process.env.FISCAPI_API_KEY!);

// Ou viser explicitement une instance locale
const localFiscapi = new FiscApi({
  apiKey: process.env.FISCAPI_API_KEY!,
  target: 'local',
});

// Créer et normaliser une facture
const invoice = await fiscapi.invoices.create({
  type: 'FV',                    // FV = Facture de Vente
  items: [
    { 
      name: 'Consultation', 
      price: 50000,              // Prix TTC
      quantity: 1, 
      taxGroup: 'B'              // B = TVA 18%
    }
  ],
  client: { 
    name: 'SARL Example',
    ifu: '0101010101010'         // Optionnel
  }
});

// Récupérer les données de sécurité DGI
console.log(invoice.status);                   // 'CONFIRMED'
console.log(invoice.security?.codeMECeFDGI);   // 'TEST-XXXX-YYYY-ZZZZ'
console.log(invoice.security?.qrCode);         // QR code à imprimer
console.log(invoice.security?.nim);            // NIM de la machine
console.log(invoice.totals.total);             // 50000
console.log(invoice.totals.totalTVA);          // 7627
```

> Le SDK est un wrapper HTTP : les mêmes endpoints sont disponibles en API directe via `/api/v1/*` et `/api/v2/*` avec le header `x-api-key`.

---

## 🧱 Structure du SDK

Le client expose des ressources qui correspondent directement aux endpoints HTTP :

- `fiscapi.invoices` → API v1 (factures, normalisation, avoirs)
- `fiscapi.credits` → API v1 (crédits)
- `fiscapi.fiscalProfiles` → API v1 (profils fiscaux)
- `fiscapi.webhooks` → API v1 (webhooks)
- `fiscapi.taxGroups` → local (pas d'appel HTTP)
- `fiscapi.accounting.*` → API v2 (comptabilité)
- `fiscapi.tax.*` → API v2 (fiscalité)

---

## 🧾 API v2 — Comptabilité & fiscalité

Les endpoints compta/fiscalité sont exposés sous `/api/v2/*` et accessibles via `fiscapi.accounting.*` / `fiscapi.tax.*`.

### Dépense + justificatif (image) + OCR

```typescript
import fs from 'node:fs';

const receipt = new Blob([fs.readFileSync('./invoice.jpg')], { type: 'image/jpeg' });

const created = await fiscapi.accounting.expenses.createWithReceipt(
  { vendorName: 'SBEE', amountTtc: 12000, runOcr: true },
  receipt,
  { fileName: 'invoice.jpg' },
);

await fiscapi.accounting.expenses.ocr(created.expenseId);
```

### Import relevé bancaire

```typescript
const statementFile = new Blob([fs.readFileSync('./releve.csv')], { type: 'text/csv' });

const statement = await fiscapi.accounting.bankStatements.create(statementFile, {
  fileName: 'releve.csv',
  label: 'Banque Janvier',
  bankAccountCode: '521',
  currency: 'XOF',
});
```

---

## 📋 Format de requête (Format DGI)

```typescript
interface CreateInvoicePayload {
  // Type de facture
  type: 'FV' | 'FA' | 'EV' | 'EA';  // FV=Vente, FA=Avoir, EV=Export, EA=Avoir Export
  
  // AIB (Acompte Impôt Bénéfice)
  aib?: 'A' | 'B' | null;           // A=1%, B=5%, null=pas d'AIB
  
  // Client (optionnel pour vente au comptoir)
  client?: {
    name: string;                    // Nom (min 5 caractères)
    ifu?: string;                    // IFU 13 chiffres
    contact?: string;                // Téléphone
    address?: string;                // Adresse
  };
  
  // Articles
  items: Array<{
    name: string;                    // Désignation
    price: number;                   // Prix unitaire
    quantity: number;                // Quantité
    taxGroup: 'A'|'B'|'C'|'D'|'E'|'F';
    taxSpecific?: number;            // Taxe spécifique par unité
  }>;
  
  // Paiements
  payment?: Array<{
    name: 'ESPECES' | 'VIREMENT' | 'CARTEBANCAIRE' | 'MOBILEMONEY';
    amount: number;
  }>;
  
  // Options
  pricingMode?: 'HT' | 'TTC';        // Défaut: TTC
  autoNormalize?: boolean;            // Défaut: true
  reference?: string;                 // Pour avoirs FA/EA
}
```

---

## 📊 Groupes de taxes

| Code | Nom | Taux | Description |
|------|-----|------|-------------|
| `A` | Exonéré | 0% | Produits exonérés de TVA |
| `B` | Taxable | 18% | TVA standard |
| `C` | Exportation | 0% | Vente à l'export |
| `D` | Exception | 0% | Régime d'exception |
| `E` | TPS | 0% | Taxe Professionnelle Synthétique |
| `F` | Spécifique | 0% | Taxe spécifique uniquement |

---

## 📝 Exemples

### Facture simple (FV)

```typescript
const invoice = await fiscapi.invoices.create({
  type: 'FV',
  client: { name: 'Client Simple' },
  items: [
    { name: 'Consultation', price: 50000, quantity: 1, taxGroup: 'B' }
  ]
});

console.log(invoice.status);                  // 'CONFIRMED'
console.log(invoice.security?.codeMECeFDGI);  // 'TEST-XXXX-...'
```

### Facture multi-lignes

```typescript
const invoice = await fiscapi.invoices.create({
  type: 'FV',
  client: {
    name: 'Entreprise ABC',
    ifu: '0101010101010',
    contact: '+229 97 00 00 00',
    address: 'Cotonou, Bénin'
  },
  items: [
    { name: 'Ordinateur portable', price: 350000, quantity: 2, taxGroup: 'B' },
    { name: 'Souris sans fil', price: 15000, quantity: 5, taxGroup: 'B' },
    { name: 'Câble HDMI', price: 5000, quantity: 3, taxGroup: 'A' }  // Exonéré
  ]
});
```

### Facture avec AIB

```typescript
const invoice = await fiscapi.invoices.create({
  type: 'FV',
  aib: 'A',  // 1% du montant
  client: {
    name: 'Client avec AIB',
    ifu: '0202020202020'
  },
  items: [
    { name: 'Prestation de services', price: 100000, quantity: 1, taxGroup: 'B' }
  ]
});

console.log(invoice.totals.aib);  // Montant AIB (1000 FCFA)
```

### Facture d'exportation (EV)

```typescript
const invoice = await fiscapi.invoices.create({
  type: 'EV',
  client: {
    name: 'Client Export International',
    address: 'Paris, France'
  },
  items: [
    { name: 'Noix de cajou (export)', price: 500000, quantity: 10, taxGroup: 'C' },
    { name: 'Karité brut (export)', price: 250000, quantity: 5, taxGroup: 'C' }
  ]
});

console.log(invoice.totals.totalTVA);  // 0 (pas de TVA à l'export)
```

### Mode HT (prix hors taxes)

```typescript
const invoice = await fiscapi.invoices.create({
  type: 'FV',
  pricingMode: 'HT',  // Prix hors taxes
  client: { name: 'Client HT' },
  items: [
    { name: 'Service', price: 10000, quantity: 1, taxGroup: 'B' }
  ]
});

// Le SDK calcule automatiquement la TVA
console.log(invoice.totals.totalHT);   // 10000
console.log(invoice.totals.totalTVA);  // 1800
console.log(invoice.totals.total);     // 11800
```

---

## 💳 Avoirs (Remboursements)

### Avoir total (remboursement complet)

```typescript
// Créer d'abord une facture
const invoice = await fiscapi.invoices.create({
  type: 'FV',
  client: { name: 'Client à rembourser' },
  items: [
    { name: 'Produit', price: 30000, quantity: 1, taxGroup: 'B' }
  ]
});

// Attendre 4 secondes pour la prise en compte DGI
await new Promise(resolve => setTimeout(resolve, 4000));

// Créer l'avoir total
const creditNote = await fiscapi.invoices.refund(invoice.uid, 'Annulation commande');

console.log(creditNote.type);                    // 'FA'
console.log(creditNote.status);                  // 'CONFIRMED'
console.log(creditNote.security?.codeMECeFDGI);  // Code de l'avoir
```

### Avoir partiel (remboursement sélectif)

```typescript
// Rembourser 2 unités de la ligne 1
const creditNote = await fiscapi.invoices.refundPartial('clx123abc', {
  lines: [
    { position: 1, quantity: 2 },  // Ligne 1, 2 unités
    { position: 3 },                // Ligne 3 entière
  ],
  reason: 'Retour produit défectueux'
});
```

---

## 📄 Brouillons

### Créer un brouillon (sans normaliser)

```typescript
const draft = await fiscapi.invoices.createDraft({
  type: 'FV',
  items: [{ name: 'Test', price: 25000, quantity: 2, taxGroup: 'B' }],
  client: { name: 'Client Draft' }
});

console.log(draft.status);  // 'DRAFT'
```

### Normaliser le brouillon plus tard

```typescript
const confirmed = await fiscapi.invoices.normalize(draft.uid);
console.log(confirmed.status);                   // 'CONFIRMED'
console.log(confirmed.security?.codeMECeFDGI);   // Code MECeF
```

---

## ⚠️ Gestion des erreurs

```typescript
import { FiscApiError } from '@fiscapi-node/sdk';

try {
  const invoice = await fiscapi.invoices.create({ ... });
} catch (error) {
  if (error instanceof FiscApiError) {
    console.error('Code:', error.code);      // 'VALIDATION_ERROR'
    console.error('Message:', error.message);
    console.error('Status:', error.status);  // 422
    console.error('Details:', error.details);
    
    // Codes d'erreur possibles
    switch (error.code) {
      case 'VALIDATION_ERROR':
        // Payload invalide
        break;
      case 'INSUFFICIENT_CREDITS':
        // Pas assez de crédits
        break;
      case 'NORMALIZATION_ERROR':
        // Erreur e-MCF (rejet DGI)
        break;
      case 'UNAUTHORIZED':
        // Clé API invalide
        break;
    }
  }
}
```

---

## 🛠 Utilitaires

### Calculer la TVA (synchrone)

```typescript
// Calculer à partir du TTC
const result = fiscapi.taxGroups.calculateVat(11800, 'B', 'TTC');
console.log(result);  // { ht: 10000, tax: 1800, ttc: 11800 }

// Calculer à partir du HT
const result2 = fiscapi.taxGroups.calculateVat(10000, 'B', 'HT');
console.log(result2);  // { ht: 10000, tax: 1800, ttc: 11800 }

// Exonéré
const result3 = fiscapi.taxGroups.calculateVat(50000, 'A', 'TTC');
console.log(result3);  // { ht: 50000, tax: 0, ttc: 50000 }
```

### Vérifier les crédits

```typescript
const balance = await fiscapi.credits.getBalance();
console.log(`Crédits: ${balance.available}/${balance.total}`);

if (balance.isLow) {
  console.warn('Solde bas ! Rechargez vos crédits.');
}

// Vérifier avant un batch
const canProcess = await fiscapi.credits.hasSufficientBalance(100);
```

### Vérifier la connexion

```typescript
const isHealthy = await fiscapi.ping();
if (!isHealthy) {
  console.error('API FiscAPI indisponible');
}
```

### Lister les factures avec pagination

```typescript
const { data: invoices, count } = await fiscapi.invoices.list({
  limit: 20,
  status: 'CONFIRMED'
});

console.log(`${count} factures trouvées`);
invoices.forEach(inv => console.log(inv.uid, inv.status));
```

---

## ⚙️ Configuration avancée

```typescript
const fiscapi = new FiscApi({
  apiKey: process.env.FISCAPI_API_KEY!,
  baseUrl: 'https://www.fiscapi.com',  // Optionnel
  timeout: 30000,                       // Optionnel (ms)
});
```

---

## 🔄 Polling (attente de confirmation)

```typescript
// Attendre qu'une facture soit confirmée
const confirmed = await fiscapi.invoices.waitForConfirmation('clx123abc', {
  maxAttempts: 30,  // Défaut: 30
  intervalMs: 1000, // Défaut: 1000ms
});
```

---

## 📚 API Reference

### `fiscapi.invoices`

| Méthode | Description |
|---------|-------------|
| `create(payload)` | Créer et normaliser une facture |
| `createDraft(payload)` | Créer un brouillon |
| `get(id)` | Récupérer une facture |
| `list(filters?)` | Lister les factures (paginé) |
| `normalize(id)` | Normaliser un brouillon |
| `refund(id, reason?)` | Avoir total |
| `refundPartial(id, options)` | Avoir partiel |
| `isConfirmed(id)` | Vérifier si confirmée |
| `waitForConfirmation(id, options?)` | Polling |
| `getPdfUrl(id, options?)` | URL du PDF |

### `fiscapi.credits`

| Méthode | Description |
|---------|-------------|
| `getBalance()` | Solde de crédits |
| `hasSufficientBalance(n)` | Vérifier solde suffisant |
| `listPacks()` | Packs disponibles |

### `fiscapi.fiscalProfiles`

| Méthode | Description |
|---------|-------------|
| `list()` | Lister les profils |
| `get(id)` | Récupérer un profil |
| `hasValidToken(id)` | Token e-MCF valide ? |

### `fiscapi.taxGroups`

| Méthode | Description |
|---------|-------------|
| `list()` | Tous les groupes |
| `get(code)` | Infos d'un groupe |
| `calculateVat(amount, taxGroup, mode)` | Calculer TVA |

---

## 🔁 Mapping SDK ↔ Endpoints HTTP

### API v1 (facturation, crédits, webhooks)

| SDK | HTTP |
| --- | --- |
| `fiscapi.invoices.create(payload)` | `POST /api/v1/invoices` |
| `fiscapi.invoices.createDraft(payload)` | `POST /api/v1/invoices` (`autoNormalize=false`) |
| `fiscapi.invoices.get(id)` | `GET /api/v1/invoices/{id}` |
| `fiscapi.invoices.list(filters)` | `GET /api/v1/invoices` |
| `fiscapi.invoices.normalize(id)` | `POST /api/v1/invoices/{id}/normalize` |
| `fiscapi.invoices.refund(id, reason?)` | `POST /api/v1/invoices/{id}/credit-note` (`mode=TOTAL`) |
| `fiscapi.invoices.refundPartial(id, opts)` | `POST /api/v1/invoices/{id}/credit-note` (`mode=PARTIAL`) |
| `fiscapi.invoices.getPdfUrl(id, { absolute: true })` | `GET /api/v1/invoices/{id}/pdf` |
| `fiscapi.credits.getBalance()` | `GET /api/v1/credits/balance` |
| `fiscapi.credits.listPacks()` | `GET /api/v1/credits/packs` |
| `fiscapi.fiscalProfiles.list()` | `GET /api/v1/fiscal-profiles` |
| `fiscapi.fiscalProfiles.get(id)` | `GET /api/v1/fiscal-profiles/{id}` |
| `fiscapi.webhooks.create(config)` | `POST /api/v1/webhooks` |
| `fiscapi.webhooks.list()` | `GET /api/v1/webhooks` |
| `fiscapi.webhooks.get(id)` | `GET /api/v1/webhooks/{id}` |
| `fiscapi.webhooks.update(id, patch)` | `PATCH /api/v1/webhooks/{id}` |
| `fiscapi.webhooks.delete(id)` | `DELETE /api/v1/webhooks/{id}` |
| `fiscapi.webhooks.rotateSecret(id)` | `POST /api/v1/webhooks/{id}/rotate-secret` |
| `fiscapi.taxGroups.*` | local (pas d'appel HTTP) |

### API v2 (comptabilité & fiscalité)

| SDK | HTTP |
| --- | --- |
| `fiscapi.accounting.journal.list(filters)` | `GET /api/v2/accounting/journal` |
| `fiscapi.accounting.journal.get(id)` | `GET /api/v2/accounting/journal/{id}` |
| `fiscapi.accounting.journal.create(payload)` | `POST /api/v2/accounting/journal` |
| `fiscapi.accounting.journal.post(id)` | `POST /api/v2/accounting/journal/{id}/post` |
| `fiscapi.accounting.vendors.list(filters)` | `GET /api/v2/accounting/vendors` |
| `fiscapi.accounting.vendors.upsert(payload)` | `POST /api/v2/accounting/vendors` |
| `fiscapi.accounting.expenses.list(filters)` | `GET /api/v2/accounting/expenses` |
| `fiscapi.accounting.expenses.create(payload)` | `POST /api/v2/accounting/expenses` |
| `fiscapi.accounting.expenses.createWithReceipt(payload, file)` | `POST /api/v2/accounting/expenses` (multipart) |
| `fiscapi.accounting.expenses.get(id)` | `GET /api/v2/accounting/expenses/{id}` |
| `fiscapi.accounting.expenses.update(id, payload)` | `PUT /api/v2/accounting/expenses/{id}` |
| `fiscapi.accounting.expenses.delete(id)` | `DELETE /api/v2/accounting/expenses/{id}` |
| `fiscapi.accounting.expenses.ocr(id)` | `POST /api/v2/accounting/expenses/{id}/ocr` |
| `fiscapi.accounting.expenses.recognize(id)` | `POST /api/v2/accounting/expenses/{id}/recognize` |
| `fiscapi.accounting.expenses.pay(id, payload)` | `POST /api/v2/accounting/expenses/{id}/payments` |
| `fiscapi.accounting.expenses.uploadDocument(id, file)` | `POST /api/v2/accounting/expenses/{id}/documents` (multipart) |
| `fiscapi.accounting.documents.getSignedUrl(id)` | `GET /api/v2/accounting/documents/{id}/signed-url` |
| `fiscapi.accounting.vendorRules.list(filters)` | `GET /api/v2/accounting/vendor-rules` |
| `fiscapi.accounting.vendorRules.create(payload)` | `POST /api/v2/accounting/vendor-rules` |
| `fiscapi.accounting.vendorRules.update(id, payload)` | `PUT /api/v2/accounting/vendor-rules/{id}` |
| `fiscapi.accounting.vendorRules.delete(id)` | `DELETE /api/v2/accounting/vendor-rules/{id}` |
| `fiscapi.accounting.bankStatements.list(filters)` | `GET /api/v2/accounting/bank-statements` |
| `fiscapi.accounting.bankStatements.create(file, payload)` | `POST /api/v2/accounting/bank-statements` (multipart) |
| `fiscapi.accounting.bankStatements.get(id)` | `GET /api/v2/accounting/bank-statements/{id}` |
| `fiscapi.accounting.bankStatements.getSignedUrl(id)` | `GET /api/v2/accounting/bank-statements/{id}/signed-url` |
| `fiscapi.accounting.bankStatements.getSuggestions(id, params)` | `GET /api/v2/accounting/bank-statements/{id}/suggestions` |
| `fiscapi.accounting.bankStatements.createMatch(id, payload)` | `POST /api/v2/accounting/bank-statements/{id}/matches` |
| `fiscapi.accounting.bankStatements.deleteMatch(id, params)` | `DELETE /api/v2/accounting/bank-statements/{id}/matches` |
| `fiscapi.tax.obligations.list(filters)` | `GET /api/v2/tax/obligations` |
| `fiscapi.tax.obligations.upsert(payload)` | `POST /api/v2/tax/obligations` |
| `fiscapi.tax.paymentOrders.list(filters)` | `GET /api/v2/tax/payment-orders` |
| `fiscapi.tax.paymentOrders.create(payload)` | `POST /api/v2/tax/payment-orders` |

---

## 🧪 Tests effectués

Le SDK a été testé avec succès pour :

- ✅ Facture de Vente simple (FV)
- ✅ Facture multi-lignes avec TVA mixte
- ✅ Facture avec AIB (1% et 5%)
- ✅ Facture d'Exportation (EV)
- ✅ Création et normalisation de brouillons
- ✅ Avoir total (FA) avec code MECeF
- ✅ Mode de tarification HT et TTC
- ✅ Calculs TVA synchrones

---

## 🆘 Support

- 📖 Documentation: https://fiscapi.com/developers
- 📧 Email: support@fiscapi.com
- 🖥️ Dashboard: https://fiscapi.com/dashboard

## 📄 Licence

MIT