---
titre: Import multi-format (CSV + PDF) — Analyse corpus LCL et suppression des imports
statut: Spécification proposée (implémentation à venir)
date: 2026-05-14
parent: 04-import-donnees.md
---

# Import multi-format PDF + CSV, et suppression des imports

Ce document complète [04 — Import de données](04-import-donnees.md). Il synthétise une **analyse** des relevés PDF déposés sous `uploads/` (hors dépôt Git) et fixe les **exigences fonctionnelles et techniques** pour :

1. Étendre le pipeline d’import aux **fichiers PDF** (relevés bancaires), en conservant le comportement **CSV** actuel.
2. Permettre la **suppression** d’un import (métadonnées, fichiers associés, et effet sur les données transactionnelles).

La roadmap détaillée précédemment dans `docs/evolutions/001-import-reste-a-faire.md` (Messenger, profils de mapping, etc.) est **volontairement mise de côté** au profit de cette orientation **PDF** ; les éléments de cette RFC peuvent être réordonnancés ultérieurement sans impacter les exigences ci-dessous.

---

## 1. Analyse du corpus fourni

### 1.1 Fichiers concernés

Répertoire observé : `/uploads/` à la racine du projet (conforme au volume `viizia_uploads`, ignore Git).

| Fichier | Pages (extrait texte) |
|--------|------------------------|
| `LCL202501.pdf` … `LCL202512.pdf` (série 2025) | 1 ou 2 pages selon le mois ; le contenu des opérations peut s’étaler sur plusieurs pages |

Tous les fichiers examinés partagent la **même structure logique** et le **même IBAN** dans le bandeau (compte professionnel LCL / offre liée Okali « Essentiel Pro »).

### 1.2 Origine technique des PDF (métadonnées)

Les métadonnées indiquent une chaîne de production **JavaScript côté client** :

- `Producer`: `pdf-merger-js`
- `Creator`: `pdf-lib`

Interprétation pour la spec : ce ne sont **pas** des PDF « natifs banque imprimante », mais des PDF **reconstruits ou fusionnés**. En pratique ils exposent une **couche texte exploitables** par extraction (pas de dépendance obligatoire à l’OCR pour ce corpus).

### 1.3 Structure logique d’un relevé

Sur chaque page :

1. **Bandeau colonnes** : `DATE`, `TRANSACTIONS`, `DÉBIT`, `CRÉDIT` (présent même si le flux texte fusionne souvent débit/crédit en fin de ligne).
2. **Période** : `PÉRIODE DU <j> <mois> <année> AU <j> <mois> <année>`.
3. **Titulaire / adresse**.
4. **IBAN** du type `IBAN : FR15 1744 8000 …` (espaces, à **normaliser** pour comparaison avec le compte VIIZIA).
5. **Bloc « Synthèse »** : soldes d’ouverture / fermeture, totaux « Transactions entrantes » / « Transactions sortantes » (le libellé « Transations » apparaît tel quel — erreur d’origine document).
6. **Lignes d’opération** : généralement une date `JJ/MM/AAAA` en tête de ligne, suivie du libellé, suivie d’un montant avec signe explicite (`+` ou `-`) et suffixe `€`.
7. **Bloc légal / commercial** en pied de page (texte long sur « Essentiel Pro », Regafi, etc.), puis numéro de page `Page X/Y`.

Le **bandeau synthèse et le pied de page sont répétés** sur les pages suivantes lorsque le relevé fait plusieurs pages.

### 1.4 Forme des lignes d’opération (après extraction texte)

Exemples représentatifs tirés de l’extraction :

- `06/01/2025 OKALI: Carte Confort -9,60 €`
- `06/01/2025 NEOLOOP: Retour chez LCL +1 990,00 €` (les espaces fins / insécables peuvent apparaître comme espaces classiques selon l’outil)

Constats :

- Les montants sont en **euros** avec **virgule décimale**, signe **+/−** en préfixe au montant sur la même ligne.
- Les libellés peuvent être **longs** et **tronqués** avec des **ellipses** (`…`) dues à la mise en page ou à l’extraction.
- Plusieurs opérations peuvent partager la **même date** ; le **mécanisme `occurrence_index`** décrit dans la spec 04 (discrimination des lignes identiques) reste pertinent pour l’**anti-doublon** après normalisation du libellé.

### 1.5 Risques et limites de l’extraction texte seule

| Risque | Impact | Mitigation prévue (spec) |
|--------|--------|-------------------------|
| PDF **image scannée** sans couche texte | Aucune ligne exploitable | Détecter `nb caractères utiles ≈ 0` → statut **Révision manuelle** ou branche **OCR** (phase ultérieure hors MVP PDF texte) |
| Ordre et découpe des lignes | Montant collé au libellé ou coupé | Règles de parsing + tests sur corpus ; possibilité **édition manuelle** en preview |
| Répétition des en-têtes / pieds de page | Faux positifs si on parse naïvement | Filtrer lignes : conserver uniquement les lignes matchant `^\d{2}/\d{2}/\d{4}\s` après nettoyage ; exclure sections connues |
| Lignes hors opérations mais avec montant | Risque d’importer le solde comme mouvement | Exclure lignes commençant par `Solde le`, `Transactions entrantes`, etc. |
| Colonnes « DÉBIT » / « CRÉDIT » séparées visuellement | Dans l’extraction, tout peut tenir sur une seule ligne avec signe | **V1** : s’appuyer sur le **signe du montant terminal** ; évolution : analyse positionnelle ou tableau si la lib d’extraction le permet |

### 1.6 Piste de contrôle qualité (optionnelle mais utile)

Pour un relevé PDF **LCL**, les totaux « entrantes / sortantes » et les **soldes** ouverture / clôture peuvent servir de **sanity check** après parsing : somme des crédits vs « Transactions entrantes », etc. Écart → **avertissement** (blocage dur ou souple à trancher produit ; recommandation : **avertissement + import autorisé** en v1 pour ne pas bloquer sur arrondis ou libellés ambigus).

---

## 2. Spécification fonctionnelle — Parcours utilisateur multi-format

### 2.1 Principe général

- Un `ImportJob` reste rattaché à une **Organization**, un **BankAccount** et à un **utilisateur** lorsque pertinent.
- Le champ existant `ImportSourceType` distingue au minimum **`csv`** et **`pdf`** (déjà défini en code). À terme d’autres extensions pourront être ajoutées ; l’architecture doit **basculer** après détection du type de fichier vers le **bon parcours** (pas un seul écran générique forcé).

### 2.2 Étape 1 — Upload unifié (`/admin/imports/new`)

- L’utilisateur choisit **explicitement le compte bancaire cible** (`BankAccount`). **V1 : pas de détection automatique** du compte à partir du PDF (IBAN ou autre) — l’utilisateur est seul responsable du bon rattachement.
- Puis il choisit un fichier.
- Extensions autorisées étendues : **`.csv`, `.txt`** (comportement actuel) et **`.pdf`**.
- Création du job en `Pending`, stockage sous `/uploads/{organization_id}/{import_job_id}/{filename}` (inchangé).
- `sourceType` : `csv` ou `pdf` selon l’extension **et** une validation minimale (voir §3.1).
- **Routing après création du job** :
  - **CSV / TXT** → écran et flux **actuels** (mapping colonnes → runner → rapport).
  - **PDF** → **autre écran**, volontairement **plus simple** : **pas de mapping** entre colonnes du fichier et le modèle métier VIIZIA (contrairement au CSV).

### 2.3 Branche CSV (inchangée, rappel)

Conformément à [04-import-donnees.md](04-import-donnees.md) : **mapping des colonnes**, validation des rôles, exécution du runner, rapport.

### 2.4 Branche PDF — parcours cible (simple et fonctionnel)

**Philosophie** : le système **extrait** les lignes du relevé ; l’utilisateur **valide une à une** ce qui entre en base. Les métadonnées enrichissantes (bien immobilier, catégorie, plateforme de location, etc.) peuvent être **proposées ou renseignées** par ligne mais restent **facultatives** à cette étape.

1. **Upload** + **compte choisi** (§2.2).
2. **Analyse serveur** :
   - extraction de texte page par page ;
   - sélection d’un **profil de relevé** (ex. LCL « Synthèse » ou LCL « compte courant » / tableau des écritures de la période) ;
   - si **aucun profil ne convient** ou extraction vide / invalide : **arrêt** avec message **« PDF non reconnu »** (voir §3.1) ;
   - constitution d’une **liste ordonnée de lignes candidates** (date, libellé, montant / sens).
3. **Écran PDF dédié** :
   - présentation sobre des lignes détectées (tableau ou liste) ;
   - pour chaque ligne : **seuls les champs d’enrichissement** (catégorie, bien, plateforme, etc.) sont **éditables** à cette étape — pas la date, le montant ni le libellé extraits (les corrections sur le cœur de la transaction se font ensuite dans **Transactions** si besoin) ;
   - champs d’enrichissement **optionnels** ; alignés sur le **CSV / les transactions** ;
   - **pas** d’obligation de remplir ces champs pour enregistrer la ligne.
4. **Actions par ligne** (deux boutons distincts) :
   - **« Importer »** : **une seule** `Transaction` est **créée en base** immédiatement, rattachée au **compte choisi à l’upload** et traçable vers l’`ImportJob` (lien `transaction.import_job_id` à prévoir).
   - **« Ignorer »** : la ligne est **marquée ignorée** (pas de `Transaction`) ; elle reste visible dans l’historique de cet écran / du job avec l’état « ignorée » pour clarté et traçabilité.
   - Après import ou ignore : la ligne ne doit plus proposer les actions comme une ligne **pending** (désactiver les boutons ou replier la ligne) pour éviter les doubles clics sur « Importer ».
5. **Reprise de session** : l’utilisateur peut **quitter** l’écran et **revenir plus tard** sur le **même** import — les lignes déjà importées sont déjà en base ; les lignes ignorées restent marquées ; les lignes encore **pending** restent actionnables.
6. **Après coup** : compléter ou modifier bien, catégorie, plateforme, etc. depuis **Transactions** (y compris correction métier avancée si le produit l’autorise sur ces écrans).

**Anti-doublon (PDF)** : **pas de blocage** au clic « Importer ». Si les données extraites produiraient un hash déjà connu sur le compte, **l’import reste autorisé** : appliquer la même logique que la spec 04 pour **`occurrence_index`** (incrément pour distinguer les lignes identiques et hashes distincts en base). L’utilisateur peut aussi **ignorer** la ligne s’il considère qu’il s’agit d’un doublon inutile.

**État du job** : tant qu’il reste des lignes **pending**, statut cohérent avec **`Partial`** (ou équivalent « en cours de révision PDF »). Lorsque **toutes** les lignes sont soit **importées** soit **ignorées** : passer le job en **`Succeeded`** (et mettre à jour les compteurs : importées / ignorées / détectées).

**PDF non reconnu** : si aucun profil ne correspond ou si le fichier est illisible (texte vide, etc.), afficher un message utilisateur clair du type **« PDF non reconnu »** (sans tentative de saisie ligne à ligne à partir du parseur en V1).

**Note** : ce mode **remplace** pour le PDF l’ancienne description « import de masse puis rapport unique » ; le **rapport global** est mis à jour **au fil de l’eau** (compteurs du job + vue liste).

### 2.5 Évolutions possibles (hors périmètre V1 ci-dessus)

- **Détection automatique** du compte (ex. comparaison IBAN extraite du PDF vs comptes de l’organisation) — **explicitement hors V1**.
- Si un jour activée : simple **suggestion** ou **avertissement**, sans se substituer au choix utilisateur tant que la règle produit ne change pas.

### 2.6 Extension future — autres formats de fichiers

Prévoir que le même point d’entrée `/admin/imports/new` puisse router vers d’**autres** écrans ou parseurs (ex. OFX, MT940) sans fondre tous les formats dans un seul écran de mapping type CSV.

---

## 3. Spécification technique — Pipeline PDF

### 3.1 Validation « minimum viable » du fichier

- Taille max (alignée sur l’infra ; ex. quelques Mo) — à fixer identique ou supérieure au CSV.
- **PDF** :
  - fichier commençant par la signature binaire `%PDF` ;
  - pour la v1 « texte », un **seuil** de caractères extraits en dessous duquel le traitement s’arrête avec message **« PDF non reconnu »** (aligné message utilisateur §2.4) ;
  - absence de **profil de relevé** applicable après analyse → même message **« PDF non reconnu »** ; le `ImportJob` peut être passé en **`Failed`** avec ce message (ou statut dédié si on en introduit un — rester simple en V1).

### 3.2 Extraction

- **V1** : bibliothèque PHP ou service interne basé sur extraction de texte « standard » (positions possibles : `smalot/pdfparser`, wrapper autour d’un outil système, ou job interne — **décision d’implémentation hors scope de ce document**).
- Sortie normalisée par page : chaîne UTF-8 ; journalisation d’un **extrait hex** ou compteur de glyphes pour debug (comme pour l’encodage CSV dans la spec 04).

### 3.3 Profils de parsing (stratégie)

- Introduire une notion de **`StatementProfile`** (nom logique) : identifiant stable (`lcl_statement_synthèse_fr_v1`), critères de **détection** (regexp sur bandeaux, présence « PÉRIODE DU », « Synthèse », mentions LCL / Okali, etc.).
- Parser dédié par profil qui produit une liste de **transactions normalisées** (même shape interne que le runner CSV avant persistance).

### 3.4 Règles de parsing pour le profil observé « LCL relevé mensuel »

Ordre conseillé :

1. Concaténer le texte de toutes les pages en conservant `\n`.
2. Découper en lignes ; écarter lignes vides.
3. Exclure blocs avec préfixes connus (`Synthèse`, `Solde le`, `Transactions entrantes`, `IBAN`, `PÉRIODE DU`, pied de page `Essentiel Pro`, `Page \d+/\d+`, etc.).
4. Pour chaque ligne résiduelle, matcher `^(?<day>\d{2})/(?<month>\d{2})/(?<year>\d{4})\s+(?<rest>.+)$`.
5. Depuis `rest`, isoler le **dernier montant** au format européen avec signe : `(?<amount>[+-]?\s*[\d\s]+,\d{2})\s*€` ; tout ce qui précède = **libellé**.
6. Convertir montant → **centimes BIGINT** comme pour le CSV.
7. Déterminer direction : signe ou colonne crédit/débit (v1 signe obligatoire sur ce corpus).

Journaliser chaque ligne acceptée/refusée dans `import.log` (aligné philosophie spec 04).

### 3.5 OCR et PDF non textuels (hors V1 prévue ci-dessus)

- Hors périmètre **immédiat** pour ce corpus (texte disponible).
- Prévoir extension : file d’attente OCR, fichier `StatementProfile` fallback, état métier **`Reviewing`** si on reprend les statuts envisagés dans la RFC évolutions.

### 3.6 Performance et exécution

- Chaque validation de ligne PDF déclenche une **requête HTTP** courte (création d’une transaction + commit) : acceptable pour des relevés de **quelques dizaines de lignes** ; à surveiller si besoin (debounce UX, ou batch ultérieur sans changer la philosophie ligne-par-ligne).
- Les étapes **lourdes** (extraction + parsing du PDF entier) peuvent rester **synchrones** au premier chargement de l’écran PDF ou être déléguées plus tard à une file — sans changer le flux utilisateur ligne à ligne.

### 3.7 Persistance des lignes candidates (implémentation ultérieure)

Option pragmatique : stocker les lignes extraites en **JSON** dans `ImportJob.mapping` ou colonne dédiée (`preview_payload`), avec pour chaque entrée un **index stable** et un état `{ pending | saved | skipped }`. Alternative : table `import_pdf_row` — à décider selon volumétrie et besoins de reprise de session.

---

## 4. Suppression d’un import

### 4.1 Objectif utilisateur

Permettre de **retirer** une entrée d’historique d’import et les **données associées**, pour corriger une erreur (mauvais compte, mauvais fichier, doublonnage massif après coup), sans passer par une opération SQL manuelle.

### 4.2 Périmètre de suppression

À définir en **trois niveaux** (produit choisit un ou plusieurs) :

| Niveau | Effet | Quand |
|--------|--------|-------|
| **A — Soft delete job** | Le job reste visible pour audit mais marqué supprimé ; fichiers peuvent être marqués à effacer async | Obligations légales / audit |
| **B — Fichiers + métadonnées** | Suppression des fichiers sous `uploads/.../{job_id}/`, du `import.log`, et passage du job à un statut **`Deleted`** (nouveau enum) ou suppression physique de ligne | Cas courant utilisateur « j’efface cet import » |
| **C — Transactions importées** | Suppression ou soft-delete des `Transaction` liées au job | Nécessite une **association persistante** `transaction.import_job_id` (voir RFC 001) |

**Exigence** : La spec fonctionnelle **exige la possibilité** de suppression ; le **minimum** doit au moins inclure **B**. Le **niveau C** est **cohérent** avec l’expression « suppression d’import » côté produit : sans lien job → transaction, on ne peut qu’« oublier le fichier », ce qui frustrera l’utilisateur.

### 4.3 Règles métier

- **Permissions** : mêmes rôles que l’administration des imports (`ROLE_SUPERADMIN` en v1, aligné spec 04 ; évolution ROLE_ADMIN tenant).
- **Statuts autorisés** : interdire suppression si **`Running`** (ou annulation contrôlée après timeout). **`Pending`** peut être éligible après annulation sans side-effects ou suppression directe.
- **Doubles confirmation** pour **C** (texte récap du nombre de transactions supprimées).
- **Cascade** :
  - recalcul / mise à jour des **soldes** de compte si le modèle en dépend ;
  - idempotence : re-suppression = no-op.
- **Traçabilité** : log applicatif (qui, quand, quel job) même si ligne `import_job` disparaît (table audit ou fichier log pérenne global).

### 4.4 UI

Depuis la page **listage** ou **détail** d’un import :

- Action **« Supprimer cet import »** avec variante clarifiée : **« Et retirer les transactions créées par cet import »** (toggle ou deux boutons niveaux B vs C selon wording choisi).

---

## 5. Synthèse des livrables attendus pour l’implémentation ultérieure

1. **`/admin/imports/new`** : upload **CSV/TXT ou PDF**, choix **explicite** du compte ; **redirection / route** selon `sourceType`.
2. **Service PDF** : extraction + registre **`StatementProfile`** + implémentation **LCL v1**.
3. **Écran PDF dédié** : liste des lignes détectées ; **édition uniquement des enrichissements** ; boutons **« Importer »** et **« Ignorer »** par ligne ; reprise de session ; message **« PDF non reconnu »** si échec analyse.
4. **Endpoint(s)** « traiter une ligne » : création `Transaction` à la demande (**sans blocage doublon**, avec `occurrence_index` comme spec 04) ou marquage **ignoré** ; lien `import_job_id` ; compteurs du job ; statuts **Partial** / **Succeeded** selon §2.4.
5. **Suppression** : statut(s), endpoint, contrôleur, effacement disque, migration `import_job_id` sur `Transaction` si niveau **C** (voir §4).
6. **Jeux de tests** : corpus `LCL2025MM.pdf` + scénarios doublon / reprise page / abandon à préciser avec les réponses aux questions ouvertes ci-dessous et message utilisateur.

---

## 6. Références internes

- [04-import-donnees.md](04-import-donnees.md) — comportement CSV, anti-doublon, logs, sécurité.
- [docs/evolutions/001-import-reste-a-faire.md](../evolutions/001-import-reste-a-faire.md) — historique backlog (mis de côté pour prioriser PDF).

