POC CRS generator — XML OCDE v2.0 (Kotlin)
POC :
poc-crs-generator/(~600 lignes Kotlin pur, 0 dépendance externe). Pendant CRS du POC FATCA — combinés ils couvrent toute la sortie XML de la pipeline TCR.Status : 17/17 tests passants. Démo CLI fonctionnelle (4 cas + 1 validation).
Ce POC démontre qu’un générateur XML CRS conforme OCDE peut être construit en Kotlin pur, sans dépendance externe (pas de JAXB, pas de Jackson XML). Il couvre les 3 messageTypeIndic (CRS701 New, CRS702 Correction, CRS703 Void) et les 2 types de titulaire (Individual, Organisation = Passive NFE) avec leurs Controlling Persons.
1. Pourquoi un générateur CRS interne
Section intitulée « 1. Pourquoi un générateur CRS interne »| Argument | Détail |
|---|---|
| Schéma public et stable | OECD CRS XML v2.0 fixé en 2019, pas de breaking change depuis. Aucun différenciateur commercial. |
| Cohérence avec FATCA | Schémas FATCA et CRS partagent la couche stf: (Standard Transmission Format). Code mutualisable. |
| Multi-juridiction | CRS exige 1 fichier par ReceivingCountry. Le générateur paramètre cela trivialement. |
| Hors-ligne | Aucune dépendance à un service externe. Fonctionne en air-gap. |
| Coût | 0 USD marginal vs 0,30–0,80 USD/compte chez certains fournisseurs reporting. |
2. Architecture
Section intitulée « 2. Architecture »poc-crs-generator/├── build.gradle.kts // Kotlin 2.0, JUnit 5, AssertJ — 0 dep XML├── src/main/kotlin/io/vitakyc/crs/│ ├── Model.kt // CrsMessage, MessageSpec, ReportingFI, AccountReport, IndividualHolder, OrganisationHolder, ControllingPerson, Payment, DocSpec, enums│ ├── CrsXmlBuilder.kt // sérialise CrsMessage en XML CRS v2.0 + validation pré-build│ └── Main.kt // démo 5 cas└── src/test/kotlin/io/vitakyc/crs/ └── CrsXmlBuilderTest.kt // 17 tests3. Couverture du schéma OCDE CRS v2.0
Section intitulée « 3. Couverture du schéma OCDE CRS v2.0 »| Élément schéma | Couvert | Test |
|---|---|---|
MessageSpec (TransmittingCountry, ReceivingCountry, MessageTypeIndic, MessageRefId, ReportingPeriod, Timestamp, CorrMessageRefId) | ✓ | standard individual account + correction with CorrMessageRefId |
ReportingFI (ResCountryCode, IN, Name, Address, DocSpec) | ✓ | standard individual account |
Sponsor block | ✗ (V2) | — |
ReportingGroup | ✓ | tous |
NilReport | ✓ | nil report + nil report with accountReports rejected |
AccountReport.AccountHolder.Individual (avec multi-résidence, multi-TIN) | ✓ | multiple tax residences |
AccountReport.AccountHolder.Organisation (Passive NFE) | ✓ | passive NFE with controlling person |
ControllingPerson (CRS801…CRS816) | ✓ | passive NFE with controlling person |
AcctHolderType (CRS101/CRS102/CRS103) | ✓ | idem |
Payment (CRS501 Dividends, CRS502 Interest, CRS503 Gross Proceeds, CRS504 Other) | ✓ | currency on payment different from account balance |
| Multi-currency (Account vs Payment dans devises différentes) | ✓ | idem |
DocSpec (DocTypeIndic OECD0/1/2/3, DocRefId, CorrDocRefId) | ✓ | correction with CorrMessageRefId |
Corrections CRS702 + OECD2 | ✓ | idem |
Annulations CRS703 | ✓ | void requires correctedMessageRefId |
Echappement XML (&, <, >, ', ") | ✓ | XML escape special characters |
4. Validation enforcée à la génération
Section intitulée « 4. Validation enforcée à la génération »| Invariant | Erreur typée | Test |
|---|---|---|
nilReport=true ⇒ accountReports doit être vide | NilReportConflict | nil report with accountReports rejected |
messageTypeIndic ∈ {CORRECTION, VOID} ⇒ correctedMessageRefId requis | InconsistentCorrection | correction (CRS702) requires… + void (CRS703) requires… |
messageTypeIndic = NEW ⇒ correctedMessageRefId interdit | InconsistentCorrection | new (CRS701) must not include… |
DocRefId unique dans le message | DuplicateDocRefId | duplicate DocRefId rejected |
| Country codes ISO 3166-1 alpha-2 (2 lettres maj) | InvalidCountryCode | invalid country code rejected |
| Currency codes ISO 4217 (3 lettres maj) | InvalidCurrencyCode | invalid currency code rejected |
Passive NFE CRS101 ⇒ controllingPersons non-vide | MissingControllingPersons (validate) ou exception au constructeur AccountReport | passive NFE CRS101 without controlling person rejected |
IndividualHolder.resCountryCodes non-vide | IllegalArgumentException au constructeur | implicite |
OrganisationHolder.resCountryCodes non-vide | idem | implicite |
Address exige soit addressFix soit addressFree | idem | implicite |
5. Output démo CLI (extraits)
Section intitulée « 5. Output démo CLI (extraits) »Cas 1 : Compte individuel résident fiscal FR (banque tunisienne)
<?xml version="1.0" encoding="UTF-8"?><CRS_OECD version="2.0" xmlns="urn:oecd:ties:crs:v2" xmlns:stf="urn:oecd:ties:stf:v5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <MessageSpec> <SendingCompanyIN>TN12345</SendingCompanyIN> <TransmittingCountry>TN</TransmittingCountry> <ReceivingCountry>FR</ReceivingCountry> <MessageType>CRS</MessageType> <MessageRefId>TN12345-2025-FR-001</MessageRefId> <MessageTypeIndic>CRS701</MessageTypeIndic> <ReportingPeriod>2025-12-31</ReportingPeriod> <Timestamp>2026-03-25T10:00:00Z</Timestamp> </MessageSpec> <CrsBody> <ReportingFI>...</ReportingFI> <ReportingGroup> <AccountReport> <DocSpec> <stf:DocTypeIndic>OECD1</stf:DocTypeIndic> <stf:DocRefId>TN12345-2025-acc-001</stf:DocRefId> </DocSpec> <AccountNumber>TN5901000000000123456789</AccountNumber> <AccountHolder> <ResCountryCode>FR</ResCountryCode> <TIN issuedBy="FR">1234567890123</TIN> <Name> <FirstName>Jean</FirstName> <LastName>Dupont</LastName> </Name> <Address>...</Address> <Nationality>FR</Nationality> <BirthInfo> <BirthDate>1980-05-15</BirthDate> <City>Lyon</City> <CountryCode>FR</CountryCode> </BirthInfo> </AccountHolder> <AccountBalance currCode="TND">125430.50</AccountBalance> <Payment> <Type>CRS502</Type> <PaymentAmnt currCode="TND">1250.75</PaymentAmnt> </Payment> </AccountReport> </ReportingGroup> </CrsBody></CRS_OECD>Cas 2 : Passive NFE LU avec Controlling Person DE — couvre <AcctHolderType>CRS101</AcctHolderType> et <CtrlgPersonType>CRS801</CtrlgPersonType>.
Cas 3 : Nil report — bloc <NilReport> au lieu de <AccountReport>, DocRefId suffixé -NIL.
Cas 4 : Correction CRS702 — <MessageTypeIndic>CRS702</MessageTypeIndic> + <CorrMessageRefId> + <stf:DocTypeIndic>OECD2</stf:DocTypeIndic> + <stf:CorrDocRefId>.
Cas 5 : Validation — Passive NFE CRS101 sans Controlling Person rejeté à la construction (AccountReport.init { require(...) }).
6. Comment ce POC se branche dans xml-generator
Section intitulée « 6. Comment ce POC se branche dans xml-generator »xml-generator est un module de la pipeline TCR (cf tcr-pipeline). Il :
- Reçoit du
classifier-enginela liste desAccountReportclassés. - Reçoit du
balance-aggregatorles soldes au 31/12 et paiements de l’exercice. - Construit un
CrsMessagepar couple(tenant, ReceivingCountry, reportingPeriod). - Appelle
CrsXmlBuilder.build(msg)(ce POC) → string XML. - Valide XSD (xsd OCDE officiel) en sus du valid in-builder.
- Persiste en MinIO + envoie au
submit-adapter.
Latence cible : < 50 ms pour 1 000 comptes (mesuré en local : ~12 ms pour 100 AccountReports complets).
7. Différences FATCA ↔ CRS visibles dans le code
Section intitulée « 7. Différences FATCA ↔ CRS visibles dans le code »| Aspect | POC FATCA | POC CRS |
|---|---|---|
ReceivingCountry | toujours US | paramétré (FR, DE, BE, …) |
| Catégorie filer | FilerCategory (FATCA601…) | non applicable |
MessageTypeIndic | non | obligatoire (CRS701 etc.) |
| Type compte holder | AccountHolderType (FATCA102…) | AccountHolderType (CRS101/102/103) |
| Substantial Owner / Controlling Person | SubstantialOwner (FATCA) | ControllingPerson (CRS) avec types détaillés (CRS801…CRS816) |
Nationality champ | non | optionnel |
| Multi-résidences fiscales | non | oui (1 à N ResCountryCode) |
8. Hors scope POC (à ajouter en prod)
Section intitulée « 8. Hors scope POC (à ajouter en prod) »- Validation XSD réelle contre
CrsXML_v2.0.xsdofficiel — POC valide la structure logique mais pas le schéma XML formel. À ajouter avec validateur W3C XSD avant soumission. - Sponsor model : un sponsor déclare pour plusieurs FFI sponsorées. Reporté V2.
- CRS XML v3.0 : OCDE travaille sur une v3 attendue 2027. À implémenter quand publié.
- Signature XAdES : exigence DGI Tunisie. Hors scope POC, à câbler dans le
submit-adapter. - Encryption M3M IRS IDES : pour les FFI en IGA modèle 2 qui soumettent directement à l’IRS. Hors scope POC.
- Adapters core banking : ce POC suppose les soldes et paiements déjà collectés. La collecte réelle (Temenos, Sopra, FCC, iMAL) est dans
balance-aggregator. - Schemas locaux : certaines juridictions étendent le schéma OCDE (ex. France ajoute des éléments fiscaux nationaux). Pas couvert ici.
9. Reproduire le POC
Section intitulée « 9. Reproduire le POC »cd poc-crs-generator./gradlew test # 17/17 tests verts./gradlew run # démo CLI 5 casDépendances : Kotlin 2.0.20, JVM 17, JUnit 5.11, AssertJ 3.26. Aucune dépendance externe XML (pas de JAXB, pas de Jackson) — toute la sérialisation est à la main pour garantir un output déterministe et auditable.
10. Références
Section intitulée « 10. Références »- Pipeline TCR — orchestration complète FATCA + CRS
- POC FATCA generator — pendant FATCA
- ADR-034
- Schéma : OECD CRS XML User Guide v2.0
- Loi Tunisie : Loi 2016-71 (transposition CRS, échange automatique d’informations)