ADR-0001 — Introduction de TaxAuthorityAdapter comme abstraction multi-juridiction de FISCAL¶
Date : 2026-05-07 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 Sprint d'implémentation : Sprint 1 phase 1 FISCAL (M+0)
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