feat(balance): CSV import of holdings in detailed snapshot (#245) #251

Closed
maximus wants to merge 0 commits from issue-245-import-csv-titres into main
Owner

Adds a CSV import path for detailed (by-security) balance accounts: an "Import CSV" button per detailed account opens a native picker, auto-detects the delimiter/encoding/columns (symbol, quantity, optional price + book_cost), and lets the user adjust the mapping before importing.

Purely frontend — file -> HoldingDraft[] -> IMPORT_HOLDINGS reducer -> existing atomic save (no SQL/Rust change). Duplicate symbols are merged both within the CSV and against the basket, so no UNIQUE(snapshot_line_id, security_id) violation. A CSV without a price column imports quantities with an empty price (fetch/type later). New pure helpers are unit-tested (csvAutoDetect + useSnapshotEditor); 654 vitest green, build clean, i18n FR/EN.

Resolves #245

Generated autonomously by /autopilot run of 2026-07-04

Adds a CSV import path for detailed (by-security) balance accounts: an "Import CSV" button per detailed account opens a native picker, auto-detects the delimiter/encoding/columns (symbol, quantity, optional price + book_cost), and lets the user adjust the mapping before importing. Purely frontend — file -> HoldingDraft[] -> IMPORT_HOLDINGS reducer -> existing atomic save (no SQL/Rust change). Duplicate symbols are merged both within the CSV and against the basket, so no UNIQUE(snapshot_line_id, security_id) violation. A CSV without a price column imports quantities with an empty price (fetch/type later). New pure helpers are unit-tested (csvAutoDetect + useSnapshotEditor); 654 vitest green, build clean, i18n FR/EN. Resolves #245 Generated autonomously by /autopilot run of 2026-07-04
maximus added 1 commit 2026-07-04 22:04:28 +00:00
feat(balance): CSV import of holdings in detailed snapshot
All checks were successful
PR Check / rust (pull_request) Successful in 22m45s
PR Check / frontend (pull_request) Successful in 2m25s
c8c765e2f0
A detailed (by-security) account can now import its positions from a CSV
instead of adding each security one by one. An "Import CSV" button next to
"Add a title" opens a native picker; the delimiter, encoding and columns
(symbol, quantity, optional price + book_cost) are auto-detected via new
csvAutoDetect helpers and adjustable in a small mapping editor. Rows sharing
a symbol are merged (SUM qty + book_cost, first price) and the batch is merged
into the basket by normalized symbol, so no UNIQUE(snapshot_line_id,
security_id) violation can occur at save. A CSV without a price column imports
quantities with an empty unit_price (fetch/type later) — the existing atomic
save path is unchanged (no SQL/Rust change).

- csvAutoDetect: autoDetectHoldingColumns + analyzeHoldingsCsv (+ tests)
- useSnapshotEditor: holdingsFromCsvRows + IMPORT_HOLDINGS action + importHoldings (+ tests)
- HoldingsCsvImportModal: file pick -> parse -> mapping editor + preview
- i18n FR/EN under balance.snapshot.detailed.importCsv.*
- CHANGELOG (Added / Ajouté)

Resolves #245

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maximus added the
status:approved
autopilot:pending-human
labels 2026-07-04 22:04:43 +00:00
Author
Owner

/pr-review — PR #251 · feat(balance): CSV import of holdings in detailed snapshot (#245)

Verdict : APPROVE

Résumé

Feature purement frontend, bien découpée : la modale possède le cycle de vie fichier/parse, tandis que la construction des HoldingDraft et le merge dans le panier vivent dans useSnapshotEditor (purs, testés). Le chemin de sauvegarde atomique reste inchangé — aucun SQL/Rust touché. La déduplication par symbole normalisé, à la fois dans le CSV (holdingsFromCsvRows) et contre le panier existant (reducer IMPORT_HOLDINGS), protège correctement la contrainte UNIQUE(snapshot_line_id, security_id). i18n FR/EN complet (17 clés, jeux identiques), CHANGELOG mis à jour dans les deux langues, CI verte (frontend 2m25s + rust 22m45s), commit conventionnel, référence l'issue.

Vérifié : boundary Tauri (pick_import_file filtre Vec<(String, Vec<String>)>, detect_encoding/read_file_content en filePath camelCase) conforme aux usages existants ; category_asset_type existe bien sur BalanceAccountWithCategory ; le nouveau prop requis onImportHoldings de SnapshotEditor est fourni par son unique appelant (SnapshotEditPage) ; couverture de tests solide (drafts, merge intra-CSV, reducer in-place/append, préservation du prix, bout-en-bout buildDetailedLines + gotcha no-price qui échoue à la validation de save). Aucun skip/only.

Suggestions (non bloquantes)

  1. autoDetectHoldingColumns — fallback quantité ignore used (csvAutoDetect.ts, ~L1442). Sur un CSV à 2 colonnes Symbole,Cours (sans colonne quantité), le fallback quantity = symbol === 0 && colCount > 1 ? 1 : 0 force quantity = 1, or la colonne 1 est déjà used par unit_price → quantité et prix pointent la même colonne. Résultat : holdingsFromCsvRows lit le cours comme quantité. Cas dégénéré (CSV de positions sans quantité) et rattrapable via la preview, mais le défaut auto est trompeur. Idéalement, ne pas réutiliser une colonne déjà prise, ou surfacer un état « aucune colonne quantité détectée ».

  2. Quantité illisible avalée en 0 (useSnapshotEditor.ts, ~L819). const qty = isNaN(qtyParsed) ? 0 : qtyParsed : une cellule quantité type "N/A"/"-" importe une position à 0 au lieu de la signaler. La preview affiche la valeur brute, donc c'est rattrapable, mais un léger décalage preview↔import subsiste.

  3. Accumulation flottante au merge intra-CSV (useSnapshotEditor.ts, ~L835). existing.qty += qty / existing.book = (existing.book ?? 0) + book peuvent produire des artefacts (0.1 + 0.2 → 0.30000000000000004) dans les strings quantity/book_cost. Cosmétique — la valeur est re-arrondie au cent au save — mais un arrondi à la construction rendrait l'affichage propre.

  4. Preview : symbole normalisé, nombres bruts. Le symbole affiché passe par normalizeSecuritySymbol (majuscules) alors que qty/cours/coût restent bruts non parsés. Léger manque de cohérence visuelle (choix défendable puisque la preview reflète la source).

  5. Pas de skip de préambule. Contrairement à autoDetectConfig, analyzeHoldingsCsv ne saute pas les lignes d'en-tête non tabulaires (relevés de courtier avec préambule « Compte : … »). La détection de colonnes/délimiteur peut alors dériver ; rattrapable via l'éditeur de mapping.

  6. Micro : data[0].map(normalizeHeaderCell) recalculé deux fois dans autoDetectHoldingColumns (step 1 + exclusion des colonnes « valeur »).

Aucun de ces points ne bloque le merge.

## /pr-review — PR #251 · feat(balance): CSV import of holdings in detailed snapshot (#245) **Verdict : APPROVE** ✅ ### Résumé Feature purement frontend, bien découpée : la modale possède le cycle de vie fichier/parse, tandis que la construction des `HoldingDraft` et le merge dans le panier vivent dans `useSnapshotEditor` (purs, testés). Le chemin de sauvegarde atomique reste inchangé — aucun SQL/Rust touché. La déduplication par symbole normalisé, à la fois **dans le CSV** (`holdingsFromCsvRows`) et **contre le panier existant** (reducer `IMPORT_HOLDINGS`), protège correctement la contrainte `UNIQUE(snapshot_line_id, security_id)`. i18n FR/EN complet (17 clés, jeux identiques), CHANGELOG mis à jour dans les deux langues, CI verte (frontend 2m25s + rust 22m45s), commit conventionnel, référence l'issue. Vérifié : boundary Tauri (`pick_import_file` filtre `Vec<(String, Vec<String>)>`, `detect_encoding`/`read_file_content` en `filePath` camelCase) conforme aux usages existants ; `category_asset_type` existe bien sur `BalanceAccountWithCategory` ; le nouveau prop requis `onImportHoldings` de `SnapshotEditor` est fourni par son unique appelant (`SnapshotEditPage`) ; couverture de tests solide (drafts, merge intra-CSV, reducer in-place/append, préservation du prix, bout-en-bout `buildDetailedLines` + gotcha no-price qui échoue à la validation de save). Aucun `skip`/`only`. ### Suggestions (non bloquantes) 1. **`autoDetectHoldingColumns` — fallback quantité ignore `used`** (`csvAutoDetect.ts`, ~L1442). Sur un CSV à 2 colonnes `Symbole,Cours` (sans colonne quantité), le fallback `quantity = symbol === 0 && colCount > 1 ? 1 : 0` force `quantity = 1`, or la colonne 1 est déjà `used` par `unit_price` → quantité **et** prix pointent la même colonne. Résultat : `holdingsFromCsvRows` lit le cours comme quantité. Cas dégénéré (CSV de positions sans quantité) et rattrapable via la preview, mais le défaut auto est trompeur. Idéalement, ne pas réutiliser une colonne déjà prise, ou surfacer un état « aucune colonne quantité détectée ». 2. **Quantité illisible avalée en 0** (`useSnapshotEditor.ts`, ~L819). `const qty = isNaN(qtyParsed) ? 0 : qtyParsed` : une cellule quantité type `"N/A"`/`"-"` importe une position à 0 au lieu de la signaler. La preview affiche la valeur brute, donc c'est rattrapable, mais un léger décalage preview↔import subsiste. 3. **Accumulation flottante au merge intra-CSV** (`useSnapshotEditor.ts`, ~L835). `existing.qty += qty` / `existing.book = (existing.book ?? 0) + book` peuvent produire des artefacts (`0.1 + 0.2 → 0.30000000000000004`) dans les strings `quantity`/`book_cost`. Cosmétique — la valeur est re-arrondie au cent au save — mais un arrondi à la construction rendrait l'affichage propre. 4. **Preview : symbole normalisé, nombres bruts.** Le symbole affiché passe par `normalizeSecuritySymbol` (majuscules) alors que qty/cours/coût restent bruts non parsés. Léger manque de cohérence visuelle (choix défendable puisque la preview reflète la source). 5. **Pas de skip de préambule.** Contrairement à `autoDetectConfig`, `analyzeHoldingsCsv` ne saute pas les lignes d'en-tête non tabulaires (relevés de courtier avec préambule « Compte : … »). La détection de colonnes/délimiteur peut alors dériver ; rattrapable via l'éditeur de mapping. 6. **Micro** : `data[0].map(normalizeHeaderCell)` recalculé deux fois dans `autoDetectHoldingColumns` (step 1 + exclusion des colonnes « valeur »). Aucun de ces points ne bloque le merge.
Author
Owner

Merged into main locally as part of the milestone stack (merge commit 2a3cc07). Closing manually — the merge was done outside Forgejo's API, so it can't flag this PR as merged. The change is on main.

Merged into `main` locally as part of the milestone stack (merge commit `2a3cc07`). Closing manually — the merge was done outside Forgejo's API, so it can't flag this PR as *merged*. The change is on `main`.
maximus closed this pull request 2026-07-05 21:06:20 +00:00
All checks were successful
PR Check / rust (pull_request) Successful in 22m45s
PR Check / frontend (pull_request) Successful in 2m25s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#251
No description provided.