An open API service indexing awesome lists of open source software.

https://github.com/qrcommunication/scell-io-scell-php

Sdk Scell.io
https://github.com/qrcommunication/scell-io-scell-php

Last synced: 17 days ago
JSON representation

Sdk Scell.io

Awesome Lists containing this project

README

          

# Scell.io PHP SDK

[![Latest Version on Packagist](https://img.shields.io/packagist/v/scell/sdk.svg?style=flat-square)](https://packagist.org/packages/scell/sdk)
[![Total Downloads](https://img.shields.io/packagist/dt/scell/sdk.svg?style=flat-square)](https://packagist.org/packages/scell/sdk)
[![License](https://img.shields.io/packagist/l/scell/sdk.svg?style=flat-square)](https://packagist.org/packages/scell/sdk)
[![PHP Version](https://img.shields.io/packagist/php-v/scell/sdk.svg?style=flat-square)](https://packagist.org/packages/scell/sdk)

SDK PHP officiel pour l'API Scell.io - Facturation electronique (Factur-X/UBL/CII) et signature electronique (eIDAS EU-SES).

## Features

- Facturation electronique conforme (Factur-X, UBL 2.1, UN/CEFACT CII)
- Support B2B et **B2C** (particulier) avec generation Factur-X conforme BR-CO-26 EN16931
- Signature electronique simple (eIDAS EU-SES)
- Gestion multi-tenant (sub-tenants, factures directes et entrantes)
- Conformite fiscale ISCA (integrite, clotures, FEC, attestations)
- Statistiques et facturation plateforme
- Integration Laravel native avec auto-discovery
- Builders fluent pour factures et signatures
- Verification HMAC-SHA256 des webhooks
- Retry automatique avec backoff exponentiel
- DTOs types et Enums PHP 8.2+
- Gestion d'erreurs complete

## Installation

```bash
composer require scell/sdk
```

## Configuration

### Variables d'environnement

```env
# API
SCELL_BASE_URL=https://api.scell.io/api/v1
SCELL_API_KEY=tk_live_...

# Optionnel - Pour le dashboard (Bearer token)
SCELL_BEARER_TOKEN=eyJ...

# Webhooks
SCELL_WEBHOOK_SECRET=whsec_...
```

## Utilisation Standalone

### Client API (integration backend)

Pour les integrations serveur-a-serveur avec API Key:

```php
use Scell\Sdk\ScellApiClient;
use Scell\Sdk\DTOs\Address;
use Scell\Sdk\Enums\AuthMethod;

// Initialisation
$api = ScellApiClient::withApiKey('tk_live_...');

// Mode sandbox pour les tests
$api = ScellApiClient::sandbox('tk_test_...');
```

### Creer une facture

```php
use Scell\Sdk\DTOs\Address;

$invoice = $api->invoices()->builder()
->externalId('my-internal-id')
->outgoing()
->facturX()
->issueDate(new DateTime())
->dueDate((new DateTime())->modify('+30 days'))
->seller(
siret: '12345678901234',
name: 'Ma Societe SARL',
address: new Address(
line1: '1 Rue de la Paix',
postalCode: '75001',
city: 'Paris'
)
)
->buyer(
siret: '98765432109876',
name: 'Client SA',
address: new Address(
line1: '2 Avenue du Commerce',
postalCode: '69001',
city: 'Lyon'
)
)
->addLine('Prestation de conseil', 10, 100.00, 20.0)
->addLine('Formation', 2, 500.00, 20.0)
->archiveEnabled()
->create();

echo "Facture creee: {$invoice->id}";
echo "Total TTC: {$invoice->totalTtc} EUR";

> **Note:** Invoice and credit note numbers are automatically generated by Scell.io. Draft documents receive a temporary number (DRAFT-XXXXX-00001), and the definitive fiscal number (XXXXX-YYYYMM-00001) is assigned at submission.
```

### Facturation internationale

Pour les parties non-francaises, le SIRET n'est pas requis. Utilisez les numeros de TVA pour les entreprises EU et `legal_id` avec un code de schema pour les entreprises hors-EU.

#### Facture avec acheteur belge (EU)

```php
$invoice = $client->invoices()->create([
'issue_date' => '2026-03-29',
'due_date' => '2026-04-28',
'currency' => 'EUR',
// Vendeur francais (SIRET requis)
'seller_siret' => '12345678901234',
'seller_name' => 'Ma Société SAS',
'seller_country' => 'FR',
'seller_vat_number' => 'FR12345678901',
'seller_address' => ['line1' => '10 rue de Paris', 'postal_code' => '75001', 'city' => 'Paris', 'country' => 'FR'],
// Acheteur belge (pas de SIRET, numero de TVA)
'buyer_name' => 'Entreprise Belge SPRL',
'buyer_country' => 'BE',
'buyer_vat_number' => 'BE0123456789',
'buyer_address' => ['line1' => '15 Avenue Louise', 'postal_code' => '1050', 'city' => 'Bruxelles', 'country' => 'BE'],
'lines' => [
['description' => 'Consulting services', 'quantity' => 10, 'unit_price' => 150.00, 'vat_rate' => 0],
],
'format' => 'ubl',
]);
```

> **Note :** Pour les transactions B2B intra-communautaires (ex: FR -> BE, FR -> DE), le taux de TVA est generalement 0% via le mecanisme d'autoliquidation. L'acheteur comptabilise la TVA dans son propre pays.

#### Facture avec acheteur UK (hors-EU)

Pour les acheteurs hors-EU, utilisez `buyer_legal_id` et `buyer_legal_id_scheme` en plus du numero de TVA :

```php
$invoice = $client->invoices()->create([
'issue_date' => '2026-03-29',
'due_date' => '2026-04-28',
'currency' => 'GBP',
'seller_siret' => '12345678901234',
'seller_name' => 'Ma Société SAS',
'seller_country' => 'FR',
'seller_vat_number' => 'FR12345678901',
'seller_address' => ['line1' => '10 rue de Paris', 'postal_code' => '75001', 'city' => 'Paris', 'country' => 'FR'],
// Acheteur UK — legal_id avec schema
'buyer_name' => 'British Ltd',
'buyer_country' => 'GB',
'buyer_vat_number' => 'GB123456789',
'buyer_legal_id' => '12345678',
'buyer_legal_id_scheme' => '0088',
'buyer_address' => ['line1' => '20 Baker Street', 'postal_code' => 'W1U 3BW', 'city' => 'London', 'country' => 'GB'],
'lines' => [
['description' => 'Design services', 'quantity' => 5, 'unit_price' => 200.00, 'vat_rate' => 0],
],
'format' => 'ubl',
]);
```

### Creer une signature

```php
use Scell\Sdk\Enums\AuthMethod;

$signature = $api->signatures()->builder()
->title('Contrat de prestation')
->description('Contrat annuel de maintenance')
->externalId('contract-2024-001')
->documentFromFile('/path/to/contract.pdf')
->addEmailSigner('Jean', 'Dupont', 'jean.dupont@example.com')
->addSmsSigner('Marie', 'Martin', '+33612345678')
->addSignaturePosition(page: 5, x: 100, y: 700, width: 200, height: 50)
->uiConfig([
'sidebar_logo' => 'https://example.com/logo.png',
'sidebar_background_color' => '#0066CC',
])
->redirectUrls(
completeUrl: 'https://example.com/signed',
cancelUrl: 'https://example.com/cancelled'
)
->expiresAt((new DateTime())->modify('+7 days'))
->create();

echo "Signature creee: {$signature->id}";
echo "Statut: {$signature->status->label()}";

// Recuperer les URLs de signature
foreach ($signature->signers as $signer) {
echo "{$signer->fullName()}: {$signer->signingUrl}";
}
```

#### White-label avance + options de signature (v1.12.0)

```php
$signature = $api->signatures()->builder()
->title('Contrat NDA')
->documentFromFile('/path/to/nda.pdf')
->addEmailSigner('Jean', 'Dupont', 'jean@example.com', message: 'Bonjour, votre code OTP : {OTP}')
->addSignaturePosition(page: 1, x: 70, y: 85, width: 20, height: 5, unit: 'percent')
->uiConfig([
'sidebar_logo' => 'https://cdn.example.com/logo.svg',
'sidebar_background_color' => '#0F172A',
'sidebar_text_color' => '#FFFFFF',
'hide_branding' => true,
'iframe_ancestors' => ['https://app.example.com'],
])
->signatureOptions([
'signature_mode' => 'both', // 'draw' | 'type' | 'both'
'signer_must_read' => true,
'user_editable_data' => false,
'timezone' => 'Europe/Paris',
])
->create();
```

#### Positions multiples par signataire (v2.27.0)

Le champ optionnel `signerIndex` (0-base) affecte explicitement une position de
signature a un signataire precis (`0` = premier signataire ajoute, `1` = deuxieme,
etc.). EU-SES autorise desormais **plusieurs positions pour un meme signataire** :
appelez `addSignaturePosition()` autant de fois que necessaire avec le meme
`signerIndex`.

```php
$signature = $api->signatures()->builder()
->title('Contrat bipartite multi-pages')
->documentFromFile('/path/to/contract.pdf')
->addEmailSigner('Jean', 'Dupont', 'jean@example.com') // index 0
->addSmsSigner('Marie', 'Martin', '+33612345678') // index 1

// Le signataire 0 signe sur DEUX pages (1 et 3).
->addSignaturePosition(page: 1, x: 70, y: 80, signerIndex: 0)
->addSignaturePosition(page: 3, x: 70, y: 80, signerIndex: 0)

// Le signataire 1 signe une seule fois (page 3).
->addSignaturePosition(page: 3, x: 30, y: 80, signerIndex: 1)

->create();
```

Notes :
- `signerIndex` est **optionnel** et 100% retrocompatible : sans lui, le mapping
positionnel historique (1 position par signataire dans l'ordre) reste applique.
- Combinable avec `documentIndex` (multi-document) : un signataire peut signer
sur plusieurs documents du bundle.

#### Blocs personnalises (paraphe + mentions + date) — v2.12.0

```php
use Scell\Sdk\DTOs\BlockPosition;
use Scell\Sdk\DTOs\DateBlock;
use Scell\Sdk\DTOs\InitialsBlock;
use Scell\Sdk\DTOs\Mention;

$signature = $api->signatures()->builder()
->title('Contrat de prestation')
->documentFromFile('/path/to/contract.pdf')
->addEmailSigner('Jean', 'Dupont', 'jean@example.com')

// 1) Bloc paraphe : initiales auto sur toutes les pages (sauf derniere).
// Accepte un tableau brut OU un DTO InitialsBlock.
->initialsBlock([
'enabled' => true,
'mode' => 'auto', // 'auto' | 'custom'
'source' => 'signer_name', // 'signer_name' | 'custom'
'pages' => 'except_last', // 'all' | 'except_last' | [1,2,5]
'position' => ['x' => 90, 'y' => 95, 'unit' => 'percent'],
'font_size' => 10,
'color' => '#333333',
])

// 2) Mentions juridiques per-signer (label + position + signer_index 0-based).
// Le signataire saisit le texte (required=true) ; sinon fallback_text est grave.
->addMention(new Mention(
label: 'Lu et approuve',
signerIndex: 0,
position: new BlockPosition(x: 10, y: 80, page: 1, w: 60, h: 8),
required: true,
fallbackText: 'Lu et approuve',
))

// 3) Bloc date du jour (page 'last' = derniere page calculee par le backend).
->dateBlock(new DateBlock(
enabled: true,
position: new BlockPosition(x: 80, y: 10, page: 'last'),
format: 'd/m/Y',
timezone: 'Europe/Paris',
))

->create();
```

Notes :
- Les 3 champs sont **optionnels** et 100% retrocompatibles avec les payloads pre-v2.12.0.
- Chaque setter accepte un tableau associatif (snake_case) OU un DTO type. Les DTO valident defensivement les valeurs au constructeur.
- `BlockPosition::page` accepte un entier (1-indexe) pour les mentions ; pour `initialsBlock` / `dateBlock`, la chaine `'last'` est aussi acceptee.

### Client Dashboard (Bearer token)

Pour les operations via le dashboard utilisateur:

```php
use Scell\Sdk\ScellClient;

$client = new ScellClient($bearerToken);

// Lister les factures
$invoices = $client->invoices()->list([
'direction' => 'outgoing',
'status' => 'validated',
'per_page' => 50,
]);

foreach ($invoices->data as $invoice) {
echo "{$invoice->invoiceNumber}: {$invoice->totalTtc} EUR";
}

// Pagination
if ($invoices->hasNextPage()) {
$nextPage = $client->invoices()->list(['page' => $invoices->nextPage()]);
}

// Consulter le solde
$balance = $client->balance()->get();
echo "Solde: {$balance->formatted()}";

if ($balance->isLow()) {
echo "Attention: solde bas!";
}

// Gerer les entreprises
$companies = $client->companies()->list();

// Gerer les webhooks
$webhooks = $client->webhooks()->list();
```

### Onboarder un nouveau partenaire (SuperPDP OAuth2)

Le flow d'onboarding intègre SuperPDP pour gérer l'inscription, le KYB et la vérification d'identité de vos utilisateurs dans une popup. Une fois le flow complété, Scell provisionne automatiquement un tenant pour l'utilisateur.

```php
use Scell\Sdk\ScellApiClient;

$api = ScellApiClient::withApiKey('tk_live_...');

// Étape 1 : Créer une session d'onboarding
$session = $api->onboarding()->createSession([
'partner_ref' => 'mon-id-utilisateur-interne',
'redirect_url' => 'https://monapp.com/onboarding/complete',
]);

// Étape 2 : Obtenir l'URL d'autorisation SuperPDP
$auth = $api->onboarding()->getSuperPDPAuthorizeUrl($session->id);
// Ouvrir $auth['authorize_url'] dans un popup côté frontend
// SuperPDP gère l'inscription, le KYB et la vérification d'identité

// Étape 3 : Réceptionner le callback SuperPDP (code + state)
$result = $api->onboarding()->superpdpCallback(
sessionId: $session->id,
code: $request->input('code'),
state: $request->input('state'),
);

if ($result['success']) {
$tenant = $result['tenant'];
echo "Tenant enrôlé : {$tenant['name']} — SIRET {$tenant['siret']}";
// $tenant : ['id', 'name', 'siret', 'environment']
}

// Consulter le statut d'une session
$status = $api->onboarding()->getSession($session['id']);
```

| Méthode | Endpoint | Description |
|---------|----------|-------------|
| `createSession(array $input)` | `POST /onboarding/sessions` | Créer une session d'onboarding |
| `getSession(string $id)` | `GET /onboarding/sessions/:id` | Consulter le statut d'une session |
| `getSuperPDPAuthorizeUrl(string $sessionId)` | `POST /onboarding/superpdp/authorize` | Obtenir l'URL OAuth2 SuperPDP |
| `superpdpCallback(string $sessionId, string $code, string $state)` | `POST /onboarding/superpdp/callback` | Finaliser l'enrôlement après redirect |

### Gerer les factures entrantes (fournisseurs)

```php
use Scell\Sdk\Enums\RejectionCode;
use Scell\Sdk\Enums\DisputeType;

// Lister les factures entrantes
$incoming = $api->invoices()->incoming([
'status' => 'pending',
'per_page' => 25,
]);

foreach ($incoming->data as $invoice) {
echo "{$invoice->invoiceNumber} - {$invoice->sellerName}: {$invoice->totalTtc} EUR";
}

// Accepter une facture
$invoice = $api->invoices()->accept($invoiceId, [
'comment' => 'Facture conforme',
]);
echo "Facture acceptee: {$invoice->status->label()}";

// Rejeter une facture avec un code de rejet
$invoice = $api->invoices()->reject(
$invoiceId,
'Le montant ne correspond pas a la commande',
RejectionCode::IncorrectAmount
);

// Contester une facture (litige)
$invoice = $api->invoices()->dispute(
$invoiceId,
'Montant facture: 1500 EUR, montant commande: 1200 EUR',
DisputeType::AmountDispute,
expectedAmount: 1200.00
);

// Marquer une facture comme payee (obligatoire dans le cycle de vie)
$invoice = $api->invoices()->markPaid($invoiceId, [
'payment_reference' => 'VIR-2026-0124',
'paid_at' => '2026-01-24T10:30:00Z',
'note' => 'Paiement recu par virement bancaire'
]);
echo "Facture payee: {$invoice->status->label()}";
echo "Reference: {$invoice->paymentReference}";

// Verifier si une facture est payee
if ($invoice->isPaid()) {
echo "Facture reglee le: {$invoice->paidAt->format('d/m/Y')}";
}
```

### Telecharger des fichiers

```php
// Obtenir une URL temporaire de telechargement (15 min)
$download = $api->invoices()->download($invoiceId, 'converted');
echo "URL: {$download['url']}";

// Telecharger le contenu binaire d'une facture PDF (Factur-X)
$pdfContent = $api->invoices()->downloadContent($invoiceId);
file_put_contents('facture.pdf', $pdfContent);

// Telecharger le contenu XML (UBL/CII)
$xmlContent = $api->invoices()->downloadContent($invoiceId, 'xml');
file_put_contents('facture.xml', $xmlContent);

// Telecharger un document signe
$download = $api->signatures()->download($signatureId, 'signed');

// Convertir une facture vers un autre format
$api->invoices()->convert($invoiceId, OutputFormat::UBL);

// Piste d'audit (factures)
$audit = $api->invoices()->auditTrail($invoiceId);
foreach ($audit['data'] as $entry) {
echo "{$entry['action']}: {$entry['details']}";
}

// Piste d'audit (signatures)
$audit = $api->signatures()->auditTrail($signatureId);
foreach ($audit['data'] as $entry) {
echo "{$entry['action']}: {$entry['details']}";
}
```

### Gerer les sub-tenants

```php
// Lister les sub-tenants
$subTenants = $api->subTenants()->list(['per_page' => 50]);

foreach ($subTenants->data as $subTenant) {
echo "{$subTenant->name} ({$subTenant->siret})";
}

// Creer un sub-tenant
$subTenant = $api->subTenants()->create([
'external_id' => 'CLIENT-001',
'name' => 'Mon Client SARL',
'siret' => '12345678901234',
'email' => 'contact@client.fr',
'address_line1' => '1 rue de la Paix',
'postal_code' => '75001',
'city' => 'Paris',
]);

// Rechercher par ID externe
$subTenant = $api->subTenants()->findByExternalId('CLIENT-001');

// Mettre a jour
$subTenant = $api->subTenants()->update($subTenantId, [
'email' => 'nouveau@email.fr',
]);
```

| Méthode | Endpoint | Description |
|---------|----------|-------------|
| `superpdpAuthorize(string $id)` | `POST /tenant/sub-tenants/:id/superpdp-authorize` | Démarrer un flow OAuth2 SuperPDP pour un sub-tenant sans access token (`{ authorize_url, state }`) |
| `getResumeUrl(string $id)` | `POST /tenant/sub-tenants/:id/resume-url` | Régénérer une URL signée de reprise d'onboarding (7 jours) |
| `superpdpDisconnect(string $id)` | `POST /tenant/sub-tenants/:id/superpdp-disconnect` | (v3.1.0) Révoquer les tokens SuperPDP du sub-tenant et repasser `onboarding_status` à `pending_superpdp`. Les factures déjà émises restent immuables (ISCA) ; les futures B2B passent en mode papier jusqu'à reconnexion. Retourne le `SubTenantSummary` |
| `superpdpReconnect(string $id)` | `POST /tenant/sub-tenants/:id/superpdp-reconnect` | (v3.1.0) Déconnexion suivie d'une nouvelle `authorize_url` en un seul appel. Retourne `SuperPDPAuthorizeUrl` |
| `superpdpWidgetToken(string $id, bool $reset = false)` | `POST /tenant/sub-tenants/:id/superpdp-widget-token` | (v3.1.0) Émettre un jeton signé (URL signée, scopée à UN sub-tenant, 24 h, anti-IDOR HMAC) pour le web component ``. `$reset = true` déconnecte avant d'émettre le jeton |

### Factures pour les sub-tenants

```php
// Creer une facture pour un sub-tenant
$invoice = $api->tenantInvoices()->createForSubTenant($subTenantId, [
'direction' => 'outgoing',
'output_format' => 'facturx',
'issue_date' => '2026-01-26',
'seller' => [...],
'buyer' => [...],
'lines' => [
[
'description' => 'Prestation de service',
'quantity' => 1,
'unit_price' => 100.00,
'tax_rate' => 20.00,
'total_ht' => 100.00,
'total_ttc' => 120.00,
],
],
// Totaux niveau facture — OBLIGATOIRES.
// Attention : la cle TVA est `total_tax` (et NON `total_tva`).
'total_ht' => 100.00,
'total_tax' => 20.00,
'total_ttc' => 120.00,
]);

// Soumettre pour traitement
$api->tenantInvoices()->submit($invoiceId);

// Factures directes (sans sub-tenant) — memes totaux obligatoires
$invoice = $api->directInvoices()->create([
'direction' => 'outgoing',
'output_format' => 'facturx',
'issue_date' => '2026-01-26',
'seller' => [...],
'buyer' => [...],
'lines' => [...],
'total_ht' => 100.00,
'total_tax' => 20.00,
'total_ttc' => 120.00,
]);

// Operations en masse
$api->directInvoices()->bulkCreate([...]);
$api->directInvoices()->bulkSubmit([$id1, $id2, $id3]);

// Factures entrantes (fournisseurs)
$incoming = $api->incomingInvoices()->listForSubTenant($subTenantId, [
'status' => 'received',
]);

// Accepter / Rejeter / Marquer comme payee
$api->incomingInvoices()->accept($invoiceId);
$api->incomingInvoices()->reject($invoiceId, 'Montant incorrect');
$api->incomingInvoices()->markPaid($invoiceId, 'VIR-2026-001');
```

### Conformite fiscale (ISCA)

```php
// Dashboard de conformite
$compliance = $api->fiscal()->compliance();

// Verification d'integrite
$report = $api->fiscal()->integrity();
$history = $api->fiscal()->integrityHistory(['per_page' => 25]);

// Clotures
$closings = $api->fiscal()->closings();
$api->fiscal()->performDailyClosing();

// Export FEC
$fec = $api->fiscal()->fecExport(['year' => 2025]);

// Attestation annuelle
$attestation = $api->fiscal()->attestation(2025);
$pdf = $api->fiscal()->attestationDownload(2025);

// Ecritures comptables
$entries = $api->fiscal()->entries(['per_page' => 100]);

// Ancres d'integrite
$anchors = $api->fiscal()->anchors();

// Regles fiscales
$rules = $api->fiscal()->rules();
$api->fiscal()->createRule([...]);
```

### Documents de conformité ISCA

```php
// Registre des mesures
$pdf = $api->fiscal()->downloadMeasuresRegister();
file_put_contents('registre-mesures-isca.pdf', $pdf);

// Dossier technique
$pdf = $api->fiscal()->downloadTechnicalDossier();
file_put_contents('dossier-technique-isca.pdf', $pdf);

// Auto-attestation ISCA
$pdf = $api->fiscal()->downloadSelfAttestation();
file_put_contents('auto-attestation-isca.pdf', $pdf);
```

### Statistiques et facturation

```php
// Vue d'ensemble
$stats = $api->stats()->overview();

// Statistiques mensuelles
$monthly = $api->stats()->monthly(['year' => 2025]);

// Stats par sub-tenant
$stats = $api->stats()->subTenantOverview($subTenantId);

// Facturation plateforme
$invoices = $api->billing()->invoices();
$usage = $api->billing()->usage();
$transactions = $api->billing()->transactions();

// Recharger le solde
$api->billing()->topUp(['amount' => 100.00]);

// Confirmer un rechargement (apres validation paiement)
$api->billing()->confirmTopUp(['payment_intent_id' => 'pi_...']);
```

### Gerer les avoirs (Credit Notes)

```php
// Lister les avoirs
$creditNotes = $api->creditNotes()->list($subTenantId, [
'status' => 'draft',
'per_page' => 25,
]);

// Verifier les montants creditables
$remaining = $api->creditNotes()->remainingCreditable($invoiceId);

// Creer un avoir partiel
$creditNote = $api->creditNotes()->create($subTenantId, [
'invoice_id' => $invoiceId,
'reason' => 'Remise commerciale',
'type' => 'partial',
'items' => [
[
'description' => 'Remise sur prestation',
'quantity' => 1,
'unit_price' => 100.00,
'tax_rate' => 20.0,
],
],
]);

// Creer un avoir total
$creditNote = $api->creditNotes()->create($subTenantId, [
'invoice_id' => $invoiceId,
'reason' => 'Annulation de la commande',
'type' => 'full',
]);

// Envoyer l'avoir
$api->creditNotes()->send($creditNoteId);

// Telecharger le PDF
$pdf = $api->creditNotes()->download($creditNoteId);
file_put_contents('avoir.pdf', $pdf);
```

### Resolution TVA cross-border (v2.18.0)

Avant d'emettre une facture vers un client etranger, interrogez le moteur
de regles TVA pour determiner la categorie applicable (autoliquidation,
hors-champ, taux reduit, etc.) :

```php
use Scell\Sdk\Builders\InvoiceLineBuilder;
use Scell\Sdk\DTOs\Vat\BuyerContext;
use Scell\Sdk\DTOs\LineVatContext;
use Scell\Sdk\Enums\VatCategory;

// --- Mode 1 : buyer enregistre dans le registre ---
$resolution = $api->buyers()->vatContext(
buyerOrInput: '019cb416-b6db-730c-b3a5-f8b7a4512eb1',
line: ['category' => 'STANDARD'],
);
echo $resolution->rate; // 0.0 (autoliquidation)
echo $resolution->category->value; // 'REVERSE_CHARGE'
echo $resolution->en16931Code; // 'AE'
echo $resolution->justification; // "TVA non applicable, art. 259-1 du CGI"

// --- Mode 2 : buyer inline ---
$resolution = $api->buyers()->vatContext(
buyerOrInput: new BuyerContext(
country: 'DE',
vatNumber: 'DE123456789',
vatNumberValid: true,
),
line: new LineVatContext(category: VatCategory::Standard),
);

// --- Override art. 259 A CGI (lieu de prestation force) ---
$resolution = $api->buyers()->vatContext(
buyerOrInput: ['country' => 'DE', 'vat_number' => 'DE123456789'],
line: ['category' => 'STANDARD', 'place_of_supply' => 'FR'],
);
echo $resolution->category->value; // 'STANDARD' — 20 % TVA FR appliquee

// --- Builder de ligne avec categorie derivee ---
$line = (new InvoiceLineBuilder())
->withDescription('Logiciel SaaS')
->withQuantity(1)
->withUnitPrice(500.00)
->withCategory($resolution->category) // derive le tax_rate + metadata
->withPlaceOfSupply('FR') // art. 259 A CGI
->build();
// $line['tax_rate'] = 0.0
// $line['metadata']['category'] = 'REVERSE_CHARGE'
// $line['metadata']['exemption_reason'] = 'reverse_charge'
// $line['metadata']['place_of_supply'] = 'FR'
```

**VatCategory helpers** (enum `Scell\Sdk\Enums\VatCategory`) :
- `defaultRate()` — taux FR par defaut (ex: 20.0 pour STANDARD, 0.0 pour REVERSE_CHARGE)
- `en16931Code()` — code XML EN16931 (S / Z / E / AE / O)
- `exemptionReason()` — raison si taux nul, null sinon

## Integration Laravel

### Installation

Le SDK supporte l'auto-discovery Laravel. Publiez la configuration:

```bash
php artisan vendor:publish --tag=scell-config
```

### Configuration (.env)

```env
SCELL_API_KEY=tk_live_...
SCELL_WEBHOOK_SECRET=whsec_...
```

### Utilisation avec Facades

```php
use Scell\Sdk\Laravel\Facades\ScellApi;
use Scell\Sdk\Laravel\Facades\Scell;
use Scell\Sdk\Laravel\Facades\ScellWebhook;

// Creer une facture (API Key)
$invoice = ScellApi::invoices()->builder()
->outgoing()
->facturX()
// ...
->create();

// Consulter le solde (Bearer token)
$balance = Scell::balance()->get();

// Verifier un webhook
$payload = ScellWebhook::verify(
request()->getContent(),
request()->header('X-Scell-Signature')
);
```

### Controller de Webhook

```php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Scell\Sdk\Laravel\Facades\ScellWebhook;
use Scell\Sdk\Exceptions\ScellException;

class ScellWebhookController extends Controller
{
public function handle(Request $request)
{
try {
$payload = ScellWebhook::verify(
$request->getContent(),
$request->header('X-Scell-Signature')
);
} catch (ScellException $e) {
return response()->json(['error' => 'Signature invalide'], 400);
}

$event = $payload['event'];
$data = $payload['data'];

match ($event) {
'invoice.validated' => $this->handleInvoiceValidated($data),
'invoice.transmitted' => $this->handleInvoiceTransmitted($data),
'signature.completed' => $this->handleSignatureCompleted($data),
'signature.refused' => $this->handleSignatureRefused($data),
'balance.low' => $this->handleBalanceLow($data),
default => null,
};

return response()->json(['received' => true]);
}

private function handleInvoiceValidated(array $data): void
{
// Traiter la facture validee
$invoiceId = $data['id'];
// ...
}

private function handleSignatureCompleted(array $data): void
{
// Telecharger le document signe
$signatureId = $data['id'];
$download = ScellApi::signatures()->download($signatureId, 'signed');
// ...
}
}
```

### Injection de dependances

```php
use Scell\Sdk\ScellApiClient;
use Scell\Sdk\Webhooks\WebhookVerifier;

class InvoiceService
{
public function __construct(
private readonly ScellApiClient $api
) {}

public function createInvoice(Order $order): Invoice
{
return $this->api->invoices()->builder()
->outgoing()
->facturX()
// ...
->create();
}
}
```

## Echeancier de paiement (Payment Schedule)

Associez un echeancier d'acomptes a un devis et convertissez chaque ligne en facture acompte.

```php
// Creer un devis avec echeancier integre
$quote = $api->quotes()->builder()
->buyer('12345678901234', 'Client SA', new Address('1 rue Test', '75001', 'Paris'))
->line('Prestation conseil', 1, 5000.00, 20.0)
->withPaymentSchedule([
['amount_type' => 'percent', 'amount_value' => 30, 'due_date' => '2026-06-01'],
['amount_type' => 'percent', 'amount_value' => 70, 'milestone_label' => 'Livraison finale'],
])
->create();

// Acceder aux lignes de l'echeancier
$lines = $api->quotes()->paymentSchedule()->list($quote->id);
foreach ($lines as $line) {
echo "{$line->order}. {$line->amountValue}% — {$line->status}\n";
}

// Remplacer entierement l'echeancier
$lines = $api->quotes()->paymentSchedule()->set($quote->id, [
['amount_type' => 'percent', 'amount_value' => 50, 'due_date' => '2026-07-01'],
['amount_type' => 'percent', 'amount_value' => 50, 'milestone_label' => 'Recette'],
]);

// Modifier une ligne specifique (PATCH partiel)
$lines = $api->quotes()->paymentSchedule()->patch($quote->id, [
'update' => [['id' => $lineId, 'due_date' => '2026-07-15']],
]);

// Tracker financier (montants, avancement)
$summary = $api->quotes()->paymentSchedule()->summary($quote->id);
echo "Reste a facturer : {$summary->remaining} EUR ({$summary->percentInvoiced}% deja)\n";
if ($summary->isComplete()) {
echo "Devis entierement facture.\n";
}

// Convertir une ligne en facture acompte
$depositInvoice = $api->quotes()->paymentSchedule()->convertLine($quote->id, $lineId, [
'issue_date' => '2026-06-01',
]);

// Consulter les presets preconfigures (30/70, 3x mensuel, etc.)
$presets = $api->quotes()->paymentSchedule()->presets();

// Supprimer tout l'echeancier
$api->quotes()->paymentSchedule()->delete($quote->id);
```

## Branding (Marque tenant et sub-tenant)

Personnalisez le logo, la couleur primaire et les textes des emails et PDFs emis.

```php
// Consulter la configuration de marque du tenant
$branding = $api->branding()->getTenant();
if (!$branding->isReady()) {
echo "Branding incomplet — les emails utilisent la marque Scell.io par defaut.\n";
}

// Mettre a jour le branding tenant
$branding = $api->branding()->updateTenant([
'primary_color' => '#1a73e8',
'email_footer' => 'Ma Societe SAS — SIRET 123 456 789 00010 — TVA FR12345678901',
'email_signature' => "L'equipe Ma Societe",
]);

// Uploader un logo via URL presignee S3
$upload = $api->branding()->logoUploadUrlTenant('image/png');
// PUT $upload['url'] avec le binaire du logo
// Puis confirmer le logo_url via updateTenant(['logo_url' => $upload['public_url']])

// OU : upload DIRECT multipart en un seul appel (v3.4.0)
// Formats : jpeg, png, webp, svg/svgz. Max 2 Mo.
$branding = $api->branding()->uploadLogoTenant('/path/to/logo.png');
$branding = $api->branding()->uploadLogoSubTenant($subTenantId, '/path/to/logo.svg');

// Activer/desactiver le branding e-mail (v3.4.0)
// false = les e-mails sortent avec le branding par defaut du canal
$branding = $api->branding()->updateTenant(['brand_email_enabled' => false]);
// Pied de page calcule depuis la societe (lecture seule, utilise si email_footer vide)
echo $branding->computedEmailFooter;

// Branding d'un sub-tenant
$branding = $api->branding()->getSubTenant($subTenantId);
$branding = $api->branding()->updateSubTenant($subTenantId, [
'primary_color' => '#e83e1a',
'email_footer' => 'Mon Client SARL — SIRET 98765432109876',
]);

// URL presignee logo sub-tenant
$upload = $api->branding()->logoUploadUrlSubTenant($subTenantId, 'image/jpeg');

// Apercu de l'email brande AVANT tout envoi (rendu HTML par defaut)
// Ideal a injecter dans un pour une previsualisation live.
$html = $api->branding()->previewTenant(); // string (HTML)
$subHtml = $api->branding()->previewSubTenant($subTenantId);

// Apercu avec overrides NON persistes (v3.4.0) — previsualiser un branding
// avant de l'enregistrer
$html = $api->branding()->previewTenant([
'brand_primary_color' => '#e63946',
'brand_email_footer' => 'Footer de test',
'brand_email_signature' => 'Signature de test',
'brand_logo_url' => 'https://cdn.example.com/logo.png',
]);

// Deriver les couleurs du template de facture par defaut depuis le logo e-mail (v3.4.0)
// 404 si aucun logo e-mail ; 422 si logo inaccessible ou couleurs trop neutres
$tpl = $api->invoiceTemplates()->deriveColorsFromEmailLogo();
echo "{$tpl->primaryColor} / {$tpl->accentColor}";

// Deriver la palette depuis le logo de FACTURE SANS persister (v3.5.0)
$palette = $api->invoiceTemplates()->deriveColorsFromInvoiceLogo();
echo "{$palette['primary_color']} / {$palette['accent_color']}";

// Apercu d'une facture-echantillon avec overrides de branding non persistes (v3.5.0)
$html = $api->invoiceTemplates()->preview(['primary_color' => '#0066FF']); // 'pdf' => binaire

// Groupes d'acomptes (deals multi-factures) (v3.5.0)
$groups = $api->invoices()->depositGroups(['has_no_balance' => true]);
$detail = $api->invoices()->depositGroup($groups[0]['id']); // 404 hors scope (anti-IDOR)

// Assistant de mentions legales de facture (v3.5.0)
$suggested = $api->invoiceMentions()->assistant(['vat_profile' => 'franchise_base']);
$preview = $api->invoiceMentions()->preview(['company' => ['name' => 'ACME']]);

// Apercu HTML non persiste d'un document en cours de saisie (v3.4.0)
// Rendu avec le vrai template + branding + mentions de la Company emettrice
$html = $api->documents()->preview([
'type' => 'invoice', // 'invoice' | 'credit_note' | 'quote'
'buyer' => ['name' => 'Client SA'],
'lines' => [
['description' => 'Prestation', 'quantity' => 1, 'unit_price' => 1000.00, 'tax_rate' => 20.0],
],
]);

// Envoyer une facture par email (utilise le branding tenant si isReady())
$result = $api->invoices()->sendByEmail($invoiceId, [
'email' => 'client@example.com',
'subject' => 'Votre facture FA-2026-0042',
'message' => 'Veuillez trouver ci-joint votre facture.',
]);
echo "Email envoye a {$result['recipient']} le {$result['sent_at']}\n";
```

## Gestion des erreurs

```php
use Scell\Sdk\Exceptions\ScellException;
use Scell\Sdk\Exceptions\ValidationException;
use Scell\Sdk\Exceptions\AuthenticationException;
use Scell\Sdk\Exceptions\RateLimitException;

try {
$invoice = $api->invoices()->create([...]);
} catch (ValidationException $e) {
// Erreurs de validation
foreach ($e->getErrors() as $field => $messages) {
echo "$field: " . implode(', ', $messages);
}
} catch (AuthenticationException $e) {
// API Key invalide
echo "Authentification echouee: {$e->getMessage()}";
} catch (RateLimitException $e) {
// Limite de requetes atteinte
$retryAfter = $e->getRetryAfter();
echo "Reessayez dans {$retryAfter} secondes";
} catch (ScellException $e) {
// Autre erreur API
echo "Erreur: {$e->getMessage()}";
echo "Code: {$e->getScellCode()}";
}
```

### Exceptions metier specifiques (v2.13.0+)

```php
use Scell\Sdk\Exceptions\QuoteNotEditableException;
use Scell\Sdk\Exceptions\ScheduleLineAlreadyInvoicedException;
use Scell\Sdk\Exceptions\ScheduleSumExceedsTotalException;
use Scell\Sdk\Exceptions\BuyerHasNoEmailException;
use Scell\Sdk\Exceptions\InvoiceBrandingIncompleteException;

try {
$invoice = $api->quotes()->paymentSchedule()->convertLine($quoteId, $lineId);
} catch (QuoteNotEditableException $e) {
// Devis signe/accepte : impossible de modifier l'echeancier (409)
} catch (ScheduleLineAlreadyInvoicedException $e) {
// Cette ligne a deja ete convertie en facture (422)
} catch (ScheduleSumExceedsTotalException $e) {
// La somme des lignes depasse le total TTC du devis (422)
}

try {
$result = $api->invoices()->sendByEmail($invoiceId);
} catch (BuyerHasNoEmailException $e) {
// L'acheteur n'a pas d'adresse email dans le registre (422)
} catch (InvoiceBrandingIncompleteException $e) {
// Le branding tenant est incomplet — passer force_branding: false (422)
}
```

## Types et Enums

Le SDK utilise des enums PHP 8.2+ pour les valeurs predefinies:

```php
use Scell\Sdk\Enums\Direction;
use Scell\Sdk\Enums\OutputFormat;
use Scell\Sdk\Enums\InvoiceStatus;
use Scell\Sdk\Enums\SignatureStatus;
use Scell\Sdk\Enums\AuthMethod;
use Scell\Sdk\Enums\WebhookEvent;
use Scell\Sdk\Enums\Environment;
use Scell\Sdk\Enums\RejectionCode;
use Scell\Sdk\Enums\DisputeType;

// Direction de facture
Direction::Outgoing; // Vente
Direction::Incoming; // Achat

// Format de sortie
OutputFormat::FacturX; // Factur-X PDF/A-3
OutputFormat::UBL; // UBL 2.1
OutputFormat::CII; // UN/CEFACT CII

// Methode d'authentification
AuthMethod::Email; // OTP par email
AuthMethod::Sms; // OTP par SMS
AuthMethod::Both; // Email + SMS

// Codes de rejet (factures entrantes)
RejectionCode::IncorrectAmount; // Montant incorrect
RejectionCode::Duplicate; // Facture en double
RejectionCode::UnknownOrder; // Commande inconnue
RejectionCode::IncorrectVat; // TVA incorrecte
RejectionCode::Other; // Autre

// Types de litige (factures entrantes)
DisputeType::AmountDispute; // Litige sur le montant
DisputeType::QualityDispute; // Litige sur la qualite
DisputeType::DeliveryDispute; // Litige sur la livraison
DisputeType::Other; // Autre

// Statut de facture
InvoiceStatus::Paid; // Facture payee

// Evenements webhook
WebhookEvent::InvoiceValidated;
WebhookEvent::InvoiceIncomingReceived;
WebhookEvent::InvoiceIncomingAccepted;
WebhookEvent::InvoiceIncomingPaid;
WebhookEvent::SignatureCompleted;
WebhookEvent::BalanceLow;
```

## Configuration avancee

```php
use Scell\Sdk\Config;
use Scell\Sdk\ScellApiClient;

$config = new Config(
baseUrl: 'https://api.scell.io/api/v1',
timeout: 60,
connectTimeout: 15,
retryAttempts: 5,
retryDelay: 200,
verifySsl: true,
webhookSecret: 'whsec_...',
);

$api = ScellApiClient::withApiKey('tk_live_...', $config);
```

## Tests

```bash
# Run tests
composer test

# Run tests with coverage
composer test-coverage

# Run static analysis
composer analyse

# Run all checks
composer check
```

## API Reference

### ScellClient (Bearer token)

| Resource | Description |
|----------|-------------|
| `invoices()` | Gestion des factures electroniques (+ depositGroups()/depositGroup() pour les deals multi-factures, v3.5.0) |
| `signatures()` | Gestion des signatures electroniques |
| `companies()` | Gestion des entreprises |
| `products()` | Catalogue produits/services (CRUD, scope tenant + sub_tenant) |
| `productCategories()` | Categories du catalogue produits (CRUD) |
| `balance()` | Consultation du solde |
| `webhooks()` | Gestion des webhooks |
| `branding()` | Configuration marque tenant (logo, couleur, textes emails, upload direct, apercu avec overrides) |
| `documents()` | Apercu HTML non persiste d'un document en cours de saisie |
| `invoiceTemplates()` | Templates de personnalisation factures/avoirs (CRUD, default, logo, derive-colors email + facture, preview, v3.5.0) |
| `invoiceMentions()` | Assistant de mentions legales de facture : assistant() + preview() (v3.5.0) |

### ScellApiClient (API Key)

| Resource | Description |
|----------|-------------|
| `invoices()` | Factures (builder, download, audit trail, sendByEmail, depositGroups()/depositGroup() v3.5.0) |
| `signatures()` | Signatures (builder, download, audit trail) |
| `subTenants()` | Gestion des sub-tenants (CRUD, recherche) |
| `tenantInvoices()` | Factures des sub-tenants (create, submit, update) |
| `directInvoices()` | Factures directes (create, bulk operations) |
| `incomingInvoices()` | Factures entrantes (accept, reject, markPaid) |
| `creditNotes()` | Avoirs (create, send, download) |
| `fiscal()` | Conformite fiscale ISCA (integrite, clotures, FEC) |
| `stats()` | Statistiques (overview, monthly, par sub-tenant) |
| `billing()` | Facturation plateforme (invoices, usage, top-up) |
| `quotes()` | Devis (builder, send, convert, echeancier, paymentSchedule()) |
| `products()` | Catalogue produits/services (CRUD, scope tenant + sub_tenant) |
| `productCategories()` | Categories du catalogue produits (CRUD) |
| `branding()` | Configuration marque tenant + sub-tenant (logo, couleur, emails, upload direct, apercu avec overrides) |
| `documents()` | Apercu HTML non persiste d'un document en cours de saisie |
| `invoiceTemplates()` | Templates de personnalisation factures/avoirs (CRUD, default, logo, derive-colors email + facture, preview, v3.5.0) |
| `invoiceMentions()` | Assistant de mentions legales de facture : assistant() + preview() (v3.5.0) |

### ScellTenantClient (Multi-Tenant Partner)

```php
use Scell\Sdk\ScellTenantClient;

// Create client with tenant key
$tenant = ScellTenantClient::create('tk_live_...');

// Sandbox mode
$tenant = ScellTenantClient::sandbox('tk_test_...');

// Profile management
$profile = $tenant->me();
$tenant->update(['company_name' => 'New Name']);
$balance = $tenant->balance();
$stats = $tenant->stats();
$result = $tenant->regenerateKey();

// Sub-Tenants
$subTenants = $tenant->subTenants()->list();
$sub = $tenant->subTenants()->create([...]);

// Direct Invoices (without sub-tenant scope)
$invoices = $tenant->directInvoices()->list();
$invoice = $tenant->directInvoices()->create([...]);
$tenant->directInvoices()->bulkCreate([...]);
$tenant->directInvoices()->bulkSubmit([...]);

// Direct Credit Notes
$notes = $tenant->directCreditNotes()->list();
$note = $tenant->directCreditNotes()->create([...]);

// Per sub-tenant invoices
$subInvoices = $tenant->invoices()->listForSubTenant($subId);
$invoice = $tenant->invoices()->createForSubTenant($subId, [...]);

// Incoming invoices
$incoming = $tenant->incomingInvoices()->listForSubTenant($subId);
$tenant->incomingInvoices()->accept($invoiceId);

// Fiscal compliance (ISCA)
$compliance = $tenant->fiscal()->compliance();
$integrity = $tenant->fiscal()->integrity();
$attestation = $tenant->fiscal()->attestation(2025);

// Billing
$billingInvoices = $tenant->billing()->invoices();
$usage = $tenant->billing()->usage();

// Detailed stats
$overview = $tenant->detailedStats()->overview();
```

#### ScellTenantClient API Reference

| Resource | Methods |
|----------|---------|
| Direct methods | `me()`, `update(data)`, `balance()`, `stats()`, `regenerateKey()` |
| `subTenants()` | `list()`, `create(data)`, `get(id)`, `update(id, data)`, `delete(id)`, `findByExternalId(externalId)` |
| `directInvoices()` | `list(filters)`, `create(data)`, `bulkCreate(invoices)`, `bulkSubmit(ids)`, `bulkStatus(ids)` |
| `directCreditNotes()` | `list(filters)`, `create(data)`, `get(id)`, `send(id)`, `update(id, data)`, `download(id)`, `remainingCreditable(invoiceId)` |
| `invoices()` | `listForSubTenant(subId, filters)`, `createForSubTenant(subId, data)`, `get(id)`, `update(id, data)`, `delete(id)`, `submit(id)`, `status(id)`, `remainingCreditable(id)` |
| `creditNotes()` | `listForSubTenant(subId, filters)`, `createForSubTenant(subId, data)`, `get(id)`, `update(id, data)`, `delete(id)`, `send(id)`, `download(id)`, `remainingCreditable(invoiceId)` |
| `incomingInvoices()` | `listForSubTenant(subId, filters)`, `create(subId, data)`, `get(id)`, `accept(id, data)`, `reject(id, reason, code)`, `markPaid(id, ref, data)` |
| `signatures()` (v2.7.0+) | `list(filters)`, `get(id)`, `listForSubTenant(subId, filters)`, `getForSubTenant(subId, id)` — read-only, scope tenant URL-nested. Pour les writes (create/remind/cancel/download/auditTrail), utiliser `ScellApiClient::signatures()`. |
| `fiscal()` | `compliance()`, `integrity(params)`, `integrityHistory(params)`, `integrityForDate(date)`, `closings(params)`, `performDailyClosing(data)`, `fecExport(params)`, `attestation(year)`, `attestationDownload(year)`, `entries(params)`, `killSwitchStatus()`, `killSwitchActivate(data)`, `killSwitchDeactivate(data)`, `anchors(params)`, `rules(params)`, `ruleDetail(key)`, `ruleHistory(key, params)`, `createRule(data)`, `updateRule(id, data)`, `exportRules(params)`, `replayRules(data)`, `forensicExport(params)` |
| `billing()` | `invoices(params)`, `showInvoice(id)`, `downloadInvoice(id)`, `usage(params)`, `topUp(data)`, `confirmTopUp(data)`, `transactions(params)` |
| `detailedStats()` | `overview(params)`, `monthly(params)`, `subTenantOverview(subId, params)` |

### Webhook Events

| Event | Description |
|-------|-------------|
| `invoice.created` | Facture creee |
| `invoice.validated` | Facture validee et conforme |
| `invoice.transmitted` | Facture transmise au PDP |
| `invoice.accepted` | Facture acceptee par le destinataire |
| `invoice.rejected` | Facture rejetee |
| `invoice.error` | Erreur de traitement de la facture |
| `invoice.incoming.received` | Facture entrante recue |
| `invoice.incoming.accepted` | Facture entrante acceptee |
| `invoice.incoming.rejected` | Facture entrante rejetee |
| `invoice.incoming.disputed` | Facture entrante contestee |
| `invoice.incoming.paid` | Facture entrante payee |
| `signature.created` | Signature creee |
| `signature.waiting` | Signature en attente des signataires |
| `signature.signer_completed` | Un signataire a signe |
| `signature.signed` | Tous les signataires ont signe |
| `signature.completed` | Tous les signataires ont signe |
| `signature.refused` | Signature refusee |
| `signature.expired` | Signature expiree |
| `signature.error` | Erreur de traitement de la signature |
| `balance.low` | Solde bas (seuil configurable) |

## Requirements

- PHP 8.2+
- Guzzle 7.0+
- Laravel 11/12 (optionnel)

## Contributing

Les contributions sont bienvenues. Merci de:

1. Fork le repository
2. Creer une branche (`git checkout -b feature/amazing-feature`)
3. Commit les changements (`git commit -m 'Add amazing feature'`)
4. Push sur la branche (`git push origin feature/amazing-feature`)
5. Ouvrir une Pull Request

### Code Standards

- PSR-12 pour le style de code
- PHPStan niveau 8 minimum
- Tests pour toute nouvelle fonctionnalite

## Security

Si vous decouvrez une vulnerabilite, merci d'envoyer un email a security@scell.io plutot que d'ouvrir une issue publique.

## Changelog

Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique des versions.

## License

MIT License. Voir [LICENSE](LICENSE) pour plus d'informations.

## Support

- Documentation: [docs.scell.io](https://docs.scell.io)
- Email: support@scell.io
- Issues: [GitHub Issues](https://github.com/scell-io/sdk-php/issues)