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 viacom.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 anomalieTROP_DE_LIGNESet abandon avecParseResult { rows : [], anomalies : [TROP_DE_LIGNES] }. Ne doit pas utiliser le mode DOMXSSFWorkbook(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 04signature ZIP avecxl/workbook.xmlà l'intérieur). Mismatch → anomalieFORMAT_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*_ABSENTcorrespondante si manquante. montant_du: analysé comme un entier XOF (pas de décimales) ; rejeter négatif ou zéro avecMONTANT_INVALIDE. Accepte15000,15 000,15.000(regroupement CIMA) ; rejette15.50.date_echeance: ISOyyyy-MM-ddoudd/MM/yyyyaccepté ; sinonDATE_ECHEANCE_INVALIDE. v0.0.0 n'applique pas la règleECHEANCE_DEPASSEEde 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_agenceest 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 ; accepte225→ prepend+; accepte le local à 10 chiffres (commence par01/05/07) → prepend+225; accepte le legacy à 8 chiffres → prepend+225+ leading0per CIMA mapping ; rejette sinon avecTELEPHONE_INVALIDE. Sortie+225XXXXXXXX. Chaîne de regex en ligne - pas delibphonenumberdans la v0.0.0 (architecture §10).IntraFileDeduplicator: scan des lignes par(sourceCustomerId, sourceContractId); deuxième occurrence → anomalieDOUBLON_INTRA_FICHIERsur 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_dustocké en unités entières XOFlong. 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 mockingXSSFReader; 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
ParseResultpourclean-5-rows.csvdans 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_INVALIDEetc., 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_INVALIDEunique avec la raisonio_error)