承接 scell/sdk 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

scell/sdk

Composer 安装命令:

composer require scell/sdk

包简介

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

README 文档

README

Latest Version on Packagist Total Downloads License PHP Version

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

composer require scell/sdk

Configuration

Variables d'environnement

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

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

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)

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

$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

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)

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

$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

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:

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.

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)

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

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

// 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 <scell-onboarding mode="superpdp" resume-token="...">. $reset = true déconnecte avant d'émettre le jeton

Factures pour les sub-tenants

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

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

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

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

// 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.) :

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:

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

Configuration (.env)

SCELL_API_KEY=tk_live_...
SCELL_WEBHOOK_SECRET=whsec_...

Utilisation avec Facades

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

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

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.

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

// 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 <iframe srcdoc="..."> 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

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

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:

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

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

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

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 pour l'historique des versions.

License

MIT License. Voir LICENSE pour plus d'informations.

Support

统计信息

  • 总下载量: 1.17k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-01-24

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固