ADR-0001 — Introduction de TaxAuthorityAdapter comme abstraction multi-juridiction de FISCAL¶
Date : 2026-05-07 (révisée 2026-07-13)
Catégorie : A (décidée en autonomie par le lead architecte dans le cadre de la roadmap FISCAL validée)
Décideur : Lead architecte DYNORS
Statut : Acceptée — implémentation Phase 1 en cours
Sprint d'implémentation : port et migration des consommateurs métier livrés ; Sénégal exploité en DECLARATION_ONLY faute d'API DGI officielle ; résilience outbox encore ouverte (voir §État réel 2026-07-13).
État réel 2026-07-13 (vérifié dans le code)¶
Le port et les deux premiers adapters sont désormais livrés. Preuves :
TaxAuthorityAdapter,TaxAuthorityRegistry, les exceptions typées et leurs tests existent dansdomain/port/authority.DgiSenegalAdapterémet localement enDECLARATION_ONLY, sans appel réseau ni faux DFE ;ChorusProAdapterencapsule le client historique France.PublicDfeVerificationControlleretSaleCertificationServicerésolvent désormais l'autorité viaTaxAuthorityRegistry.- Les imports directs de
DgiClientetChorusProIntegrationServicene subsistent que dans leurs adapters d'infrastructure respectifs. - Validation locale du 2026-07-13 : 194 tests backend, 0 échec, dont tests du registry, des adapters et de la certification POS.
Le bloqueur de création du port est levé. La Gate Phase 1 reste toutefois ouverte tant que la recette locale DECLARATION_ONLY et l'outbox de certification ne sont pas livrées :
| Item | Action | Owner | Échéance proposée |
|---|---|---|---|
| FISC-PORT-001 | Créer le port, le registry, les records/exceptions et les tests unitaires | Équipe FISCAL | Livré |
| FISC-PORT-002 | Encapsuler DGI/Chorus et migrer les consommateurs métier derrière le registry | Équipe FISCAL | Livré |
| FISC-PORT-003 | Tests automatiques zéro régression + recette Sénégal sans API DGI | Équipe FISCAL | Tests verts ; recette déployée ouverte |
| FISC-PORT-004 | Outbox + état CERTIFICATION_DEFERRED + circuit breaker + events fiscal.invoice.certified.* |
Équipe FISCAL | 2026-09-30 |
| FISC-PORT-005 | DgiCoteIvoireAdapter (FNE clearance temps réel) |
Équipe FISCAL | 2026-11-15 |
| FISC-PORT-006 | FirsNigeriaAdapter (NGN, en-NG) |
Équipe FISCAL | 2026-12-31 |
Gate de revue : toute nouvelle intégration d'autorité fiscale ou toute extension directe de DgiClient* hors adapter reste refusée en code review. Le futur passage du Sénégal à une API DGI exigera une preuve officielle, un ADR de bascule et une recette sandbox. La Côte d'Ivoire et le Nigeria restent conditionnés à leurs propres contrats officiels et au mécanisme outbox.
Prochaine preuve attendue : rapport de recette démontrant l'émission normale Sénégal sans numéro DFE/QR simulé, la numérotation interne, l'archivage et les événements BOOKS. Une recette DGI réseau est non applicable tant qu'aucune API officielle n'est disponible.
Contexte¶
FISCAL doit aujourd'hui parler à plusieurs autorités fiscales pour certifier ou enregistrer des factures et documents fiscaux. À ce jour, deux intégrations coexistent comme services ad hoc côte à côte :
DgiClient— Sénégal, avec trois modes (STUB,SANDBOX,PROD), portant la vérification DFE/QR publique et la certification en modeDECLARATION_ONLY.ChorusProIntegrationService— France, dépôt de factures B2G dans le système Chorus Pro.
La roadmap 12 mois (voir fiscal-roadmap-12-mois.md) vise l'ajout immédiat de deux nouvelles juridictions : Côte d'Ivoire (FNE — clearance temps réel) et Nigeria (FIRS — clearance temps réel anglophone NGN). Sans une abstraction commune, chaque nouvelle juridiction est une intégration ad hoc supplémentaire, ce qui crée :
- Duplication de la logique de résilience (timeout, retry, circuit breaker, idempotence) dans chaque service.
- Difficulté à appliquer une politique de retry homogène (outbox, retry exponentiel, DLQ).
- Code métier FISCAL qui doit savoir quelle autorité appeler en fonction du pays, avec des branchements
if (country == "SN") ...qui ne scalent pas. - Impossibilité de déclarer au runtime les capabilities d'une autorité (sync vs async, fenêtre offline, pré-allocation, déclaration uniquement).
L'abstraction est nécessaire maintenant parce que la décision stratégique de viser SN + CI + NG en première vague est actée.
Décision¶
Introduire l'interface TaxAuthorityAdapter dans com.dynors.fiscal.domain.port.authority comme abstraction obligatoire pour toute intégration d'autorité fiscale, présente ou future. Migrer DgiClient → DgiSenegalAdapter et ChorusProIntegrationService → ChorusProAdapter derrière cette interface. Résoudre les adapters au runtime via un TaxAuthorityRegistry Spring indexé par JurisdictionCode.
Interface¶
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.
* Lève 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;
/** Certification d'un avoir (Credit Note). Délègue par défaut à certify. */
default CertificationResult certifyCreditNote(CreditNote cn, IdempotencyKey key)
throws AuthorityUnavailableException {
return certify(cn.toInvoiceLike(), key);
}
/** État de santé pour circuit breaker — ne propage pas d'exception. */
HealthStatus health();
/** Construit le bundle de déclaration périodique (consommé par BOOKS). */
DeclarationBundle buildPeriodic(FiscalPeriod period, List<Invoice> issued);
/** Vérification publique d'un DFE/QR/numéro fiscal — flux non authentifié. */
PublicVerificationResult verifyPublic(String fiscalReference);
}
Capabilities¶
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,
Set<TaxKind> taxKindsSupported,
Currency primaryCurrency,
Locale primaryLocale
) {}
Profils initiaux¶
| Adapter | clearanceMode |
declarationCadence |
archival |
Devise / Locale |
|---|---|---|---|---|
DgiSenegalAdapter |
DECLARATION_ONLY |
MONTHLY |
10 ans | XOF / fr-SN |
ChorusProAdapter |
SYNC_REQUIRED (B2G) |
NONE |
10 ans | EUR / fr-FR |
DgiCoteIvoireAdapter (à venir Sprint 10) |
SYNC_REQUIRED |
MONTHLY |
10 ans | XOF / fr-CI |
FirsNigeriaAdapter (à venir Sprint 12) |
SYNC_REQUIRED |
MONTHLY |
7 ans | NGN / en-NG |
GenericFallbackAdapter |
DECLARATION_ONLY |
NONE |
configurable | configurable |
Conséquences¶
Bénéfices¶
- Le moteur FISCAL devient agnostique de la juridiction : il lit les
Capabilitiesau runtime et applique la politique de résilience appropriée (synchrone strict / async outbox / fallback offline). - L'ajout d'un nouveau pays se réduit à implémenter une nouvelle classe
XxxAdapter+ une migration desTaxRuledu pays dans le registry. Aucune modification du code métier FISCAL n'est requise. - Toute la mécanique de retry (outbox, circuit breaker, DLQ) est mutualisée dans le moteur, pas dupliquée par adapter.
- Les tests d'intégration deviennent plus robustes : on peut tester le moteur avec un
FakeAuthorityAdaptersans dépendre des sandboxes externes.
Coûts / contraintes induites¶
- Sprint 1 dédié au refactoring : zéro nouvelle fonctionnalité livrée, mais l'investissement est non-négociable pour les phases suivantes.
- Backward compatibility : les services existants qui injectent directement
DgiClientouChorusProIntegrationServicedoivent passer par le registry. La compilation forcera la mise à jour ; les tests E2E doivent prouver zéro régression sur le périmètre Sénégal et France actuel. - Discipline de revue : tout futur PR qui ajoute un client HTTP vers une autorité fiscale hors
TaxAuthorityAdapterdoit être refusé en revue (règle Cursor.cursor/rules/fiscal-transformation-2026-priorite.mdc§2.1).
Composants impactés¶
dynors-internal/applications/fiscal/backend— nouveaux packages, migration des deux services existants.- Tests d'intégration FISCAL — à compléter avec un
FakeAuthorityAdapterréutilisable. - Documentation : roadmap, architecture cible, cette ADR, mise à jour du README backend FISCAL.
Alternatives considérées¶
Alternative A — Statu quo (continuer en ad hoc par juridiction). Écartée : ne scale pas au-delà de 2-3 pays, duplication de la résilience, code métier pollué.
Alternative B — Externaliser la certification à un service tiers (Sovos, Fonoa, équivalents). Écartée : (1) coût récurrent par facture significativement plus élevé que notre cible « Stripe of West Africa », (2) dépendance critique sur un fournisseur étranger pour notre cœur produit, (3) perte de l'avantage compétitif d'agrément éditeur direct par pays UEMOA.
Alternative C — Adapter pattern mais sans Capabilities (juste signature certify). Écartée : oblige le moteur FISCAL à connaître les contraintes de chaque autorité hors interface — perd l'avantage clé de pouvoir décider la politique de résilience au runtime.
Plan d'exécution¶
- Sprint 1 (M+0 → M+0,5) : créer l'interface, les records et exceptions, le
TaxAuthorityRegistry, les tests unitaires. Pas de migration encore. - Sprint 2 (M+0,5 → M+1) : migrer
DgiClient→DgiSenegalAdapter, migrerChorusProIntegrationService→ChorusProAdapter. AdapterFiscalInvoiceServicepour appeler le registry. Tests E2E zéro régression. - Sprint 3 (M+1 → M+1,5) : étendre
InvoiceStatusavecPENDING_CERTIFICATION,CERTIFIED,CERTIFICATION_DEFERRED. Introduire la table outbox + worker + circuit breaker. Eventsfiscal.invoice.certified,fiscal.invoice.certification.deferredau catalogue. - Sprints 10-13 (M+5 → M+8) : implémenter
DgiCoteIvoireAdapteretFirsNigeriaAdaptersans modification du moteur FISCAL.
Suivi¶
Critère de succès : à la fin du sprint 2, aucune ligne de code métier FISCAL ne référence directement DgiClient ou ChorusProIntegrationService ; tout passe par TaxAuthorityRegistry.get(JurisdictionCode).
Critère de mesure long terme : à l'introduction de la 3ème juridiction (Côte d'Ivoire ou Nigeria), zéro modification de code dans FiscalInvoiceService, InvoiceNumberingService, ComplianceValidationService. Si modification nécessaire, c'est un signal de défaut de l'abstraction et l'ADR doit être révisée.
Références¶
- Roadmap : fiscal-roadmap-12-mois.md
- Architecture cible : fiscal-architecture-cible-vs-existant.md §4
- Règle Cursor associée :
.cursor/rules/fiscal-transformation-2026-priorite.mdc