Aller au contenu

Histoire ING-003 : Analyse CSV/XLSX + validation des lignes + normalisation des téléphones

Module : ingestion Tranche : Sous-coupe de niveau 1 ING-001-b (architecture §11) Side : [BACKEND] Version cible : V0.0.0 Priorité : 3 Dépend de : ING-001 (fixtures + test infra) Peut être développé en même temps que : ING-002 (pas de chevauchement - cette tranche est du pur code en mémoire) Ordre de fusion : Avant ING-004 Complexité estimée : M Histoires d'utilisateurs de la DRP : Dérivé de ING-US-01..03 (noyau de validation) ; règles V1 ING-R-001, R-002, R-005, R-008, R-010, R-011 (sous-ensemble v0.0.0) Wireframe : N/A - backend uniquement


Objectif

Fournir le noyau d'analyse et de validation pur et sans base de données pour la v0.0.0 : CSV (Jackson CSV streaming, ;/, delimiter auto-détect, UTF-8), XLSX (Apache POI SAX XSSFReader), vérification des octets magiques, l'ensemble de validation des colonnes obligatoires de la v0.0.0, la détection des doublons à l'intérieur des fichiers, et la normalisation des téléphones CI à E.164. La sortie est un ParseResult { rows : List<ValidRow>, anomalies : List<Anomaly> }. Pas d'E/S en dehors du flux d'entrée. Ce noyau est consommé par la transaction de validation d'ING-004.


Portée du backend

Entités

Aucune (uniquement en mémoire). DTO de sortie : - ValidRow { rowNumber, sourceCustomerId, sourceContractId, nom, prenoms, telephoneE164, montantDu (long, XOF whole units), dateEcheance (LocalDate), email ?, dateNaissance ?, branche ?, numeroPolice ?, immatriculation ?, statutSource ?, informationsComplementaires ? }. - Anomalie { rowNumber, code (enum), field ?, rawValue ? (expurgé des informations confidentielles) }. - ParseResult { rows, anomalies, totalRowsRead }.

Codes d'anomalie (enum, sous-ensemble v0.0.0) : FORMAT_NON_SUPPORTE, TROP_DE_LIGNES, EN_TETE_INVALIDE, IDENTIFIANT_CLIENT_ABSENT, IDENTIFIANT_CONTRAT_ABSENT, NOM_ABSENT, PRENOMS_ABSENT, TELEPHONE_ABSENT, TELEPHONE_INVALIDE, MONTANT_ABSENT, MONTANT_INVALIDE, DATE_ECHEANCE_ABSENTE, DATE_ECHEANCE_INVALIDE, DOUBLON_INTRA_FICHIER.

Migrations

Aucune.

Couche de service

  • CsvIngestionParser : streaming via com.fasterxml.jackson.dataformat.csv.CsvMapper. Détection automatique des délimiteurs : renifle la première ligne non vide pour ; vs , (selon ce qui apparaît en premier) ; rupture d'égalité avec ; (convention CIMA). UTF-8 avec tolérance de nomenclature.
  • XlsxIngestionParser : XSSFReader + SharedStringsTable + SAXParserFactory (POI streaming mode). Arrêt brutal à la ligne 5 001 - en cas de dépassement, émission d'une seule anomalie TROP_DE_LIGNES et abandon avec ParseResult { rows : [], anomalies : [TROP_DE_LIGNES] }. Ne doit pas utiliser le mode DOM XSSFWorkbook (architecture §7.6 A1 + §12 A5 : heap cap 256 MB asserted by CI).
  • MagicByteVerifier : 8 premiers octets → CSV (n'importe quel fichier imprimable) ou XLSX (50 4B 03 04 signature ZIP avec xl/workbook.xml à l'intérieur). Mismatch → anomalie FORMAT_NON_SUPPORTE + abort.
  • RowValidator (v0.0.0 sous-ensemble de V1 ING-R-001..016) :
  • Obligatoire non vide : identifiant_client, identifiant_contrat, nom, prenoms, telephone, montant_du, date_echeance → anomalie *_ABSENT correspondante si manquante.
  • montant_du : analysé comme un entier XOF (pas de décimales) ; rejeter négatif ou zéro avec MONTANT_INVALIDE. Accepte 15000, 15 000, 15.000 (regroupement CIMA) ; rejette 15.50.
  • date_echeance : ISO yyyy-MM-dd ou dd/MM/yyyy accepté ; sinon DATE_ECHEANCE_INVALIDE. v0.0.0 n'applique pas la règle ECHEANCE_DEPASSEE de plus de 30 jours (différée selon l'architecture Appendix A, V1 R-007).
  • Champs optionnels analysés s'ils sont présents, jamais bloquants : date_naissance, branche, email, numero_police, immatriculation, statut_source, informations_complementaires. code_agence est ignoré dans la v0.0.0 (une seule agence implicite par défaut).
  • PhoneNormalizer (V1 ING-R-005 CI algorithm) : strip whitespace + () + - + . ; accepte le préfixe +225 → garder ; accepte 225 → prepend + ; accepte le local à 10 chiffres (commence par 01/05/07) → prepend +225 ; accepte le legacy à 8 chiffres → prepend +225 + leading 0 per CIMA mapping ; rejette sinon avec TELEPHONE_INVALIDE. Sortie +225XXXXXXXX. Chaîne de regex en ligne - pas de libphonenumber dans la v0.0.0 (architecture §10).
  • IntraFileDeduplicator : scan des lignes par (sourceCustomerId, sourceContractId) ; deuxième occurrence → anomalie DOUBLON_INTRA_FICHIER sur la dernière ligne (V1 ING-R-011).

Points d'extrémité de l'API

Aucun.

Règles de validation

Voir "Couche de service → RowValidator" ci-dessus. Contrat v0.0.0 : toute anomalie unique fait que l'ING-004 prend le chemin du rejet tout ou rien (V1 ING-R-014 + architecture §0).

Considérations multi-tenant

Ce noyau est sans état et sans locataire - il ne reçoit que les octets. Les appelants (ING-004) sont responsables du marquage des lignes résultantes avec tenant_id. Fonctionne de manière identique sous les profils SHARED et BYO DB car il ne touche pas à la base de données.

Audit & Logging

Pas d'émission d'audit directe dans cette tranche. Le résumé de l'anomalie (compte par code + 5 premiers numéros de ligne) est retourné à l'appelant (ING-004), qui émet ING_PARSE_FAILED avec ce résumé dans details. Règle PII : les valeurs brutes de téléphone/nom n'apparaissent jamais dans Anomaly.rawValue - ce champ n'est rempli que pour MONTANT_INVALIDE et les champs de date, jamais pour TELEPHONE_* ou NOM_* ou PRENOMS_*.

-

Portée du frontend

N/A - backend uniquement.

États de l'interface utilisateur (TOUS OBLIGATOIRES)

State Behavior French Copy
Loading N/A — backend only
Empty N/A — backend only
Error N/A — backend only
Offline / low network N/A — backend only
Success N/A — backend only

Comportement réactif

N/A.

Interactions

N/A.


Critères d'acceptation

AC-001: Clean CSV produces zero anomalies and exact row mapping
Given the fixture `clean-5-rows.csv` (UTF-8, `;` delimited, 5 valid rows)
When CsvIngestionParser + RowValidator process the stream
Then ParseResult.rows.size() == 5
  And ParseResult.anomalies is empty
  And every row's `telephoneE164` matches `^\+225\d{10}$`
  And every row's `montantDu` is a positive `long`.
AC-002: Tainted XLSX flags exact anomaly codes on exact rows
Given the fixture `blocking-anomalies.xlsx` containing: row 2 missing `telephone`, row 3 `telephone="abc"`, row 4 `montant_du="15.50"`, row 5 duplicate of row 2's contract
When XlsxIngestionParser + RowValidator process the stream
Then ParseResult.anomalies contains exactly: TELEPHONE_ABSENT@row=2, TELEPHONE_INVALIDE@row=3, MONTANT_INVALIDE@row=4, DOUBLON_INTRA_FICHIER@row=5
  And no anomaly rawValue contains a phone or name string.
AC-003: 5001-row XLSX aborts with TROP_DE_LIGNES and stays under 256 MB heap
Given a generated XLSX with 5 001 data rows
When XlsxIngestionParser processes it
Then ParseResult.anomalies == [TROP_DE_LIGNES] and ParseResult.rows is empty
  And the JFR snapshot in CI shows max heap < 256 MB during parse.
AC-004: Magic-byte mismatch is rejected before any row parsing
Given a file whose extension is `.csv` but whose first 8 bytes are `50 4B 03 04 ...` (ZIP/OOXML)
When the parser dispatcher inspects the magic bytes
Then ParseResult.anomalies == [FORMAT_NON_SUPPORTE] and ParseResult.rows is empty.

Règles de conformité

  • V1 ING-R-001 (formats), R-002 (limites 5 MB / 5 000 lignes), R-005 (normalisation téléphone CI), R-008 (montant entier XOF), R-010 (champs obligatoires), R-011 (déduplication intra-fichier), R-014 (tout ou rien) - tous les statuts VÉRIFIÉ selon l'architecture §0.
  • Règle critique n° 2 (argent) : montant_du stocké en unités entières XOF long. Les flottants sont interdits.
  • Pas de PII (téléphone, nom) dans les payloads d'erreur - architecture §7.4 La discipline PiiHasher s'applique à la construction des anomalies.

Normes et conventions

  • docs/standards/critical-rules.md (#2 money, #3 audit).
  • docs/standards/java-spring-guidelines.md § analyse de flux.
  • docs/standards/database-guidelines.md § convention XOF integer minor-unit.

Exigences de test

Tests unitaires

  • CsvIngestionParserTest : détection des délimiteurs (; vs ,), BOM UTF-8, citations malformées → EN_TETE_INVALIDE.
  • XlsxIngestionParserTest : SAX mode confirmed by mocking XSSFReader ; row 5 001 cutoff.
  • MagicByteVerifierTest : CSV / XLSX / mismatch / empty file.
  • RowValidatorTest : matrice de chaque code d'anomalie avec des entrées basées sur des tableaux.
  • PhoneNormalizerTest : chaque variante de format acceptée + chaque cas de rejet.
  • IntraFileDeduplicatorTest : 0 / 1 / plusieurs doublons.

Tests d'intégration

  • XlsxLargeFileMemoryIT - génère 5 XLSX de 001 lignes in-temp, affirme AC-003 heap bound via JFR.

Ce que l'AQ validera

  • Lancez ./gradlew :backend:test --tests "*ingestion.parse*" "*ingestion.validate*" - vert.
  • Inspecter un exemple de ParseResult pour clean-5-rows.csv dans le debugger de l'IDE.

Hors du champ d'application

  • Écritures dans la base de données (ingestion_import, ingestion_import_row, customer, contract) - ING-004.
  • Déduplication SHA-256 contre les importations précédentes - ING-004.
  • Émission d'audit de ING_PARSE_FAILED / ING_COMMITTED - ING-004.
  • Validation obligatoire de la date de naissance (DATE_NAISSANCE_ABSENTE) - V0.0.2 ING-003.
  • Règle de retard ECHEANCE_DEPASSEE (>30 jours de retard) - V0.0.2.
  • résolution du code_agence - V0.0.2.
  • Interface utilisateur / atelier - V0.0.3+.
  • Jeu de validation V1 pour tous les champs requis - V0.0.3.

Définition de Fait

  • [Tous les endpoints du backend sont implémentés et renvoient des réponses correctes (N/A - pas d'endpoints)
  • [Toutes les pages du frontend s'affichent correctement à 360 / 768 / 1024 px (N/A - backend uniquement)
  • [Les 5 états de l'interface utilisateur sont implémentés (N/A - backend uniquement)
  • [ ] La micro-copie française correspond exactement à la spécification de conception (les noms d'enum d'anomalie SONT la copie française - TELEPHONE_INVALIDE etc., utilisés mot pour mot dans le JSON du sidecar que ING-004 écrira)
  • [Tous les critères d'acceptation sont passés manuellement
  • [Tests unitaires et d'intégration écrits et réussis
  • [L'isolation du locataire est vérifiée (le noyau est agnostique au locataire ; la responsabilité du consommateur est documentée dans la javadoc)
  • [ ] [Histoires de clients] payload + Slow-3G + resumability (N/A - backend uniquement)
  • [Les entrées du journal d'audit sont présentes pour chaque événement requis (N/A - pas d'émissions d'audit ici ; le consommateur ING-004 émet)
  • [Aucune défaillance silencieuse sur les erreurs de réseau (l'analyseur reçoit un InputStream ; les erreurs d'E/S se propagent à l'appelant sous la forme d'une anomalie EN_TETE_INVALIDE unique avec la raison io_error)