Aller au contenu

POC · Normaliseur de transactions (Kotlin)

Code source : poc-transaction-normalizer/ dans le monorepo VitaKYC.

Quatrième POC technique : le normaliseur de transactions. Ingestion des deux formats dominants du monde bancaire (SWIFT MT103 legacy, ISO 20022 pacs.008 moderne) et conversion vers le modèle pivot canonique JSON de VitaKYC — la fondation du module AML Transaction Monitoring.


Les banques MENA et européennes reçoivent des transactions depuis plusieurs systèmes hétérogènes simultanément :

  • SWIFT MT103 (legacy) — encore majoritaire sur les flux internationaux jusqu’à fin 2025
  • ISO 20022 pacs.008 / pacs.009 / pain.001 (moderne) — SEPA obligatoire, SWIFT MX en migration, norme TARGET2
  • CSV batch nocturne du core banking (Temenos, Finastra, Oracle FlexCube)
  • CDC Debezium sur réplique DB en temps réel
  • Kafka streaming (fintechs modernes)

Décision d’architecture (cf. AML Transaction Monitoring §3.2) : tout est normalisé vers un unique modèle JSON pivot avant d’entrer dans le moteur de règles AML. Cela isole la logique métier des variations de formats et rend le code AML testable sur un seul type d’entrée.


Ce POC implémente P1 (MT103) et P2 (ISO 20022 pacs.008) ; P3 et P4 sont documentés mais livrés en V1.


Extrait de CanonicalTransaction :

data class CanonicalTransaction(
val txId: String,
val timestamp: String, // ISO 8601 UTC
val account: Account, // { id, iban, bic, currency, customerId }
val direction: Direction, // DEBIT | CREDIT
val type: TxType, // TRANSFER | CASH_DEPOSIT | ...
val channel: Channel, // SWIFT | SEPA | RTGS | ...
val amount: Money,
val amountEurEquivalent: Double? = null,
val counterparty: Counterparty? = null,
val purpose: Purpose? = null, // ISO 20022 purpose code + remittance
val endToEndRef: String? = null,
val sourceSystem: String, // "SWIFT_MT" | "ISO20022" | ...
val sourceFormat: String, // "swift_mt103" | "iso20022_pacs008" | ...
val rawRef: String? = null, // pointeur vers la copie archivée
)

Un CanonicalBatch contient les transactions acceptées + les transactions rejetées (RejectedTransaction avec raison précise).


Extrait le bloc 4 du message et route selon les tags :

  • :20: — référence (→ txId)
  • :23B: — code opération bancaire
  • :32A: — date valeur + devise + montant (ex 260422USD847320,00)
  • :50K: — ordering customer
  • :52A: — BIC banque émettrice
  • :59: — beneficiary (ligne 1 = /account, lignes suivantes = nom + adresse)
  • :57A: — BIC banque bénéficiaire
  • :70: — purpose / remittance
  • :71A: — details of charges

Robustesse : tolère absence des blocs 1-3 (enveloppe), supporte format européen du montant (virgule décimale), distingue account de nom dans :59:.

Parsing StAX streaming, indépendant de la version (.08 à .13). Chemin XML routé par une stack d’ancêtres :

  • GrpHdr/MsgId → référence groupe
  • CdtTrfTxInf/PmtId/TxIdtxId
  • CdtTrfTxInf/PmtId/EndToEndIdendToEndRef
  • CdtTrfTxInf/IntrBkSttlmAmt[@Ccy]amount.value + amount.currency
  • CdtTrfTxInf/IntrBkSttlmDttimestamp
  • Dbtr/Nm, Dbtr/CtryOfRes → débiteur
  • DbtrAcct/Id/IBAN → IBAN débiteur
  • DbtrAgt/FinInstnId/BICFI → BIC banque débiteur
  • Idem Cdtr* pour le créditeur
  • Purp/Cd + RmtInf/Ustrd → purpose

Inférence direction : si notre IBAN = débiteur → DEBIT, sinon CREDIT. Dérivation pays counterparty : si CtryOfRes absent, extrait des positions 5-6 du BIC (standard ISO 9362).


Sortie ./gradlew test :

NormalizerTest > MT103 · parse une transaction complète PASSED
NormalizerTest > MT103 · rejet propre quand :20: manquant PASSED
NormalizerTest > MT103 · rejet propre quand :32A: manquant PASSED
NormalizerTest > MT103 · nombre avec virgule européenne parsé correctement PASSED
NormalizerTest > pacs.008 · parse une transaction complète PASSED
NormalizerTest > pacs.008 · direction CREDIT quand notre IBAN = Cdtr PASSED
NormalizerTest > pacs.008 · dérive le pays counterparty depuis le BIC si CtryOfRes absent PASSED
NormalizerTest > toJson · produit un JSON canonique valide indenté PASSED
NormalizerTest > Batch · champs ingestedAt + batchId générés PASSED
BUILD SUCCESSFUL

{1:F01BNKATNTTXXXX0000000000}{2:I103BNKAFRPPXXXXN}{3:{108:REF-2026-042200001}}{4:
:20:TX-2026-00126
:23B:CRED
:32A:260422USD847320,00
:50K:/TN59 1000 1234 5678 9012 3456
ACME TRADING SARL
12 AV HABIB BOURGUIBA
1000 TUNIS TN
:52A:BNKATNTT
:59:/FR7630001007941234567890185
HORIZON BUSINESS LLC
DIFC GATE BUILDING 3
DUBAI AE
:57A:FRPPBICX
:70:PASS-THROUGH SUPP INV-2026-00126
:71A:SHA
-}{5:}

Extrait (74 500 € EUR, Dbtr Tunisie BNKATNTT, Cdtr UK NWBKGB2L, purpose code SUPP, message “Supplier Payment Invoice INV-2026-00123”).


{
"batchId": "6a7c3e2b-1111-4222-8333-abcdef012345",
"ingestedAt": "2026-04-22T10:00:00Z",
"sourceFormat": "swift_mt103",
"transactions": [
{
"txId": "TX-2026-00126",
"timestamp": "2026-04-22T00:00:00Z",
"account": {
"id": "ACCT-TN59-1234567",
"iban": "TN59 1000 1234 5678 9012 3456",
"currency": "USD",
"customerId": "CUST-42"
},
"direction": "DEBIT",
"type": "TRANSFER",
"channel": "SWIFT",
"amount": { "value": 847320.0, "currency": "USD" },
"counterparty": {
"name": "HORIZON BUSINESS LLC",
"accountNumber": "FR7630001007941234567890185",
"bic": "FRPPBICX"
},
"purpose": { "description": "PASS-THROUGH SUPP INV-2026-00126" },
"endToEndRef": "TX-2026-00126",
"sourceSystem": "SWIFT_MT",
"sourceFormat": "swift_mt103"
}
]
}

Après normalisation, le CanonicalBatch est publié sur Kafka (topic tx.normalized) :

SFTP drop Kafka topic
MT103 files ────────┐ tx.normalized
├──► ingest-batch-svc ──► Normalizer ──────────┐
REST POST JSON ──────┤ │
▼ ▼
ISO 20022 pacs.008 aml-svc (rules + ML)
via Kafka inbound → alerts + scoring

Latence cible : < 5 s streaming temps réel (cf. AML TxMon performance §7).


  • Un seul pipeline de traitement AML peut recevoir des formats hétérogènes sans altération du code métier.
  • La production peut adopter progressivement ISO 20022 pendant que legacy MT continue à fonctionner (pattern strangler fig).
  • Banques tunisiennes : MT103 reste le flux entrant majoritaire pour les virements internationaux hors SEPA — ce POC couvre leur cas d’usage immédiat.
  • Banques européennes / SEPA : pacs.008 est déjà obligatoire pour SEPA ; ce POC prouve qu’on est prêt.

  1. Parseur CSV VitaKYC (format trivial, tolérant).
  2. Connecteur Kafka CDC Debezium pour consommer les logs core banking.
  3. Connecteur Temenos T24 via MQ + TOCF (cf. AML TxMon §3.4).
  4. Validation XSD stricte pour ISO 20022 (schéma publié sur iso20022.org).
  5. Champs additionnels rares : InstrForDbtrAgt, ClsRmbrsmntAgt, chargeable to multiple intermediaries.
  6. Benchmark performance : cible 1 000 tx/s/tenant au MVP.
  7. Intégration au service ingest-svc en production.
  8. Conversion FX automatique vers EUR equivalent via taux BCE quotidien.
  9. Validation BIC (longueur 8/11, algorithme checksum).
  10. Dead letter queue pour les messages rejetés avec replay manuel via UI.

  • Argumentaire AO : « nous supportons dès le MVP MT103 legacy + ISO 20022 moderne — zéro migration forcée côté banque ».
  • Différenciation vs concurrents SaaS KYC modernes qui ne savent pas traiter MT103.
  • Base du module AML TxMon premium — un tenant qui ingère 1 M tx/mois = +2 500 €/mois de revenu récurrent.

POC validé le 2026-04-22 · 9 tests passants · 0 dépendance lourde (Kotlin + StAX JDK + OkHttp léger).