Initial import: LifeTrack v1 (santé, vape, finances)

Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-14 10:48:57 +02:00
co-authored by Claude Opus 5
commit 93f0689c1e
273 changed files with 80746 additions and 0 deletions
+590
View File
@@ -0,0 +1,590 @@
# Module FINANCE — Recherche sur les sources de données et l'import
> Document de recherche pour LifeTrack (module Finance). Rédigé le 2026-08-13.
> Public : agents d'implémentation. Ce document est autoportant — **aucune recherche complémentaire n'est prévue**.
> Convention : prose en français, identifiants de code en anglais.
---
## Sommaire
1. [Vue d'ensemble et enseignements clés](#1-vue-densemble-et-enseignements-clés)
2. [Formats d'export des banques françaises (fichiers)](#2-formats-dexport-des-banques-françaises-fichiers)
3. [Export d'activité PayPal](#3-export-dactivité-paypal)
4. [Le format OFX en France (et QIF)](#4-le-format-ofx-en-france-et-qif)
5. [Agrégation PSD2 : état des lieux 2025/2026](#5-agrégation-psd2--état-des-lieux-20252026)
6. [Stratégies de déduplication](#6-stratégies-de-déduplication)
7. [Recommandations d'implémentation — v1 (import fichiers)](#7-recommandations-dimplémentation--v1-import-fichiers)
8. [Recommandations d'implémentation — v2 (synchronisation automatique)](#8-recommandations-dimplémentation--v2-synchronisation-automatique)
9. [Sources](#9-sources)
---
## 1. Vue d'ensemble et enseignements clés
### 1.1 Constats structurants
1. **Le "CSV bancaire français" n'existe pas** : chaque banque a son propre dialecte. Points communs majoritaires : séparateur `;`, virgule décimale, dates `JJ/MM/AAAA`, encodage `ISO-8859-1`/`ISO-8859-15` (Windows-1252 en pratique). Exceptions notables : BoursoBank (dates `AAAA-MM-JJ`), Revolut et N26 (séparateur `,`, point décimal, UTF-8).
2. **Beaucoup de fichiers ont un préambule** (lignes d'en-tête métier avant la ligne d'en-têtes de colonnes) : Crédit Agricole (nombre de lignes **variable**), Société Générale (1 ligne), La Banque Postale (~8 lignes), BNP (1 ligne de solde). Le mapper CSV générique doit donc savoir **sauter N lignes** et/ou **détecter la ligne d'en-tête**.
3. **Les profondeurs d'historique téléchargeable sont faibles** chez les banques traditionnelles (30 à 90 jours typiquement, 6 mois chez SG) : l'utilisateur devra importer régulièrement, d'où l'importance capitale de la **déduplication** et des **fenêtres de recouvrement** (mieux vaut réimporter large que de créer des trous).
4. **OFX est disponible mais imparfait en France** : versions SGML 1.x, encodages legacy, et surtout des `FITID` non fiables chez certaines banques (cas documenté LCL : FITID = type+date+montant ⇒ collisions). Le FITID est un bon signal de dédup, **jamais une garantie**.
5. **Bouleversement PSD2 2025** : GoCardless Bank Account Data (ex-Nordigen), la solution gratuite historique des self-hosters (utilisée par Firefly III et Actual Budget), **n'accepte plus de nouveaux comptes depuis juillet 2025** et est en cours d'extinction. La relève gratuite pour un particulier est **Enable Banking** (mode "restricted" gratuit sur ses propres comptes), désormais supportée par Firefly III et Actual Budget.
6. Firefly III et Actual Budget fournissent des modèles éprouvés de déduplication : identifiant externe prioritaire (`imported_id` / "external identifier"), puis hash de contenu, puis rapprochement flou (montant identique + date proche + libellé similaire). Nous reprenons cette hiérarchie.
### 1.2 Décision recommandée (résumé)
- **v1** : import par fichiers uniquement. Un **mapper CSV générique** (encodage, séparateur, préambule, mapping de colonnes, format de date, virgule décimale, colonnes débit/crédit vs montant signé) + **presets par banque** livrés en JSON + **parseur OFX** (lib Python `ofxparse`) + **preset PayPal**. Déduplication à 3 niveaux (voir §6.4). Saisie manuelle et règles de catégorisation.
- **v2** : connecteur **Enable Banking** (PSD2 officiel, gratuit en mode restreint pour ses propres comptes, couvre les grandes banques françaises) branché sur le **même pipeline de staging/dédup** que les fichiers. Connecteur GoCardless conservé en option pour les détenteurs de comptes historiques. `woob` documenté comme adaptateur optionnel "fragile", non prioritaire.
---
## 2. Formats d'export des banques françaises (fichiers)
### 2.0 Tableau de synthèse
| Banque | Formats | Séparateur CSV | Encodage | Format date | Décimale | Montant | Préambule | Historique |
|---|---|---|---|---|---|---|---|---|
| BoursoBank | CSV, OFX, QIF | `;` | UTF-8 | `AAAA-MM-JJ` | virgule (sauf col. solde : point !) | signé, 1 colonne | non | plusieurs années, période libre |
| Crédit Agricole | CSV, Excel, OFX, QIF (selon caisse) | `;` | ISO-8859-15 | `JJ/MM/AAAA` | virgule | 2 colonnes Débit/Crédit | oui, **variable** (fin = ligne `Date;`) | ~3090 j selon caisse |
| BNP Paribas | CSV, OFX, QIF, PDF | `;` | ISO-8859-1 | `JJ/MM/AAAA` | virgule (+ espace milliers) | signé, 1 colonne | 1 ligne (solde, HTML-échappée 2×) | ~90 j |
| Société Générale | CSV, QIF (revenu en 2020) | `;` | ISO-8859-1/15, CRLF | `JJ/MM/AAAA` | virgule | signé, 1 colonne | 1 ligne (`="compte"`;début;fin;) | ~6 mois |
| La Banque Postale | CSV, TSV, OFX | `;` | ISO-8859-15 | `JJ/MM/AAAA` | virgule | signé, 1 colonne | ~8 lignes (n° compte, soldes) | ~90 j |
| Caisse d'Épargne | CSV (OFX selon interfaces) | `;` | ISO-8859-1 | `JJ/MM/AAAA` | virgule | 2 colonnes Débit/Crédit (crédit préfixé `+`) | non (nouveau format) | limité (pas de solde dans le fichier) |
| Fortuneo | CSV, XLS, QIF, OFX | `;` | ISO-8859-1 / Windows-1252 (à détecter) | `JJ/MM/AAAA` | virgule | 2 colonnes Débit/Crédit | non | jusqu'à ~10 ans |
| Revolut | CSV, Excel, PDF | `,` | UTF-8 | `AAAA-MM-JJ HH:MM:SS` | point | signé + colonne `Fee` | non | historique complet |
| N26 | CSV | `,` | UTF-8 | `AAAA-MM-JJ` | point | signé, 1 colonne | non | historique complet |
| PayPal | CSV, TAB (QIF USD only) | `,` | UTF-8 | selon locale (`JJ/MM/AAAA` en FR) | selon locale (virgule en FR) | Gross/Fee/Net | non | 7 ans (tranches de 12 mois) |
Détails et pièges banque par banque ci-dessous. Les en-têtes cités sont **exacts** (issus de fichiers réels analysés dans des projets open-source, notamment `mincong-h/finance-toolkit`, et de documentations d'import OpenFlyers).
### 2.1 BoursoBank (ex-Boursorama Banque)
- **Accès** : espace client → historique du compte → sélection de période libre → « Exporter » ; formats **CSV, OFX, QIF** proposés. Tous les comptes sélectionnés sont regroupés dans **un seul fichier**.
- **Nom de fichier** : `export-operations-{JJ}-{MM}-{AAAA}_{hh}-{mm}-{ss}.csv` (date de génération).
- **Encodage** : UTF-8. **Séparateur** : `;`. Champs texte entre guillemets doubles.
- **En-tête exact (1re ligne)** :
```
dateOp;dateVal;label;category;categoryParent;amount;comment;accountNum;accountLabel;accountbalance
```
- **Exemple de ligne réelle** :
```
2021-08-17;2021-08-17;"Prime Parrainage";"Virements reçus";"Virements reçus";130,00;;001234;"BOURSORAMA BANQUE";226.68
```
- **Particularités / pièges** :
- Dates en **`AAAA-MM-JJ`** (seule banque française classique dans ce cas).
- `amount` utilise la **virgule** décimale, mais `accountbalance` utilise le **point** décimal dans le même fichier. Ne jamais parser les deux colonnes avec la même routine.
- La colonne `accountbalance` est un solde recalculé glissant, peu fiable : **l'ignorer** pour la comptabilité ; le solde de compte doit être saisi/rapproché séparément.
- `category`/`categoryParent` : catégorisation maison BoursoBank — utile comme **suggestion** de catégorie initiale au mapping.
- `accountNum` répété sur chaque ligne ⇒ permet de **router les lignes vers plusieurs comptes** LifeTrack depuis un fichier unique (le preset doit gérer un fichier multi-comptes).
- Le PEA n'est pas exportable.
### 2.2 Crédit Agricole
- **Accès** : espace client (par **caisse régionale** — les interfaces varient) → « Vos opérations » → « Télécharger vos opérations ». Formats selon caisse : **CSV, Excel (xls), OFX, QIF, TXT**. Historique en ligne court (souvent 30 à 90 jours).
- **Structure CSV** (modèle documenté par OpenFlyers) :
- **Encodage** : ISO-8859-15. **Séparateur** : `;`.
- **Préambule de longueur variable** (infos compte, période, solde). La fin du préambule est repérable par la ligne commençant par `Date;`.
- **En-tête** : `Date;Date valeur;Libellé;Débit Euros;Crédit Euros;`
- Dates `JJ/MM/AAAA`, virgule décimale, montants ventilés en **deux colonnes** Débit/Crédit.
- **Les libellés peuvent contenir des retours à la ligne** (champ multi-lignes entre guillemets) — le parseur CSV doit être configuré pour les champs quotés multi-lignes (le module Python `csv` le gère nativement si on ne pré-découpe pas par lignes).
- Un **pied de page** peut suivre les données (séparé par des lignes vides) — arrêter le parsing à la première ligne dont la 1re colonne n'est pas une date valide.
- **Implémentation preset** : `skip_until_header_startswith: "Date;"` + `stop_on_non_date_row: true`.
### 2.3 BNP Paribas
- **Accès** : mabanque.bnpparibas → « Virements et services » → « Téléchargement des opérations » (URL directe : `/fr/secure/virements-services/telechargement-des-operations` ; la page a été retirée de la navigation en juillet 2023 mais restait accessible en direct). Formats : **PDF, CSV, OFX, QIF**, fenêtre ~90 jours.
- **Nom de fichier** : type `E{digits}.csv` se terminant par les 4 derniers chiffres du compte (ex. `E0790170.csv`) ; regex utilisable : `E\d+{last4}\.csv`.
- **Structure CSV** (fichier réel) :
- **Encodage** : ISO-8859-1. **Séparateur** : `;`.
- **1re ligne = métadonnées de solde**, PAS un en-tête de colonnes :
```
"Cr&eacute;dit immobilier";"Cr&amp;eacute;dit immobilier";****1234;18/03/2022;;-123 456,78
```
Soit : libellé compte ; libellé compte (échappé HTML **deux fois** — il faut appliquer `html.unescape()` **2×**) ; n° compte masqué ; date d'export ; (vide) ; **solde** avec espace de milliers et virgule décimale.
- **Lignes suivantes = opérations**, sans ligne d'en-tête :
```
05/01/2022;;; AMORTISSEMENT PRET 1234;70,93
```
Colonnes : `date; (vide); (vide); libellé; montant_signé`. Date `JJ/MM/AAAA`, virgule décimale, espaces de milliers possibles.
- **Implémentation preset** : `header_rows: 1` (ligne solde à parser à part pour proposer un rapprochement de solde), `columns: [date, skip, skip, label, amount]`, `has_column_header: false`.
### 2.4 Société Générale
- **Accès** : espace client → « Gestion et suivi » / « Relevés et documents » → export **CSV** (~6 mois d'historique). Le **QIF** avait disparu à la refonte du site (2019) puis est **revenu en février 2020**.
- **Structure CSV** (fichier réel analysé par enodev.fr) :
- **Encodage** : ISO-8859-1 (ou -15), fins de ligne **CRLF**. **Séparateur** : `;`.
- **Ligne 1 (préambule)** : `="0201900016400270";17/05/2019;16/11/2019;` — n° de compte en notation Excel `="…"` (pour préserver les zéros de tête), puis début et fin de période.
- **Ligne 2 (en-tête)** : `date_comptabilisation;libellé_complet_operation;montant_operation;devise;`
- **Ligne de données** : `15/11/2019;CARTE X7527 15/11 METRO ;-14,90;EUR;`
- Date `JJ/MM/AAAA`, **montant signé** unique, virgule décimale, devise explicite, point-virgule terminal (colonne vide finale).
- Les libellés sont **paddés d'espaces** et peuvent s'étaler sur plusieurs lignes pour les virements/prélèvements ⇒ `strip()` + gestion des champs multi-lignes.
- **Implémentation preset** : `header_rows: 1` puis ligne d'en-têtes ; extraire le n° de compte de la ligne 1 via regex `="(\d+)"`.
### 2.5 La Banque Postale
- **Accès** : espace client → menu « OPÉRATIONS » → « Téléchargement d'opérations » → choix du compte → « Format CSV (compatible Excel) » (aussi **TSV** et **OFX** selon le type de compte, via le bouton « Télécharger le détail »). **Limite : ~90 jours** d'historique.
- **Structure CSV** (modèle documenté par OpenFlyers) :
- **Encodage** : ISO-8859-15. **Séparateur** : `;`. Pas de pied de page.
- **Préambule (~8 lignes)**, exploitables pour le rapprochement de solde :
```
Numéro Compte ;[numéro]
Type ;COMPTE
Compte tenu en ;euros
Date ;[date]
Solde (EUROS) ;[solde]
Solde (FRANCS) ;[solde]
```
- **En-tête de colonnes** : `Date;Libellé;Montant(EUROS);Montant(FRANCS)`
- Date `JJ/MM/AAAA`, **montant signé** (négatif = débit), virgule décimale. La colonne `Montant(FRANCS)` est un vestige à ignorer.
- Attention : l'export **TSV perd la date de valeur** et est découpé mois par mois — préférer CSV.
- **Implémentation preset** : `skip_until_header_startswith: "Date;"` (robuste face aux variations du préambule), colonne FRANCS ignorée.
### 2.6 Caisse d'Épargne (groupe BPCE)
- **Accès** : espace client → sur le compte, « Gérer » → « Télécharger les opérations » (page d'aide officielle : `aide.caisse-epargne.fr/contents/comment-exporter-mes-operations`). Le fichier **ne contient pas le solde**.
- **Noms de fichier observés** : `{DDMMYYYY}_{numéro}.csv` (ancien) et `{numéro}_{DDMMYYYY}_{DDMMYYYY}.csv` (récent, période début/fin). Regex preset : `\d*{account}_\d{8}_\d{8}\.csv` et `\d{8}_{account}\.csv`.
- **Structure CSV « nouveau format » (2024+, fichier réel)** :
- **Encodage** : ISO-8859-1. **Séparateur** : `;`. Pas de préambule.
- **En-tête exact** :
```
Date de comptabilisation;Libelle simplifie;Libelle operation;Reference;Informations complementaires;Type operation;Categorie;Sous categorie;Debit;Credit;Date operation;Date de valeur;Pointage operation
```
- **Exemple** :
```
15/11/2024;SUPERMARCHE;CB SUPERMARCHE CENTRAL FACT 141124;;;Carte bancaire;Alimentation;Hyper/supermarche;-45,50;;14/11/2024;15/11/2024;0
10/11/2024;EMPLOYEUR SA;VIR INST Employeur SA;REF123456;Salaire Novembre-;Virement recu;Revenus;Salaires;;+3500,00;09/11/2024;09/11/2024;0
```
- Dates `JJ/MM/AAAA` (3 colonnes de dates : comptabilisation / opération / valeur), virgule décimale, **Débit en négatif** dans sa colonne, **Crédit préfixé `+`** — le parseur de montants doit accepter `+` et `-`.
- `Categorie`/`Sous categorie` : suggestions de catégorisation. `Libelle simplifie` = nom de marchand nettoyé, excellent pour l'affichage et les règles.
- Banque Populaire (même groupe BPCE) a des exports proches (« Documents → Vos écritures et opérations → CSV ») — le preset CE servira de base si besoin.
### 2.7 Fortuneo
- **Accès** : espace client → historique du compte → export. Formats annoncés : **CSV, Excel, QIF, OFX**, avec un historique allant jusqu'à **10 ans** (le plus généreux des banques FR).
- **Nom de fichier** : `HistoriqueOperations_{compte}_du_JJ_MM_AAAA_au_JJ_MM_AAAA.csv`.
- **Structure CSV** (fichier réel) :
- **Séparateur** : `;`. **Encodage** : historiquement ISO-8859-1/Windows-1252 (des accents cassés sont observés si lu en UTF-8) ; des exports récents semblent être en UTF-8 ⇒ **toujours passer par la détection d'encodage** (voir §7.3).
- **En-tête exact** (noter la casse et le `;` final) :
```
Date opération;Date valeur;libellé;Débit;Crédit;
```
- **Exemple** : `13/12/2019;13/12/2019;CARTE 12/12 FNAC METZ;-6,4;`
- Dates `JJ/MM/AAAA`, virgule décimale, **Débit déjà signé négatif**, montants parfois sans zéro final (`-6,4`), espaces de milliers possibles.
- **Pièges** : les opérations **carte à débit différé** sont isolées puis intégrées à la liste le dernier jour du mois (risque de « trou » puis d'apparition tardive ⇒ importance de la fenêtre de recouvrement) ; les opérations Bourse ne sont pas distinguées des crédits ordinaires.
### 2.8 Revolut
- **Accès** : app/web → Relevés (« Statements ») → export **CSV / Excel / PDF** par compte-devise et par période. Pas d'OFX/QIF. Historique complet disponible.
- **Nom de fichier** : `account-statement_{AAAA-MM-JJ}_{AAAA-MM-JJ}_{...}_{id}.csv`.
- **Structure CSV** (fichier réel) :
- **Séparateur** : `,`. **Encodage** : UTF-8. **Point décimal**.
- **En-tête exact** :
```
Type,Product,Started Date,Completed Date,Description,Amount,Fee,Currency,State,Balance
```
- **Exemple** : `TOPUP,Current,2024-01-05 14:00:40,2024-01-05 14:00:41,Payment from M Huang Mincong,10.00,0.00,USD,COMPLETED,74.43`
- Dates `AAAA-MM-JJ HH:MM:SS` (heure locale du compte).
- **Règles d'import** :
- **N'importer que `State == COMPLETED`** (les états `PENDING`/`REVERTED` changent ou disparaissent — source classique de doublons).
- `Amount` est **hors frais** ; l'impact réel sur le solde = `Amount - Fee` (Fee est positif). Deux stratégies : (a) créer une transaction unique de montant net, en notant le frais dans un champ `metadata` ; (b) créer deux transactions (opération + frais). Recommandé v1 : **montant net + note**, plus simple pour les budgets.
- Un compte Revolut = plusieurs devises ⇒ un export par devise ; modéliser un `account` LifeTrack par devise, ou stocker `currency` par transaction.
- `Type` utiles : `TOPUP`, `CARD_PAYMENT`, `TRANSFER`, `EXCHANGE`, `ATM`, `FEE` — mappables vers des catégories par défaut.
### 2.9 N26
- **Accès** : application web (app.n26.com) → téléchargement des activités (« Download activities ») par période. **CSV uniquement** (pas d'OFX/QIF).
- **Structure CSV actuelle (format 2023+)** :
- **Séparateur** : `,`. **Encodage** : UTF-8. **Point décimal**. Dates `AAAA-MM-JJ`. Champs quotés (sauf `Type`).
- **En-tête exact** :
```
"Booking Date","Value Date","Partner Name","Partner Iban",Type,"Payment Reference","Account Name","Amount (EUR)","Original Amount","Original Currency","Exchange Rate"
```
- `Partner Iban` = IBAN de la contrepartie (précieux pour les règles de catégorisation et la détection de virements internes). `Original Amount/Currency/Exchange Rate` renseignés pour les paiements hors EUR.
- **Ancien format (avant ~2023)**, à supporter en option dans le preset (des utilisateurs ont des archives) :
```
"Date","Payee","Account number","Transaction type","Payment reference","Amount (EUR)","Amount (Foreign Currency)","Type Foreign Currency","Exchange Rate"
```
- Le preset N26 doit **détecter la variante par la ligne d'en-tête**.
---
## 3. Export d'activité PayPal
### 3.1 Où et quoi
- **Accès** : paypal.com → Activité → « Télécharger » / Relevés → « Activité personnalisée » ; ou Rapports (comptes business) → « Activity Download ».
- **Formats** : **CSV**, **TAB**, PDF ; QIF (USD uniquement) et IIF (US uniquement) — ignorer QIF/IIF.
- **Limites** : historique **7 ans**, période max **12 mois par rapport**, **50 000 lignes max** par fichier (sinon ZIP multi-fichiers). Préréglages : « depuis le dernier téléchargement », mois écoulé, 3 mois, 6 mois…
- **Encodage** : UTF-8. **Séparateur** : `,` avec champs quotés.
- **Locale FR — piège majeur** : dates en `JJ/MM/AAAA` et **virgule décimale à l'intérieur des champs quotés** (ex. `"1 234,56"`). Un compte configuré en anglais exporte en `MM/DD/YYYY` avec point décimal. Le preset PayPal doit donc proposer le choix de locale (défaut FR).
### 3.2 Colonnes
Le rapport « Activity Download » est **personnalisable** (87 champs possibles). Champs **obligatoires** (toujours présents) :
```
Date, Time, TimeZone, Name, Type, Status, Currency, Gross, Fee, Net,
From Email Address, To Email Address, Transaction ID, Reference Txn ID,
Receipt ID, Balance Impact
```
Champs cochés par défaut (sélection) : `Balance`, `Subject`, `Note`, `Invoice Number`, `Country Code`, adresses, etc.
Sur les **comptes personnels**, l'export simplifié peut ne contenir qu'un sous-ensemble du type : `Date, Time, TimeZone, Name, Type, Status, Currency, Amount, Receipt ID, Balance` (une seule colonne `Amount` au lieu de Gross/Fee/Net). Le preset doit accepter **les deux variantes** (détection par en-tête).
### 3.3 Sémantique des montants et devises
- `Gross` = montant brut signé ; `Fee` = frais (négatif) ; **`Net = Gross + Fee`**.
- `Balance Impact` ∈ {`Credit`, `Debit`, `Memo`} : les lignes `Memo` (autorisations, paiements en attente, lignes informatives) **n'affectent pas le solde ⇒ les exclure de l'import**.
- `Status` : n'importer que `Completed` (exclure `Pending`, `Denied`, `Reversed`…).
- **Conversions de devises** : un achat en devise génère 23 lignes liées (`Type` contenant "Currency Conversion" / « Conversion de devise » : une ligne de débit dans la devise d'origine, une ligne de crédit en EUR, liées par `Reference Txn ID`). Stratégie v1 recommandée : **importer uniquement les lignes dont `Currency` == devise du compte LifeTrack (EUR) et `Balance Impact` != `Memo`**, ce qui capture l'effet net en euros sans doublonner.
- `Transaction ID` : identifiant alphanumérique 17 caractères, **unique et stable** ⇒ clé de déduplication idéale (`external_id`).
- `Reference Txn ID` : lie remboursements/conversions à la transaction d'origine — à stocker en métadonnée.
---
## 4. Le format OFX en France (et QIF)
### 4.1 Ce qu'on trouve réellement
- Les banques françaises qui proposent OFX (BoursoBank, BNP, La Banque Postale, Crédit Agricole selon caisse, Fortuneo…) livrent quasi toujours de l'**OFX 1.x SGML** (en-tête `OFXHEADER:100`, `DATA:OFXSGML`, `VERSION:102`), **pas** de l'OFX 2.x XML. Encodage souvent déclaré `ENCODING:USASCII`/`CHARSET:1252` mais réellement Windows-1252/Latin-1.
- Structure utile par transaction (`<STMTTRN>`) : `TRNTYPE` (DEBIT/CREDIT/XFER/…), `DTPOSTED` (`AAAAMMJJ`), `TRNAMT` (**point décimal**, signé), `FITID`, `NAME`, `MEMO` éventuel. Le bloc `<BANKACCTFROM>` donne banque/guichet/compte — parfait pour router vers le bon compte.
- **Avantages vs CSV** : pas d'ambiguïté de date/décimale, identifiant `FITID`, solde de fin (`<LEDGERBAL>`), n° de compte inclus.
### 4.2 Fiabilité du FITID — mise en garde
La spec OFX exige que le FITID identifie de façon unique une transaction **dans le périmètre d'un compte** et reste **stable entre téléchargements** (« FITIDs must be unique within the scope of […] an account » ; unicité inter-banques non garantie ⇒ clé = banque+compte+FITID). En pratique :
- **LCL (cas documenté par Akretion)** : FITID fabriqué comme `code_type + date JJMMAA + montant en centimes` (ex. `948 200423 -1275`) ⇒ **deux paiements CB du même montant le même jour = même FITID**. Violation flagrante, présente depuis au moins 2016.
- D'autres établissements (cas US documentés : Discover…) **régénèrent des FITID différents à chaque téléchargement** pour la même transaction.
**Conséquence pour LifeTrack** : traiter le FITID comme `external_id` de dédup **prioritaire mais non exclusif** — toujours doubler d'un hash de contenu, et ne jamais planter sur un FITID dupliqué à l'intérieur d'un même fichier (suffixer par un compteur d'occurrence, cf. §6.4).
### 4.3 QIF
Format texte Quicken sans identifiants, dates ambiguës (`D` au format local), pas de devise, pas de n° de compte. Fortuneo, SG, BNP, CA le proposent encore. **Ne pas l'implémenter en v1** (CSV + OFX couvrent tout) ; à garder en idée v3 si un utilisateur n'a que ça.
### 4.4 Bibliothèques Python
- **`ofxparse`** : tolérant, gère l'OFX 1.x SGML sale des banques (via BeautifulSoup/sgmllib). Maintenance faible mais c'est le standard de fait pour ce besoin. **Recommandé v1**, avec pré-traitement : détection/normalisation d'encodage avant parsing.
- **`ofxtools`** : plus strict/complet (OFX 2.x, typage), moins indulgent avec les fichiers non conformes français. Alternative si `ofxparse` pose problème.
---
## 5. Agrégation PSD2 : état des lieux 2025/2026
### 5.1 GoCardless Bank Account Data (ex-Nordigen) — en extinction
- Historiquement **LA** solution gratuite : jusqu'à **50 connexions bancaires/mois** gratuites, couverture de ~2500 banques UE dont toutes les grandes banques françaises, consentement PSD2 de 90 jours (180 pour certaines banques), jusqu'à 24 mois d'historique selon banque.
- **Limite de taux introduite en 2024 : ~4 appels de synchronisation par jour et par compte** (les importeurs comme Firefly III data importer ≥ 1.5.6 la gèrent).
- API simple : `secret_id`/`secret_key` → token ; `GET /institutions?country=fr` ; création d'une « requisition » (lien d'autorisation redirigeant vers la banque) ; puis `GET /accounts/{id}/transactions` (JSON, montants `transactionAmount.amount` + `currency`, `internalTransactionId`/`transactionId` pour la dédup).
- **⚠️ Depuis juillet 2025 : plus aucune inscription nouvelle** (« GoCardless has stopped accepting new Bank Account Data accounts », confirmé par la doc Actual Budget) ; le produit est en cours d'abandon. Les comptes existants continuent de fonctionner, sans garantie de durée.
- **Conclusion** : notre utilisateur ne pourra probablement **pas** créer de compte ⇒ GoCardless ne peut plus être le connecteur v2 par défaut ; il reste pertinent comme **connecteur optionnel** pour détenteurs de comptes historiques.
### 5.2 Enable Banking — la relève recommandée
- Agrégateur finlandais s'appuyant sur les **API PSD2 officielles** (~2500 banques, 29 pays). Utilisé comme alternative par les communautés Firefly III (tutoriel officiel `docs.firefly-iii.org/tutorials/data-importer/eb/`) et Actual Budget (guide de setup dédié).
- **Mode « restricted » gratuit, adapté à un particulier** : on enregistre une application de **production** dans le Control Panel (enablebanking.com), puis « **Activate by linking accounts** » : on autorise ses propres comptes via le portail Enable Banking + la page d'autorisation de la banque. L'application ne peut ensuite récupérer **que les comptes préalablement liés** (whitelist), tant qu'aucun contrat commercial n'est signé. C'est exactement le périmètre « mes propres comptes ».
- **Authentification API** : l'application possède une **clé privée RSA** ; chaque requête est signée par un **JWT RS256** (kid = application_id). Endpoints principaux : `POST /auth` (démarrage d'autorisation, URL de redirection), `POST /sessions` (échange du code), `GET /sessions/{id}`, `GET /accounts/{uid}/transactions` (pagination par `continuation_key`), `GET /accounts/{uid}/balances`.
- **Couverture France confirmée** (doc `enablebanking.com/docs/markets/fr/`) : BNP Paribas, **Crédit Agricole** (choix de la **caisse régionale** ; SCA via l'app « Ma Banque »), Société Générale, La Banque Postale, Crédit Mutuel, CIC, LCL, Banque Populaire, **Caisse d'Épargne**, etc. Les flux d'authentification français sont de type **redirect** avec SCA sur l'app mobile de la banque. BoursoBank/Fortuneo figurent dans le réseau PSD2 français (STET) ; **Revolut et N26** exposent aussi des API PSD2 européennes — vérifier leur présence exacte dans la liste d'ASPSP du Control Panel au moment de l'implémentation (non listés sur la page marché FR).
- **Contraintes PSD2 invariables** : consentement à renouveler (90 jours réglementaires, jusqu'à 180 selon banque), historique initial limité par la banque (souvent 90 jours à 24 mois au premier accès), chaque session doit être autorisée via API même en mode restreint.
### 5.3 Autres options commerciales (non retenues)
- **Powens** (ex-Budget Insight, adossé Crédit Mutuel Arkéa) : excellente couverture FR (y compris épargne/assurance-vie), mais **B2B, tarification sur contrat**, pas d'offre particulier.
- **Bridge** (ex-Bankin' B2B, adossé BPCE) : idem, B2B uniquement.
- **Tink** (Visa) : B2B, plus de free tier significatif pour un usage personnel.
- **SimpleFIN** : populaire chez Actual Budget mais **couvre les banques nord-américaines** — hors sujet pour la France.
### 5.4 woob (ex-weboob) — scraping open-source
- Framework Python AGPL de scraping bancaire (modules `boursorama`, `creditagricole`, `fortuneo`, etc.), moteur historique de **Kresus** (gestionnaire de finances self-hosted français). Projet actif (GitLab `woob/woob`).
- **Fragilité structurelle** : les modules cassent à chaque refonte des sites (ex. documenté : synchronisation BoursoBank cassée à l'automne 2025, `AttributeError` dans le module boursorama). Nécessite de stocker les **identifiants bancaires en clair côté serveur**, et le scraping peut déclencher des blocages / est contraire aux CGU de certaines banques.
- **Position LifeTrack** : ne pas en faire une dépendance cœur. Au mieux, un adaptateur optionnel v3 (« woob bridge » qui exporte du JSON/CSV consommé par notre pipeline d'import).
### 5.5 Ce que font Firefly III et Actual Budget (référence)
| App | Import fichiers | Sync bancaire |
|---|---|---|
| Firefly III (+ Data Importer) | CSV (mapper générique + configs communautaires JSON par banque, dépôt `firefly-iii/import-configurations` : profils `fr/boursorama`, `fr/fortuneo`…), camt.053 | GoCardless (legacy), **Enable Banking** (nouveau), SimpleFIN |
| Actual Budget | CSV/OFX/QFX/QIF/CAMT | GoCardless (legacy), SimpleFIN (US), **Enable Banking**, Pluggy.ai (Brésil) |
Enseignement : les deux références self-hosted ont pivoté **GoCardless → Enable Banking** pour l'Europe ; leur modèle « mapper générique + profils par banque en JSON versionnés » est exactement l'architecture retenue pour LifeTrack v1.
---
## 6. Stratégies de déduplication
Le besoin : l'utilisateur réimporte régulièrement des fichiers **qui se chevauchent** (historique court chez les banques FR), et pourra un jour cumuler fichier + sync API sur le même compte. Il faut être **idempotent** sans perdre de vraies transactions identiques (deux cafés à 2,50 € le même jour chez le même commerçant sont légitimes).
### 6.1 Ce que fait l'état de l'art
- **Firefly III** : deux mécanismes — (1) « content-based » : hash SHA-256 du JSON complet de la transaction soumise, comparé aux hashes existants (fragile : le hash change si la banque change la casse ou si le mapping change) ; (2) « identifier-based » : une colonne mappée sur `external_id`/`internal_reference` est recherchée avant import — « a very reliable way to detect duplicates ».
- **Actual Budget** : champ **`imported_id`** (FITID pour OFX, id GoCardless pour la sync) — « transactions with the same imported_id will never be added more than once » ; sinon **rapprochement flou** : même montant + date proche (fenêtre de quelques jours) + payee similaire ⇒ fusion proposée. Bug historique corrigé en 2024 : le fuzzy match ne doit **pas** fusionner deux transactions portant des `imported_id` différents (sauf quirk GoCardless) — règle à reprendre telle quelle.
### 6.2 Clés candidates
1. **`external_id` fourni par la source** : OFX `FITID` (voir caveats §4.2), PayPal `Transaction ID` (fiable), GoCardless `internalTransactionId` (fiable), Enable Banking `entry_reference` (fiabilité variable selon banque). Unicité à imposer **par (account_id, source_kind)**, jamais globalement.
2. **Hash de contenu** : les CSV français n'ont **aucun identifiant** ⇒ hash déterministe sur les champs stables : compte + date comptable + montant + libellé normalisé + devise.
3. **Rapprochement flou** (aide à la décision, jamais automatique en suppression) : même compte, même montant, date à ±3 jours, similarité de libellé (trigrammes `pg_trgm`) — utile parce que les banques FR **changent le libellé et la date** entre l'opération « en cours » et l'opération comptabilisée.
### 6.3 Pièges spécifiques observés
- **Libellés instables** : SG padde d'espaces ; CB « en cours » devient « CARTE 12/12 FNAC METZ » une fois comptabilisée ; Fortuneo intègre le débit différé en fin de mois. ⇒ normalisation agressive du libellé avant hash (cf. ci-dessous) et fenêtre de recouvrement.
- **Doublons légitimes intra-journée** : gérés par un **compteur d'occurrence** intégré au hash — technique éprouvée (les import-id YNAB/Actual sont suffixés d'un index d'occurrence). Dans un même fichier, la n-ième ligne strictement identique reçoit `occurrence = n`.
- **FITID dupliqué dans un même fichier OFX** (cas LCL) : appliquer le même compteur d'occurrence au FITID.
- **Balance/solde glissant** (Boursorama `accountbalance`) : ne jamais inclure de colonne de solde dans le hash.
- **Catégories fournies par la banque** (Bourso, CE) : ne pas les inclure dans le hash (elles changent au gré des algos de la banque).
### 6.4 Algorithme retenu pour LifeTrack
Pipeline d'import (fichier ou API) → table de **staging** → dédup → commit :
```text
for each parsed row:
1. Normalize: booking_date (ISO), amount (Decimal, 2 dec), label_norm, currency.
2. If source provides an external id:
ext_key = (account_id, source_kind, external_id [+ ":" + occurrence])
if exists in transactions.external_key -> mark DUPLICATE (skip)
3. Compute content hash:
basis = f"{account_id}|{booking_date}|{amount:+.2f}|{currency}|{label_norm}|{occurrence}"
dedup_hash = sha256(basis)
occurrence = index of this exact basis within the CURRENT import file (0,1,2…)
if dedup_hash exists in transactions -> mark DUPLICATE (skip)
4. Fuzzy pass (only for rows that survived 2 and 3):
candidates = same account, same amount, |date diff| <= 3 days,
similarity(label_norm) >= 0.5 (pg_trgm),
AND (candidate.external_id IS NULL OR row.external_id IS NULL)
if candidates -> mark NEEDS_REVIEW (user confirms merge/import in preview UI)
5. Else -> mark NEW
commit: user validates the preview (counts NEW / DUPLICATE / NEEDS_REVIEW), then batch insert.
```
Normalisation de libellé (`label_norm`) :
```python
import re, unicodedata
def normalize_label(raw: str) -> str:
s = unicodedata.normalize("NFKD", raw)
s = "".join(c for c in s if not unicodedata.combining(c)) # strip accents
s = s.upper()
s = re.sub(r"\s+", " ", s).strip() # collapse whitespace/newlines
return s
```
Parsing des montants français (couvre tous les cas observés : `-6,4`, `+3500,00`, `-123 456,78`, `1 234,56`, `226.68`) :
```python
from decimal import Decimal
def parse_amount(raw: str) -> Decimal:
s = raw.strip().replace(" ", "").replace("", "").replace(" ", "")
if "," in s:
s = s.replace(".", "").replace(",", ".") # French style
return Decimal(s) # accepts leading + or -
```
**Fenêtre de recouvrement** : encourager l'utilisateur (UI) à toujours exporter avec chevauchement (ex. « depuis 7 jours avant le dernier import ») ; côté serveur, la dédup rend le chevauchement inoffensif. Stocker par compte `last_imported_max_date` pour afficher « dernière opération connue : … » et suggérer la période d'export.
**Traçabilité** : chaque ligne insérée référence un `import_batch` (fichier source, profil utilisé, horodatage, checksum du fichier). Un batch est **annulable en bloc** (undo), et un même fichier (même checksum SHA-256) déjà importé est refusé d'emblée avec message clair.
---
## 7. Recommandations d'implémentation — v1 (import fichiers)
### 7.1 Périmètre v1
1. **Mapper CSV générique** avec presets par banque (BoursoBank, Crédit Agricole, BNP, SG, La Banque Postale, Caisse d'Épargne, Fortuneo, Revolut, N26, PayPal).
2. **Parseur OFX** (`ofxparse`) — couvre BoursoBank, BNP, LBP, CA, Fortuneo pour les utilisateurs qui préfèrent l'OFX.
3. **Preset PayPal** (variante business Gross/Fee/Net + variante perso Amount).
4. Saisie manuelle + import batch annulable + moteur de règles de catégorisation.
5. Pas de QIF, pas de PDF, pas de sync API en v1.
### 7.2 Modèle de données (SQLAlchemy — esquisse)
```python
class BankAccount(Base): # finance account, multi-user ready
id: UUID; user_id: UUID
name: str; kind: str # checking|savings|card|paypal|cash
currency: str = "EUR"
iban_last4: str | None
last_imported_max_date: date | None
class ImportProfile(Base): # a saved CSV mapping (preset or user-defined)
id: UUID; user_id: UUID | None # None => built-in preset
slug: str # "boursobank", "credit-agricole", ...
config: JSONB # see 7.4
class ImportBatch(Base):
id: UUID; user_id: UUID; account_id: UUID | None
profile_slug: str | None; source_kind: str # csv|ofx|paypal|api-...
filename: str; file_sha256: str # reject identical re-upload
created_at: datetime
stats: JSONB # {new, duplicates, review}
status: str # pending|committed|rolled_back
class Transaction(Base):
id: UUID; account_id: UUID
booking_date: date; value_date: date | None
amount: Numeric(14, 2); currency: str
label_raw: str; label_norm: str
counterparty_iban: str | None
category_id: UUID | None
source_kind: str # csv|ofx|paypal|api-enablebanking|manual
external_id: str | None # FITID / PayPal Transaction ID / API id
occurrence: int = 0
dedup_hash: str # sha256 hex, UNIQUE per account
import_batch_id: UUID | None
metadata: JSONB # bank category, fee, reference_txn_id, state...
__table_args__ = (
UniqueConstraint("account_id", "dedup_hash"),
Index(..., "account_id", "source_kind", "external_id", unique=True,
postgresql_where=text("external_id IS NOT NULL")),
)
```
Activer l'extension **`pg_trgm`** pour le fuzzy match (`similarity(label_norm, :candidate)`).
### 7.3 Chaîne de lecture des fichiers
1. **Encodage** : lire les premiers Ko ; si BOM UTF-8 ⇒ `utf-8-sig` ; sinon tenter `utf-8` strict ; en cas d'échec, `charset-normalizer` avec repli forcé `cp1252` (couvre ISO-8859-1/15 pour nos banques). Ne jamais faire confiance à l'extension.
2. **Séparateur** : `csv.Sniffer` sur un échantillon, restreint à `;`, `,`, `\t` ; heuristique de départage : compter les occurrences hors guillemets sur les 5 premières lignes.
3. **Préambule** : trois stratégies configurables par profil : `header_rows: N` (fixe) ; `skip_until_header_startswith: "Date;"` (CA, LBP) ; `has_column_header: false` + mapping positionnel (BNP). Toujours afficher un **aperçu brut** des 20 premières lignes dans l'UI pour que l'utilisateur ajuste.
4. **Champs multi-lignes** : parser avec le module `csv` sur le flux complet (jamais de `split("\n")` préalable) — requis pour CA et SG.
5. **Fin de données** : option `stop_on_non_date_row` (CA : pied de page après lignes vides).
6. **Dates** : essai ordonné des formats candidats du profil (`%d/%m/%Y`, `%Y-%m-%d`, `%Y-%m-%d %H:%M:%S`, `%d.%m.%Y`) ; en mode générique, auto-détection sur l'échantillon avec désambiguïsation JJ/MM par la présence de valeurs > 12.
7. **Montants** : `parse_amount()` de §6.4 ; modes `signed_column`, `debit_credit_columns` (CA, CE, Fortuneo), `invert_sign` (option).
### 7.4 Schéma JSON d'un profil d'import (`ImportProfile.config`)
```json
{
"file": {
"encoding": "auto",
"delimiter": ";",
"quotechar": "\"",
"header_rows": 0,
"skip_until_header_startswith": null,
"has_column_header": true,
"stop_on_non_date_row": false,
"filename_regex": "export-operations-.*\\.csv"
},
"columns": {
"booking_date": "dateOp",
"value_date": "dateVal",
"label": "label",
"amount": "amount",
"debit": null,
"credit": null,
"currency": null,
"external_id": null,
"account_hint": "accountNum",
"bank_category": ["categoryParent", "category"],
"counterparty_iban": null
},
"parsing": {
"date_formats": ["%Y-%m-%d"],
"decimal_comma": true,
"invert_sign": false,
"row_filters": [{"column": "State", "op": "equals", "value": "COMPLETED"}]
},
"dedup": {"external_id_is_reliable": false}
}
```
Les presets sont livrés dans le repo (`api/app/finance/presets/*.json`), versionnés, et **clonables** par l'utilisateur pour ajustement (les banques changent leurs formats sans préavis — c'est certain à moyen terme). La détection automatique du preset combine `filename_regex` et **signature d'en-tête** (liste de noms de colonnes attendus, comparaison insensible à la casse/accents).
### 7.5 Contenu initial des presets (récapitulatif opérationnel)
| Preset | file | columns (essentiel) |
|---|---|---|
| `boursobank` | `;`, UTF-8, header ligne 1 | `dateOp`→booking, `dateVal`→value, `label`, `amount` (virgule), `accountNum`→routage multi-comptes, `category*`→suggestion ; ignorer `accountbalance` |
| `credit-agricole` | `;`, cp1252, `skip_until_header_startswith: "Date;"`, stop_on_non_date_row | `Date`, `Date valeur`, `Libellé` (multi-lignes), `Débit Euros`/`Crédit Euros` |
| `bnp-paribas` | `;`, latin-1, `header_rows: 1`, `has_column_header: false` | positions : 0=date, 3=label, 4=amount ; ligne 1 = solde (double `html.unescape`) |
| `societe-generale` | `;`, latin-1, `header_rows: 1` | `date_comptabilisation`, `libellé_complet_operation` (trim), `montant_operation`, `devise` |
| `banque-postale` | `;`, latin-9, `skip_until_header_startswith: "Date;"` | `Date`, `Libellé`, `Montant(EUROS)` ; ignorer `Montant(FRANCS)` ; préambule → solde affichable |
| `caisse-epargne` | `;`, latin-1, header ligne 1 | `Date operation`→booking, `Date de valeur`→value, `Libelle operation`→label (+`Libelle simplifie` en metadata), `Debit`/`Credit` (accepter `+`), `Categorie`/`Sous categorie`→suggestion |
| `fortuneo` | `;`, encodage **auto**, header ligne 1 (`;` final ⇒ colonne fantôme à ignorer) | `Date opération`, `Date valeur`, `libellé`, `Débit`/`Crédit` |
| `revolut` | `,`, UTF-8, point décimal | `Completed Date`→booking, `Description`, `Amount`+`Fee`→net, `Currency`, filtre `State == COMPLETED` |
| `n26` | `,`, UTF-8, point décimal, 2 variantes d'en-tête | `Booking Date`/`Value Date`, `Partner Name`+`Payment Reference`→label, `Partner Iban`, `Amount (EUR)` |
| `paypal` | `,`, UTF-8, locale FR (date `JJ/MM/AAAA`, virgule) | `Date`+`Time`, `Name`+`Type`→label, `Net` (ou `Amount`), `Currency`, `Transaction ID`→external_id (fiable), filtres `Status == Completed` et `Balance Impact != Memo` et `Currency == EUR` |
### 7.6 Import OFX
- Endpoint d'upload commun ; si le contenu commence par `OFXHEADER` ou `<?xml` + `<OFX>`, router vers le parseur OFX.
- Pré-traitement : décoder selon §7.3 puis passer à `ofxparse.OfxParser.parse()`.
- Mapper : `stmt.account.account_id`/`routing_number` → proposition de compte LifeTrack ; par transaction : `date`→booking_date, `amount` (Decimal, point), `payee`+`memo`→label, `id` (FITID)→external_id avec `external_id_is_reliable: false` (⇒ le hash de contenu reste co-vérifié) et compteur d'occurrence en cas de FITID dupliqué dans le fichier (cas LCL).
- Exploiter `<LEDGERBAL>` pour proposer un rapprochement de solde après import.
### 7.7 UI d'import (rappel UX, libellés FR)
Assistant en 4 étapes : **1. Fichier** (drag & drop, détection preset, choix du compte) → **2. Réglages** (aperçu brut, mapping colonnes éditable, formats) → **3. Prévisualisation** (tableau : Nouvelles / Doublons ignorés / À vérifier, avec diff pour les fuzzy matches) → **4. Confirmation** (stats du batch, bouton « Annuler cet import » disponible ensuite dans l'historique des imports).
---
## 8. Recommandations d'implémentation — v2 (synchronisation automatique)
### 8.1 Choix du fournisseur : Enable Banking (et pourquoi pas GoCardless)
- **GoCardless BAD est fermé aux nouveaux comptes depuis juillet 2025** (§5.1) ⇒ inutilisable pour un nouvel utilisateur. Garder un connecteur optionnel « legacy » n'est justifié que si peu coûteux (l'API est simple) ; le marquer *deprecated* dès le départ.
- **Enable Banking** coche toutes les cases pour LifeTrack : API PSD2 officielles, **gratuit en mode restreint sur ses propres comptes** (whitelist via « Activate by linking accounts »), couverture des banques du user (CA par caisse régionale, BNP, SG, LBP, CE…), déjà éprouvé par Firefly III et Actual Budget. Prérequis utilisateur : créer un compte enablebanking.com, une application de production, télécharger la **clé privée**, lier ses comptes dans le portail.
### 8.2 Architecture du connecteur (framework « connector »)
```python
class FinanceConnector(Protocol): # in the shared connector framework
slug: str
async def list_accounts(self) -> list[RemoteAccount]: ...
async def fetch_transactions(self, remote_account_id: str,
date_from: date) -> AsyncIterator[RawTransaction]: ...
async def fetch_balance(self, remote_account_id: str) -> Balance: ...
async def authorization_status(self) -> AuthStatus # consent expiry etc.
```
- **Config stockée chiffrée** (table `connector_credentials`) : `application_id`, clé privée PEM (chiffrée au repos avec la clé applicative), mapping `remote_account_uid → bank_account_id`.
- **Auth Enable Banking** : générer un JWT RS256 par requête (`iss`/`aud` selon doc, `kid = application_id`, exp courte). Flux de consentement : `POST /auth` → URL de redirection bancaire (ouvrir dans le navigateur, callback vers l'UI LifeTrack) → `POST /sessions` → stocker `session_id` + échéance de consentement.
- **Sync** : job planifié (APScheduler dans l'API) 12×/jour + bouton « Synchroniser maintenant ». `fetch_transactions(date_from = last_synced_date - 7 jours)` (fenêtre de recouvrement), pagination `continuation_key`, puis injection dans **le même pipeline staging + dédup** que les imports fichiers (`source_kind = "api-enablebanking"`, `external_id = entry_reference` si présent, hash de contenu sinon). Les statuts `PDNG` (pending) sont ignorés ou marqués provisoires ; n'entériner que `BOOK` (booked).
- **Gestion du consentement (UX critique)** : bannière « Consentement expire le JJ/MM » + relance guidée du flux d'autorisation (échéance ~90/180 jours selon banque). Prévoir l'état « connecteur en erreur d'auth » visible sur le dashboard.
- **Multi-source sur un même compte** (fichier + API) : la dédup §6.4 est l'unique garde-fou — raison de plus pour ne jamais court-circuiter le pipeline commun.
### 8.3 Hors périmètre v2 (documenté pour v3+)
- Adaptateur **woob** optionnel (fragile, credentials en clair — §5.4).
- Import **QIF** et relevés **PDF** (OCR/extraction — projets type `LBPExtract` montrent la faisabilité pour LBP).
- **camt.053** (XML ISO 20022) si un jour utile (Firefly l'accepte ; peu diffusé côté particuliers FR).
---
## 9. Sources
Formats bancaires :
- [ScanCompte — Exporter son relevé BoursoBank (CSV/PDF/OFX)](https://www.scancompte.com/banques/exporter-releve-boursorama) ; [What In My Pocket — Export BoursoBank](https://whatinmypocket.com/guides/exporter-releve-boursobank/)
- [mincong-h/finance-toolkit](https://github.com/mincong-h/finance-toolkit) — parseurs + fichiers d'exemple réels BNP / Boursorama / Fortuneo / Revolut / Caisse d'Épargne (en-têtes cités §2), docs `docs/boursorama.md`, `docs/bnp.md`, `docs/caisse-epargne.md`
- [OpenFlyers — Modèle CSV Crédit Agricole](https://doc4-fr.openflyers.com/Mod%C3%A8le-d'import-de-relev%C3%A9-bancaire-CSV-Cr%C3%A9dit-Agricole-avec-point-virgule) ; [Modèle CSV Banque Postale](https://doc4-fr.openflyers.com/Mod%C3%A8le-d'import-de-relev%C3%A9-bancaire-CSV-Banque-Postale-avec-point-virgule) ; [Modèle CSV Société Générale](https://doc4-fr.openflyers.com/Mod%C3%A8le-d'import-de-relev%C3%A9-bancaire-CSV-Soci%C3%A9t%C3%A9-G%C3%A9n%C3%A9rale-avec-point-virgule) ; [page générale exports banques](https://doc4-fr.openflyers.com/Exporter-un-relev%C3%A9-bancaire-depuis-un-site-internet-de-banque)
- [enodev.fr — Fin du QIF à la Société Générale (structure CSV réelle)](https://enodev.fr/posts/fin-du-qif-a-la-societe-generale.html)
- [ofxpress — BNP Paribas](https://ofxpress.fr/telecharger-votre-releve-bancaire-bnp-paribas/), [La Banque Postale](https://ofxpress.fr/telecharger-votre-releve-bancaire-la-banque-postale/), [Crédit Agricole](https://ofxpress.fr/telecharger-votre-releve-bancaire-credit-agricole/)
- [Aide Caisse d'Épargne — Comment exporter mes opérations](https://www.aide.caisse-epargne.fr/contents/comment-exporter-mes-operations)
- [statementsheet — Fortuneo (formats, 10 ans)](https://statementsheet.com/how-to-convert-fortuneo-bank-statement-to-excel-csv/) ; [kdecherf — Fortuneo + woob](https://kdecherf.com/blog/2022/11/13/importer-des-transactions-fortuneo-dans-homebank-avec-woob/)
- [Lido — comparatif exports banques FR](https://www.lido.app/fr/releve-bancaire-excel) ; [MoneyVox — profondeur d'historique CSV/OFX par banque](https://www.moneyvox.fr/forums/fil/maximum-historique-des-telechargements-csv-ofx-chez-votre-banque.35927/)
- N26 : [dekodi — colonnes CSV N26](https://manuals.dekodi.de/nexuspub/datenbereitstellungsbuch/n26.html), [KontoCSV N26](https://www.kontocsv.de/en/n26)
- Revolut/N26 (bank2ynab, formats confirmés) : [bank2ynab.conf](https://github.com/bank2ynab/bank2ynab/blob/develop/bank2ynab/data/bank2ynab.conf)
PayPal :
- [PayPal Developer — Activity Download report (champs, formats, limites)](https://developer.paypal.com/docs/reports/online-reports/activity-download/) ; [PDF spec PP_ActivityDownload](https://www.paypalobjects.com/webstatic/en_US/developer/docs/pdf/PP_ActivityDownload.pdf)
- [Putler — export PayPal 2025](https://www.putler.com/export-paypal-transactions/) ; [KontoCSV — PayPal CSV](https://www.kontocsv.de/en/guides/paypal-transactions-csv)
OFX / dédup :
- [Akretion — LCL ne respecte pas la norme OFX (FITID non uniques)](https://akretion.com/fr/blog/lcl-ne-respecte-pas-la-norme-ofx)
- [Quinthar — OFX FITIDs: Not as permanent as you might think](http://blog.quinthar.com/2008/12/ofx-fitids-not-as-permanent-as-you.html) ; [OFX spec (OpenExchange, unicité par compte)](https://xml.coverpages.org/OFEXFIN1.html)
- [Firefly III — Duplicate detection (référence)](https://docs.firefly-iii.org/references/data-importer/duplicate-detection/) (source : dépôt `firefly-iii/docs`)
- [Actual Budget — Importing transactions (imported_id + fuzzy)](https://actualbudget.org/docs/transactions/importing/) ; [PR #2991 — fuzzy match vs imported_id](https://github.com/actualbudget/actual/pull/2991)
PSD2 / agrégation :
- [Actual Budget — GoCardless setup (« stopped accepting new accounts » juillet 2025, 50 connexions, 4 syncs/jour)](https://actualbudget.org/docs/advanced/bank-sync/gocardless/)
- [OpenBankingTracker — Free & Indie Open Banking APIs 2026](https://www.openbankingtracker.com/guides/free-open-banking-apis)
- [Firefly III — tutoriel Enable Banking](https://docs.firefly-iii.org/tutorials/data-importer/eb/) ; [issue #10753 — Enable Banking comme alternative à GoCardless](https://github.com/firefly-iii/firefly-iii/issues/10753)
- [Enable Banking — Linked accounts / restricted mode](https://enablebanking.com/docs/api/linked-accounts/) ; [Enable Banking — marché France](https://enablebanking.com/docs/markets/fr/) ; [FAQ](https://enablebanking.com/docs/faq/)
- [Powens](https://www.powens.com/fr/plateforme/) ; [Bridge (openfinanceguide)](https://openfinanceguide.com/en/glossary/bridge) ; [BPI — mapping open banking FR 2025](https://bigmedia.bpifrance.fr/nos-actualites/mapping-2025-des-acteurs-francais-de-lopen-banking)
- [woob (GitLab)](https://gitlab.com/woob/woob) ; [issue #803 — sync BoursoBank cassée](https://gitlab.com/woob/woob/-/issues/803)
- [Firefly III — import-configurations communautaires (profils fr/boursorama, fr/fortuneo)](https://github.com/firefly-iii/import-configurations)