Aller au contenu

FISCAL — Architecture cible vs existant

Note d'architecte — Cartographie honnête de ce qui existe, ce qui manque, et de la cible.

Périmètre : FISCAL en tant que pilier invoicing + tax authority compliance de l'écosystème DYNORS, dans la double ambition « résilience DGI » et « Stripe of West Africa ». Cette note est destinée à éviter de refaire ce qui existe et à concentrer l'effort sur les vrais gaps.

Source du croisement : code de dynors-internal/applications/fiscal/backend, docs/INTEGRATIONS_FACON_STRIPE.md, docs/DAW-YOB-FISC/FISC/FISCAL_SECURITY_SLY_DTOs_TRACEABILITE.md, docs/DYNORS_EVENTS_ARCHITECTURE.md, et l'audit dynors-ops/reports/fiscal/fiscal-audit-ajustement-stabilisation-2026-04-26.md.


1. Périmètre confirmé de FISCAL

FISCAL est et reste, dans l'écosystème DYNORS :

Inclus dans FISCAL Hors périmètre FISCAL
Émission de factures conformes (B2B, B2C, transactionnel) Comptabilité analytique (→ BOOKS)
Numérotation séquentielle inviolable Journaux et écritures comptables (→ BOOKS)
Calcul des taxes par juridiction Déclarations TVA périodiques (→ BOOKS)
Certification auprès des autorités fiscales (DGI, FNE, Chorus Pro, FIRS…) Encaissement PSP (→ DYNORS PAIEMENT)
Validation de conformité documentaire (NINEA, SIRET, IBAN, mentions obligatoires) Souscription / abonnement SaaS (→ dynors-billing)
Vérification publique de DFE (Document Fiscal Électronique, QR code) Notification utilisateur (→ dynors-notify)
Archivage légal WORM (Object Lock COMPLIANCE) Génération PDF brute (→ dynors-pdf, consommé par FISCAL)
Webhooks signés vers tenants externes (whsec_*) Stockage des PDFs générés (→ dynors-media)
API publique B2B style Stripe (fsk_live_*, fsk_test_*) Authentification utilisateur (→ dynors-auth)
Idempotence applicative (replay 7 jours) Autorisation runtime (→ dynors-entitlements)
Événements de cycle de vie facture (via dynors-events) Bus events lui-même (→ dynors-events module)

Cette frontière est saine et stable. Toute proposition qui élargirait FISCAL au-delà doit être justifiée par une décision explicite — et non par opportunisme.


2. État réel — ce qui existe déjà (avec preuves)

2.1 Couche API publique B2B « façon Stripe »

FISCAL est explicitement positionnée comme référence d'implémentation du patron API Key B2B pour l'écosystème DYNORS. Ce qui suit est livré et opérationnel :

Brique Implémentation Référence
Route SLY publique Path=/v1/fiscal/** + RewritePath=/v1/fiscal/(?<s>.*), /api/v1/fiscal/external/${s} dynors-platform/selebeyone/application.yml
Clés API typées par mode Format fsk_live_<32hex> / fsk_test_<32hex>, hash SHA-256 stocké, clé en clair retournée une seule fois FiscalApiKeyService
Filtre d'authentification FiscalApiKeyAuthFilter @Order(1) sur /api/v1/fiscal/external/**, lit Authorization: Bearer fsk_* ou X-Api-Key FiscalApiKeyAuthFilter
Scopes par clé CSV en base : certify, invoices:write, invoices:read, etc. Table fiscal_api_keys
Mode live/test sur la clé Champ mode ; isolation des données entre les deux modes idem
Webhooks sortants FiscalWebhookService — secret whsec_<32hex>, signature HMAC, dispatch via dynors-webhooks, auto-disable après 10 échecs consécutifs FiscalWebhookService
Reconnaissance B2B côté SLY SlyIdentificationFilter détecte Bearer fsk_* → marque B2B_API_KEY, forwarde sans valider (validation déléguée à FISCAL) dynors-platform/selebeyone
Admin CRUD clés et webhooks POST/GET/DELETE /api/v1/fiscal/admin/api-keys et …/webhooks FiscalApiKeyController, FiscalWebhookController
Idempotence Table invoice_creation_idempotency, replay HTTP 200 sur fenêtre 7 jours, purge cron via InvoiceIdempotencyPurgeService fiscal.idempotency.*
Validateurs métier NINEA, SIRET (Luhn), IBAN (mod-97) FiscalTaxIdentifiers

Conclusion sur cette couche : FISCAL est déjà au standard Stripe-like. Aucune nouvelle brique fondamentale à construire. Les évolutions à venir sont du raffinement (OpenAPI publique, dashboard développeur, SDK Node/TypeScript) et non du fondateur.

2.2 Couche certification fiscale et conformité

Brique Implémentation Statut
Client DGI Sénégal DgiClient avec modes STUB / SANDBOX / PROD configurables via fiscal.dgi.mode Code livré, recette sandbox DGI à exécuter
Vérification publique DFE PublicDfeVerificationController + DgiClient#verifyPublicDfe, route publique /public/verify (exclue de SlyTransitFilter) Livré (stub DFE-STUB-* + chemin HTTP réel selon mode)
Callbacks externes (DGI / Chorus / paiement) FiscalExternalCallbackController + FiscalCallbackSignatureFilter (HMAC) Livré (202 stubs ; durcissement IP whitelist / OAuth2 Chorus à faire)
Refus démarrage prod si secret SLY vide FiscalSlySecurityStartupValidator Livré
Numérotation séquentielle InvoiceNumberingService + InvoiceCreationPgLock (verrou pessimiste PostgreSQL) Livré
Archivage légal WORM S3 Object Lock COMPLIANCE, S3ArchiveClient, fiscal.archive.s3.*, alignement UEMOA Livré (validation juridique UEMOA à confirmer avec Ops)
Filtrage tenant @Filter(name = "tenantFilter", condition = "tenant_code = :tenantId") sur entités tenantées Livré
Multi-juridiction par stratégie TaxStrategy (SN 18%, FR 20%) + ComplianceValidator par pays + SequentialNumberingStrategy Base posée, abstraction TaxAuthorityAdapter à introduire (cf. §4)

2.3 Couche événementielle

Brique Implémentation Statut
Module dynors-events Publié 1.0.4 avec quatre adaptateurs : InMemoryEventAdapter, RabbitMQEventAdapter, RedisEventAdapter, KafkaEventAdapter Livré
Events FISCAL au catalogue fiscal.invoice.created, fiscal.invoice.finalized, fiscal.sale.certified, fiscal.sale.pending, fiscal.sale.failed, fiscal.supplier.payment.recorded Livré
Convention de nommage {domaine}.{entité}.{action_au_passé} ; topic {domaine}.{entités_pluriel} Livré
Activation FISCAL dynors.events.enabled non posé en prod → LoggingFiscalEventPublisher (logs uniquement, pas de publication réelle) À activer (config, pas du code)

2.4 Couche frontend

Quatre frontends Angular sont buildés et fonctionnels (admin, b2b-console, self-service, mobile) — voir l'audit du 2026-04-26. Les écrans existent mais plusieurs sont encore branchés sur des mocks (invoice-create.component.ts, certify.component.ts, monitoring.component.ts). Le branchement réel via SLY est le travail en cours, pas un gap d'architecture.


3. Cible architecturale — vision consolidée

FISCAL doit converger vers un produit dont le contrat extérieur est invariant et dont la complexité interne est encapsulée par juridiction.

                ┌──────────────────────────────────────────────────┐
                │  Intégrateur (RAGNAR, app DYNORS, partenaire B2B) │
                └──────────────────────┬───────────────────────────┘
                                       │ Authorization: Bearer fsk_*
                                       │ Idempotency-Key: <uuid>
                                       ▼
                ┌──────────────────────────────────────────────────┐
                │  SLY — Path=/v1/fiscal/**                         │
                │  RewritePath → /api/v1/fiscal/external/**         │
                └──────────────────────┬───────────────────────────┘
                                       │
                ┌──────────────────────▼───────────────────────────┐
                │  FISCAL Engine (invariant pays)                   │
                │  ┌────────────────────────────────────────────┐  │
                │  │  Resource model : Invoice / CreditNote /    │  │
                │  │  Customer / Product / Price / TaxRate       │  │
                │  └────────────────────────────────────────────┘  │
                │  ┌────────────────────────────────────────────┐  │
                │  │  State machine étendue :                    │  │
                │  │  DRAFT → ISSUED → PENDING_CERTIFICATION →   │  │
                │  │  CERTIFIED / CERTIFICATION_DEFERRED → PAID  │  │
                │  └────────────────────────────────────────────┘  │
                │  ┌────────────────────────────────────────────┐  │
                │  │  Tax Engine : TVA + RAS + droits de timbre  │  │
                │  │  + exonérations + taux réduits + multi-XX   │  │
                │  └────────────────────────────────────────────┘  │
                │  ┌────────────────────────────────────────────┐  │
                │  │  Certification Outbox (transactional)       │  │
                │  │  + Retry exponentiel + DLQ + Audit log      │  │
                │  └────────────────────────────────────────────┘  │
                │  ┌────────────────────────────────────────────┐  │
                │  │  TaxAuthorityAdapter (interface)            │  │
                │  ├────────────────────────────────────────────┤  │
                │  │  • DgiSenegalAdapter   (capabilities=...)   │  │
                │  │  • DgiCoteIvoireAdapter (FNE)               │  │
                │  │  • OtrTogoAdapter                            │  │
                │  │  • ChorusProAdapter (France)                │  │
                │  │  • FirsNigeriaAdapter                       │  │
                │  │  • GenericFallbackAdapter                   │  │
                │  └────────────────────────────────────────────┘  │
                └──────────────────────┬───────────────────────────┘
                                       │
                                       ▼
              ┌──────────────────┬──────────────────┬─────────────┐
              │ S3 Object Lock   │  dynors-events   │ Webhooks    │
              │ (WORM 10 ans)    │  (RabbitMQ)      │ tenants B2B │
              └──────────────────┴──────────────────┴─────────────┘

Tout ce qui est dans la moitié haute (API publique, identité produit, idempotence, archivage WORM) existe déjà. La couche centrale (resource model étendu, state machine, tax engine UEMOA complet, outbox de certification, abstraction multi-juridiction) est partiellement présente et doit être consolidée.


4. Le seul gap structurel — TaxAuthorityAdapter

C'est la pièce qui manque vraiment et qui débloque le reste. Aujourd'hui, DgiClient (Sénégal) et ChorusProIntegrationService (France) coexistent comme deux intégrations ad hoc. Pour passer à l'échelle UEMOA + CEDEAO, il faut une interface partagée.

4.1 Interface proposée

package com.dynors.fiscal.domain.port.authority;

public interface TaxAuthorityAdapter {

    /** Code juridiction ISO 3166-1 alpha-2 (SN, CI, BJ, TG, ML, BF, NE, NG, GH, FR…). */
    JurisdictionCode jurisdiction();

    /** Capabilities déclarées : ce que cette autorité supporte / exige. */
    Capabilities capabilities();

    /**
     * Certifie la facture auprès de l'autorité.
     * Idempotent : appel multiple avec la même clé doit produire le même résultat.
     * Lève {@link AuthorityUnavailableException} si l'autorité est indisponible
     * (le moteur FISCAL bascule alors en mode déféré via outbox).
     */
    CertificationResult certify(Invoice invoice, IdempotencyKey idempotencyKey)
            throws AuthorityUnavailableException;

    /** État de santé pour circuit breaker — ne propage pas d'exception. */
    HealthStatus health();

    /**
     * Construit le bundle de déclaration périodique (déclaration TVA mensuelle au Sénégal,
     * trimestrielle ailleurs). Délégué à BOOKS pour l'envoi effectif.
     */
    DeclarationBundle buildPeriodic(FiscalPeriod period, List<Invoice> issued);

    /**
     * Vérification publique d'un DFE/QR/numéro fiscal — flux non authentifié.
     * Exposé via /public/verify côté FISCAL.
     */
    PublicVerificationResult verifyPublic(String fiscalReference);
}

4.2 Capabilities — déclaration des contraintes par juridiction

public record Capabilities(
    ClearanceMode clearanceMode,         // SYNC_REQUIRED | DECLARATION_ONLY | HYBRID
    Duration maxOfflineToleranceWindow,  // null si DECLARATION_ONLY
    boolean preAllocatedNumbersSupported,
    DeclarationCadence declarationCadence,  // MONTHLY | QUARTERLY | NONE
    int legalArchivalYears,
    boolean softwareCertificationRequired,  // agrément éditeur DGI
    Set<TaxKind> taxKindsSupported,         // VAT, WITHHOLDING, STAMP_DUTY…
    Currency primaryCurrency
) {}

4.3 Cas concrets par adaptateur

Adaptateur clearanceMode declarationCadence archival Notes
DgiSenegalAdapter DECLARATION_ONLY (état 2026 — la facturation certifiée temps réel n'est pas encore exigée pour tous les contribuables) MONTHLY 10 ans DFE/QR public verify déjà géré par DgiClient ; softwareCertificationRequired=true
DgiCoteIvoireAdapter (FNE) SYNC_REQUIRED (FNE est en clearance temps réel pour la majorité des contribuables) MONTHLY 10 ans Pré-allocation de blocs si supporté ; outbox + circuit breaker actif
OtrTogoAdapter HYBRID MONTHLY 10 ans À cadrer selon état de mise en œuvre OTR
ChorusProAdapter SYNC_REQUIRED (B2G uniquement) NONE 10 ans Existant à enrober dans la nouvelle interface
FirsNigeriaAdapter SYNC_REQUIRED MONTHLY 7 ans Anglophone, NGN, exigences format spécifiques
GenericFallbackAdapter DECLARATION_ONLY NONE configurable Pour tenants en pays non encore intégré — facture émise sans clearance, réconciliation manuelle

4.4 Bénéfice immédiat

Le moteur FISCAL lit capabilities() au runtime et choisit la stratégie : si clearanceMode == SYNC_REQUIRED, tente la certification synchrone avec timeout court ; sur AuthorityUnavailableException, dépose dans l'outbox et passe la facture en CERTIFICATION_DEFERRED. Si clearanceMode == DECLARATION_ONLY, marque la facture CERTIFIED localement (numérotation interne suffisante) et ajoute à la déclaration mensuelle. La résilience devient déclarative et juridiction-spécifique, sans if (jurisdiction == "SN") parsemés dans le code.


5. Résilience DGI — patterns opérationnels (consolidation)

La plupart des briques existent déjà (DgiClient avec modes, idempotence 7 jours, archivage WORM, états ISSUED → PENDING → CERTIFIED). Ce qu'il faut formaliser et brancher :

5.1 Modes de fonctionnement

Mode Déclencheur Comportement Visible client
NOMINAL Adapter health() = UP Certification synchrone si SYNC_REQUIRED ; latence cible <2s Statut CERTIFIED immédiat
DEGRADED Timeout > 3s ou erreur 5xx ponctuelle Bascule sur outbox, retry exponentiel (5s, 30s, 2min, 10min, 1h, 6h, 24h max) Statut PENDING_CERTIFICATION, facture commerciale valide, QR provisoire
DEFERRED Outbox >N tâches OU plus ancienne tâche > 4h Circuit breaker ouvert ; pré-alloué si supporté, sinon notification tenant + déclaration manuelle planifiée Statut CERTIFICATION_DEFERRED, communication explicite au tenant
EMERGENCY Indisponibilité > 24h Procédure légale fallback (formelle selon juridiction) Statut CERTIFICATION_DEFERRED + ticket support obligatoire

5.2 Contrat API invariant

Le contrat POST /v1/fiscal/invoices/{id}/certify ne renvoie jamais d'erreur de certification. Il renvoie toujours 200 OK avec un champ status parmi CERTIFIED | PENDING | DEFERRED, accompagné si applicable du numeroOfficiel, qrCode, signatureAutorite. Le client externe gère trois cas explicites — pas une zone d'erreur opaque.

5.3 Audit obligatoire

Table tax_authority_certification_attempt(invoice_id, adapter_id, attempt_n, started_at, ended_at, status, http_code, response_payload, error_msg, idempotency_key). Conservation 10 ans minimum (alignement S3 Object Lock COMPLIANCE déjà en place). Indispensable en cas d'audit DGI ou interne.

5.4 Observabilité

Trois métriques exportées Datadog/Prometheus : fiscal_authority_health{jurisdiction=...} (0/1), fiscal_certification_outbox_depth{jurisdiction=...} (gauge), fiscal_certification_oldest_pending_seconds{jurisdiction=...} (gauge). SLO : 99% des factures certifiées dans les 5 minutes en mode nominal, 100% dans les 24 heures hors incident majeur.


6. Reste à faire — backlog priorisé

Format ticket-ready, prêt à être versé dans Jira projet FIS.

Sprint 1-2 — Fondations multi-juridiction

  1. FIS-NEW : Introduire l'interface TaxAuthorityAdapter + records Capabilities, CertificationResult, IdempotencyKey, AuthorityUnavailableException. Pas de breaking change : c'est une nouvelle interface, le code existant reste en place.
  2. FIS-NEW : Migrer DgiClientDgiSenegalAdapter implémentant TaxAuthorityAdapter avec capabilities DECLARATION_ONLY + MONTHLY. Conserver les modes STUB / SANDBOX / PROD.
  3. FIS-NEW : Migrer ChorusProIntegrationServiceChorusProAdapter.
  4. FIS-NEW : TaxAuthorityRegistry Spring qui résout l'adaptateur depuis le country du tenant ou de la facture.

Sprint 3-4 — Résilience DGI bout-en-bout

  1. FIS-NEW : Étendre la machine à états avec PENDING_CERTIFICATION et CERTIFICATION_DEFERRED (si pas déjà étendue dans le code). Ajouter les events fiscal.invoice.certified, fiscal.invoice.certification.deferred au catalogue DynorsEventTypes.
  2. FIS-NEW : Outbox de certification — table dédiée + worker @Scheduled avec retry exponentiel + circuit breaker Resilience4j sur l'adaptateur. Idempotency-Key dérivée de invoice_id.
  3. FIS-NEW : Activer dynors.events.enabled=true en prod FISCAL + provider rabbitmq. Déployer broker partagé écosystème (voir DYNORS_EVENTS_ARCHITECTURE.md §11).
  4. FIS-RECETTE : Recette DGI Sandbox — exécuter les scénarios listés dans l'audit du 2026-04-26 (preuve d'appel, réponse DFE, QR public verify, retry PENDING → CERTIFIED, journal append-only).

Sprint 5-6 — Enrichissement moteur fiscal UEMOA

  1. FIS-NEW : Étendre TaxEngine pour gérer retenue à la source (RAS sur loyers, honoraires non-résidents), droit de timbre, taux réduits sectoriels, exonérations, TVA inversée intra-UEMOA. Modèle de données : TaxRate versionné par juridiction et par catégorie, lié à Product/Price.
  2. FIS-NEW : Customer / Product / Price comme entités de premier ordre (Stripe-style) — séparées de InvoiceParty (qui reste pour les factures one-shot sans customer enregistré).
  3. FIS-NEW : Credit Notes (avoirs) — entité CreditNote, état machine, liens vers Invoice parent, événement fiscal.credit_note.issued.
  4. FIS-NEW : Multi-devisescurrency au niveau facture + tenant_default_currency ; pas de conversion automatique en première itération (responsabilité tenant).

Sprint 7+ — Adaptateurs supplémentaires & DX

  1. FIS-NEW : DgiCoteIvoireAdapter (FNE — clearance temps réel).
  2. FIS-NEW : OtrTogoAdapter.
  3. FIS-NEW : FirsNigeriaAdapter (anglophone, NGN, exigences spécifiques).
  4. FIS-NEW : OpenAPI publique /v1/fiscal/** générée par SpringDoc + dashboard développeur (developers.dynors.com/fiscal).
  5. FIS-NEW : SDK TypeScript/Node @dynors/fiscal — package npm.
  6. FIS-NEW : Hosted Invoice Page publique signée — URL https://invoices.dynors.com/in_xxxx?key=... avec aperçu, téléchargement PDF, paiement via DYNORS PAIEMENT.

Hors-périmètre confirmé (à ne PAS faire dans FISCAL)

  • Déclarations TVA agrégées mensuelles → BOOKS (FISCAL fournit le DeclarationBundle à BOOKS).
  • Encaissement PSP → DYNORS PAIEMENT.
  • Subscription / abonnement → dynors-billing.
  • Notification utilisateur → dynors-notify.

7. Décisions d'architecte (assumées)

Ces décisions sont prises au titre du lead architecte DYNORS et engagent l'écosystème jusqu'à révision explicite.

Décision 1 — TaxAuthorityAdapter est la dernière abstraction structurelle ajoutée à FISCAL. Toute juridiction future doit passer par cette interface ; aucune intégration directe d'autorité fiscale dans un service métier ne sera acceptée en revue.

Décision 2 — Le contrat API publique de FISCAL ne propage jamais l'indisponibilité d'une autorité fiscale. La résilience est interne à FISCAL. Les apps consommatrices voient status: PENDING | CERTIFIED | DEFERRED, jamais une erreur de gateway.

Décision 3 — La frontière FISCAL / BOOKS est figée. FISCAL produit des factures et certifie des documents fiscaux. BOOKS produit des écritures comptables et soumet des déclarations TVA agrégées. Pas de chevauchement.

Décision 4 — La frontière FISCAL / DYNORS PAIEMENT est figée. FISCAL n'encaisse jamais directement. Une facture peut porter un payment_intent_id produit par DYNORS PAIEMENT, mais l'orchestration PSP reste hors FISCAL.

Décision 5 — FISCAL est la référence d'implémentation API B2B style Stripe pour DYNORS. Les nouveaux services B2B (BOOKS API publique, TRACIUM authenticité, etc.) reproduisent le patron sans le réinventer (<prefix>_live_*, route SLY /v1/<app>/** avec RewritePath, filtre auth @Order(1), webhooks signés via dynors-webhooks).

Décision 6 — Pas d'ajout de localStorage/sessionStorage côté frontends FISCAL ; tokens en cookies HttpOnly Secure exclusivement, conformément à dynors-auth et dynors-security.

Décision 7 — Pas d'agrément éditeur de logiciel de facturation à demander avant que la DGI Sénégal ne le rende exigible pour la cible de tenants visés. Préparation technique présente (softwareCertificationRequired dans Capabilities) pour activation rapide quand le contexte change.


8. Références croisées

Sujet Document
Architecture FISCAL backend détaillée dynors-internal/applications/fiscal/backend/ARCHITECTURE.md (à mettre à jour avec le présent doc)
Traçabilité spec ↔ implémentation FISCAL docs/DAW-YOB-FISC/FISC/FISCAL_SECURITY_SLY_DTOs_TRACEABILITE.md
Audit FISCAL avril 2026 dynors-ops/reports/fiscal/fiscal-audit-ajustement-stabilisation-2026-04-26.md
Patron API B2B « façon Stripe » docs/INTEGRATIONS_FACON_STRIPE.md
Architecture événements DYNORS docs/DYNORS_EVENTS_ARCHITECTURE.md
Mécanisme facturation → PDF → media Mécanisme Facturation — PDF — Media
FISCAL bibliothèque projet FISCAL
Gateway SLY Gateway SelebeYone
dynors-auth (identité) dynors-auth
dynors-entitlements (autorisation runtime) dynors-entitlements
dynors-billing (souscription SaaS) dynors-billing
DYNORS PAIEMENT (encaissement PSP) DYNORS PAIEMENT

Note rédigée par le lead architecte DYNORS — 2026-05-07. Toute évolution majeure de FISCAL doit reprendre ce document comme point de départ pour éviter la divergence.