Files
lifetrack/docs/research/finance-sources.md
T
MeeJayandClaude Opus 5 93f0689c1e 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>
2026-08-14 10:48:57 +02:00

51 KiB
Raw Blame History

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
  2. Formats d'export des banques françaises (fichiers)
  3. Export d'activité PayPal
  4. Le format OFX en France (et QIF)
  5. Agrégation PSD2 : état des lieux 2025/2026
  6. Stratégies de déduplication
  7. Recommandations d'implémentation — v1 (import fichiers)
  8. Recommandations d'implémentation — v2 (synchronisation automatique)
  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 :

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) :

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) :

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)

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)

{
  "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 »)

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 :

PayPal :

OFX / dédup :

PSD2 / agrégation :