# Spec Decisions — Import CSV et reconnaissance de format

> Date: 2026-08-12
> Projet: simpl-resultat
> Statut: Draft
> Slug: import-csv-format

## Contexte

L'import CSV est la porte d'entrée du produit : sans lui, aucune autre page n'a de données. Le module a été conçu au début du projet et n'a pas été retouché depuis, alors que le reste de l'app (Bilan, rapports, gating) a été refondu plusieurs fois. Une revue d'implémentation menée le 2026-08-12 a établi que le symptôme rapporté — « l'app oublie le format, y compris les colonnes de montant positif/négatif » — n'est pas une faiblesse d'heuristique mais un trou de persistance, doublé de plusieurs voies de corruption silencieuse.

**Le bug racine.** La table `import_sources` ne possède ni `amount_mode` ni `sign_convention` (`consolidated_schema.sql:8-20`) ; ces deux colonnes n'existent que sur `import_config_templates` (`consolidated_schema.sql:147-159`). À la restauration d'une source configurée, `useImportWizard.ts:323` écrit donc `signConvention: "negative_expense"` en dur. Une source réglée en `positive_expense` — cas classique d'un relevé de carte de crédit où les dépenses sont positives — revient au défaut inverse au deuxième import, et `useImportWizard.ts:514` applique alors la négation à contresens : **toutes les dépenses deviennent des revenus, sans la moindre erreur affichée**. Le mode de montant subit le même sort, re-deviné depuis la présence de `mapping.debitAmount` (`useImportWizard.ts:321`) plutôt que lu ; comme `ColumnMappingEditor.tsx:82-94` ne nettoie pas le mapping au changement de mode, un basculement non suivi d'une re-sélection de colonne se perd ou s'inverse au rechargement.

La documentation Desjardins confirme que le cas n'est pas théorique : le signe des montants diffère selon le type de compte, et un même utilisateur a couramment un compte-chèque et une carte de crédit dans deux conventions opposées.

**Corruption silencieuse au parsing.** `useImportWizard.ts:509` calcule `amount = isNaN(credit) ? -debit : credit` — le crédit gagne toujours. Beaucoup de banques écrivent `0,00` dans la colonne inutilisée plutôt que de la laisser vide : tous les débits deviennent alors 0 $, et la ligne passe la validation puisque `isNaN(0)` est faux. Les fallbacks `?? 0` des lignes 504-512 lisent la colonne 0 — souvent la date — quand le mapping est incomplet.

**Détection aveugle aux en-têtes.** `csvAutoDetect.ts:461-470` assigne le débit et le crédit par ordre de colonne, si bien qu'un fichier `Date;Description;Crédit;Débit` est inversé intégralement. `detectSingleAmount` (l.507-530) déduit la convention au vote majoritaire de négatifs sur 20 lignes. `detectHeader` (l.214-238) retourne `!hasDate && !hasNumber`, donc un en-tête contenant « Solde 2024 » passe pour une ligne de données. Le savoir-faire manquant existe pourtant dans le même fichier : le flux d'import de titres (#245, juillet 2026) fait du matching par libellé via `normalizeHeaderCell` et `matchHeaderColumn` (l.633-666).

**Rien ne rattrape l'erreur.** La détection auto n'est jamais lancée d'office — elle est derrière un bouton (`SourceConfigPanel.tsx:69-77`), et une source neuve démarre sur un défaut plausible (`;`, `DD/MM/YYYY`, colonnes 0/1/2) qui produit un import faux plutôt qu'une erreur franche. `autoDetectConfig` ne teste jamais sa propre config sur les données. L'aperçu est un modal optionnel de 20 lignes sans totaux. L'écran de confirmation affiche délimiteur, encodage, format de date et lignes ignorées, mais **ni le mode de montant, ni la convention de signe, ni le mapping** (`ImportConfirmation.tsx:57-80`). Enfin la config est écrite en base dès l'étape doublons (`useImportWizard.ts:588-606`), donc un import annulé persiste quand même une configuration potentiellement fausse.

**Aucun filet.** `csvAutoDetect.test.ts` ne couvre que le flux holdings. `autoDetectConfig`, `detectAmountMode`, `preprocessQuotedCSV` et l'intégralité de `useImportWizard` n'ont aucun test, sur les 871 que compte le projet.

## Objectif

Faire du format d'import une donnée persistée intégralement et vérifiée, plutôt qu'un ensemble de réglages partiellement mémorisés et re-devinés à chaque passage. La reconnaissance s'appuie sur les libellés d'en-tête plutôt que sur la seule forme des données, annonce sa confiance, et le wizard impose un contrôle visuel des montants signés avant toute écriture en base.

## Scope

### IN

- Migration v17 : `amount_mode` et `sign_convention` sur `import_sources`, avec backfill préservant le comportement actuel.
- Persistance et restauration du format complet ; suppression de la valeur en dur et de la ré-inférence du mode.
- Le mode de montant devient la source de vérité du mapping : changer de mode nettoie les colonnes de l'autre mode.
- Règle débit/crédit corrigée (`crédit − débit` sur magnitudes), gestion de la colonne inutilisée à `0,00`, suppression des fallbacks `?? 0` au profit d'une erreur de ligne explicite.
- Détection par libellé d'en-tête, dictionnaire FR/EN, réutilisant les helpers du flux holdings.
- Détection auto lancée d'office sur une source non configurée.
- Score de confiance : la config détectée est rejouée sur les données et le taux de lignes lues est affiché.
- Aperçu promu en étape obligatoire du wizard, avec récapitulatif signé (sorties / entrées, totaux) et bascule de convention en un geste.
- Récapitulatif du format complet — mode, convention, mapping — sur l'écran de confirmation.
- La config n'est plus écrite en base avant confirmation de l'import.
- Signatures de banques reconnues automatiquement (Desjardins, RBC, BNC, Tangerine), sans sélecteur de banque.
- Détection de dérive : signature d'en-tête mémorisée, re-détection et présentation de l'écart au ré-import.
- `parseFrenchAmount` : parenthèses comptables `(50,00)` et signe suffixe `50,00-`.
- Sauvegarde et restauration des configurations de sources et des modèles dans l'export/import de données (format SREF).
- Corpus de fixtures CSV synthétiques + tests sur la détection, le parsing et le cycle sauvegarde/restauration du format.

### OUT (explicitement exclu)

- Toute correction rétroactive des transactions déjà importées avec un signe inversé. La voie de réparation existe déjà (`deleteImportWithTransactions` — supprimer l'import fautif et le rejouer) et aucune mutation automatique de données financières déjà catégorisées et budgétées ne sera ajoutée.
- Le troisième mode de montant « montant absolu + colonne indicateur » (`D`/`C`, `DB`/`CR`). Hors des formats des banques canadiennes personnelles visées.
- Suppression ou fusion de `import_config_templates`.
- Import d'autres formats que CSV (OFX, QFX, QIF, PDF).
- Modification du gating : l'import reste entièrement en édition Free.

## Decisions prises

| Question | Decision | Raison |
|----------|----------|--------|
| Portée du chantier | Les trois vagues de la revue en un seul chantier | Les vagues 2 et 3 dépendent du socle de persistance de la vague 1 ; les séparer imposerait de rouvrir les mêmes fichiers trois fois. |
| Transactions déjà importées à l'envers | Aucune correction rétroactive | `deleteImportWithTransactions` couvre déjà le besoin. Une inversion en masse mutilerait des données déjà catégorisées et budgétées pour un gain qu'un ré-import obtient sans risque. |
| Presets banques | Signatures reconnues automatiquement | Cohérent avec la détection par en-tête, aucune liste à maintenir dans l'UI, aucun choix de plus à faire, et un preset ne peut pas être appliqué à tort. Un fichier inconnu retombe sur le dictionnaire générique. |
| Corpus de tests | Fixtures synthétiques | Elles couvrent chaque clause du contrat, y compris les cas qu'un vrai relevé ne contient pas (débit/crédit inversés, colonne à `0,00`, en-tête numérique). Aucune donnée réelle au dépôt, démarrage immédiat. |
| Dérive de format | Re-détecter et présenter l'écart | Ni blocage sec sur un écart bénin, ni passage en silence sur un mapping périmé. L'utilisateur arbitre sur un diff lisible. |
| Statut de l'aperçu | Étape obligatoire du wizard | Décision de Max, contre la recommandation d'un aperçu conditionné à la confiance. L'étape `file-preview` existe déjà dans `ImportWizardStep` sans avoir jamais été rendue — le modal l'avait supplantée. |
| Modèles de configuration | Conservés, schéma aligné sur celui des sources | Le modèle reste un format nommé réutilisable entre plusieurs comptes d'une même banque ; la source porte le format en vigueur. Mêmes champs des deux côtés, l'asymétrie qui a causé le bug devient impossible. |
| Configurations de sources dans l'export de données | Sérialisées à l'export, restaurées à l'import | Découvert au re-ancrage de Phase 3b : l'export ne porte que catégories, fournisseurs, mots-clés et transactions, et l'import fait `DELETE FROM import_sources` (`dataExportService.ts:265` et `:362`) avant de créer une source factice « Data Import ». Restaurer une sauvegarde détruit donc toutes les configurations d'import. Troisième voie de perte du format, indépendante du bug racine et définitive ; la laisser ouverte viderait le chantier de son sens. |
| Troisième mode de montant | Hors scope, sans le fermer | `amount_mode` reste sans contrainte `CHECK` en base, donc l'ajouter plus tard ne demandera aucune migration. Un fichier de ce type sera refusé explicitement plutôt qu'importé de travers. |
| Exécution | Milestone `planned-2026-08-12-import-csv-format`, prête pour `/autopilot` | Bodies auto-suffisants, découpage en pile linéaire — la migration et le socle de détection sont des dépendances de presque tout le reste. |

## References

| Source | Pertinence |
|--------|------------|
| [CSV Format Bank Statement: UK Data Mapping Guide](https://snyp.ai/blog/csv-format-bank-statement) | Confirme la règle « une seule logique de montant par fichier » — ne jamais mélanger montant signé et colonnes débit/crédit séparées. Le bug `useImportWizard.ts:509` vient précisément d'un mélange mal arbitré. Recense les libellés sources à normaliser (Withdrawal, Deposit) vers un schéma cible Date / Description / Débit / Crédit / Montant / Solde — matière directe du dictionnaire d'en-têtes. |
| [Bank Statement CSV Format for Clean Accounting Imports](https://thebankstatementconverter.app/bank-statement-csv-format) | Établit que dans un format à deux colonnes, débit et crédit sont **tous deux positifs** — d'où la règle `crédit − débit` sur magnitudes retenue, et non la comparaison de nullité actuelle. |
| [How to Export a CSV from Desjardins (AccèsD)](https://www.flowvista.ca/guides/export-csv-desjardins) | Format Desjardins : Date, Description, Montant, Solde ; point-virgule ; virgule décimale ; en-têtes FR ou EN. Signale que certains exports de cartes de crédit n'ont pas de ligne d'en-tête et que **le signe des montants diffère selon le type de compte** — validation externe du bug racine. Base de la signature Desjardins. |
| [Import a CSV bank statement — Xero Central](https://central.xero.com/0/article/Import-a-CSV-bank-statement) | Documente la troisième convention (montant absolu + indicateur `D`/`C`) et la pratique du multiplicateur `-1` sur la colonne crédit. Sert à cadrer ce qui est laissé hors scope et à garder `amount_mode` extensible. |
| `csvAutoDetect.ts:633-737` (interne) | Le flux d'import de titres (#245) implémente déjà la détection par libellé d'en-tête avec normalisation des accents. Modèle à généraliser au flux transactions plutôt qu'à réécrire. |
