Aller au contenu

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.


ArgumentDétail
Schéma public et stableOECD CRS XML v2.0 fixé en 2019, pas de breaking change depuis. Aucun différenciateur commercial.
Cohérence avec FATCASchémas FATCA et CRS partagent la couche stf: (Standard Transmission Format). Code mutualisable.
Multi-juridictionCRS exige 1 fichier par ReceivingCountry. Le générateur paramètre cela trivialement.
Hors-ligneAucune dépendance à un service externe. Fonctionne en air-gap.
Coût0 USD marginal vs 0,30–0,80 USD/compte chez certains fournisseurs reporting.

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 tests

Élément schémaCouvertTest
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)
ReportingGrouptous
NilReportnil 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 + OECD2idem
Annulations CRS703void requires correctedMessageRefId
Echappement XML (&, <, >, ', ")XML escape special characters

InvariantErreur typéeTest
nilReport=trueaccountReports doit être videNilReportConflictnil report with accountReports rejected
messageTypeIndic ∈ {CORRECTION, VOID}correctedMessageRefId requisInconsistentCorrectioncorrection (CRS702) requires… + void (CRS703) requires…
messageTypeIndic = NEWcorrectedMessageRefId interditInconsistentCorrectionnew (CRS701) must not include…
DocRefId unique dans le messageDuplicateDocRefIdduplicate DocRefId rejected
Country codes ISO 3166-1 alpha-2 (2 lettres maj)InvalidCountryCodeinvalid country code rejected
Currency codes ISO 4217 (3 lettres maj)InvalidCurrencyCodeinvalid currency code rejected
Passive NFE CRS101controllingPersons non-videMissingControllingPersons (validate) ou exception au constructeur AccountReportpassive NFE CRS101 without controlling person rejected
IndividualHolder.resCountryCodes non-videIllegalArgumentException au constructeurimplicite
OrganisationHolder.resCountryCodes non-videidemimplicite
Address exige soit addressFix soit addressFreeidemimplicite

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(...) }).


xml-generator est un module de la pipeline TCR (cf tcr-pipeline). Il :

  1. Reçoit du classifier-engine la liste des AccountReport classés.
  2. Reçoit du balance-aggregator les soldes au 31/12 et paiements de l’exercice.
  3. Construit un CrsMessage par couple (tenant, ReceivingCountry, reportingPeriod).
  4. Appelle CrsXmlBuilder.build(msg) (ce POC) → string XML.
  5. Valide XSD (xsd OCDE officiel) en sus du valid in-builder.
  6. 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 »
AspectPOC FATCAPOC CRS
ReceivingCountrytoujours USparamétré (FR, DE, BE, …)
Catégorie filerFilerCategory (FATCA601…)non applicable
MessageTypeIndicnonobligatoire (CRS701 etc.)
Type compte holderAccountHolderType (FATCA102…)AccountHolderType (CRS101/102/103)
Substantial Owner / Controlling PersonSubstantialOwner (FATCA)ControllingPerson (CRS) avec types détaillés (CRS801…CRS816)
Nationality champnonoptionnel
Multi-résidences fiscalesnonoui (1 à N ResCountryCode)

  • Validation XSD réelle contre CrsXML_v2.0.xsd officiel — 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.

Fenêtre de terminal
cd poc-crs-generator
./gradlew test # 17/17 tests verts
./gradlew run # démo CLI 5 cas

Dé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.