Totaux revenus/dépenses incohérents entre le tableau de bord et les rapports de résultat (trois règles d'agrégation) #392

Open
opened 2026-10-06 23:43:58 +00:00 by maximus · 1 comment
Owner

Analyse /analyze du 2026-10-06, lue sur 8907eef (branche issue-390-manual-flag-profile-migrations, aucun écart avec main sur les fichiers cités). Décisions de Max consignées plus bas (D1 à D4).

Contexte

Les revenus, les dépenses et le résultat d'un même mois changent de valeur selon l'écran. Le corps d'origine décrivait trois règles d'agrégation. L'exploration en trouve six, et aucune ne traite de la même façon les transferts, les remboursements, les fractionnements et les transactions non classées. Dans le cas le plus grave, le comparable réel-vs-réel compte deux fois chaque montant fractionné.

Fractionnement (saveSplitAdjustment, src/services/transactionService.ts:398) : le parent garde son montant ; chaque part devient un enfant (parent_transaction_id, is_split = 1), plus un enfant de compensation positif dans la catégorie du parent. Aucune requête de rapport ne filtre parent_transaction_id : la compensation est la seule chose qui rend ces sommes justes, et toute règle qui écarte les montants positifs ou prend des ABS ligne à ligne la casse.

Les six règles actuelles

Règle Fonction (fichier) Formule Consommateurs
A. Signe du montant, sans catégorie fetchMonthlyFlows (reportService.ts:1185), fetchSeasonality (:1213), getMonthlyTrends (:30), totaux de transactionService.ts:256, getDashboardSummary (dashboardService.ts:12, sans appelant) revenus = SUM(amount > 0), dépenses = ABS(SUM(amount < 0)) KPI, sparklines, IncomeExpenseOverlayChart, saisonnalité (tableau de bord et Cartes) ; Tendances mensuelles ; barre de résumé de Transactions
B. Type, débits seulement pour les dépenses compareDeltaSql (reportService.ts:895) → getCompareMonthOverMonth / YearOverYear → compareResults.ts dépense = ABS(amount) ligne à ligne avec WHERE amount < 0 OR type IN (transfer, income) ; revenu et transfert signés Comparable réel-vs-réel ; top movers du tableau de bord et de Cartes (~:1445)
C. Type, magnitude par catégorie getCategoryOverTime (:74) → overTimeResults.ts ; getExpensesByCategory (dashboardService.ts:57) ABS(SUM(amount)) par catégorie ; net = revenus − dépenses + transferts en magnitude Tendance par catégorie ; graphiques de dépenses du tableau de bord
D. Type, solde signé par catégorie getActualsByCategoryRange (budgetService.ts:200) → getBudgetVsActualData (:224) → budgetVsActualResults.ts SUM(amount) signé Budget vs réel. Seule règle juste, mais la carte d'adhésion de Cartes (reportService.ts:1475-1490) la repasse en Math.abs, ce qui affiche un remboursement comme un dépassement
E. SUM(ABS(amount)) ligne à ligne, ni signe ni type getCategoryZoom (:1031, rollup :1067, enfants :1081, évolution :1104) une catégorie à +50 / −50 vaut 100 Zoom catégorie
F. Net signé + movers par débits, sans type getHighlights (:289) : net, YTD, sparkline en SUM(amount) (juste) ; top movers en ABS(amount) sur amount < 0 (:375-379) — Faits saillants (HubNetBalanceTile, HubTopMoversTile, HighlightsTopMoversTable) : transferts et débits de revenus mêlés aux dépenses

Sur l'exemple de reproduction (salaire +1000 ; épicerie −100 dont 30 fractionnés en Pharmacie ; remboursement +20 en Épicerie ; virement −500 / +500), avec une dépense réelle de 80 : A donne revenus 1550 / dépenses 630, B donne 1000 / 130, C et D donnent 1000 / 80. Rejoué en SQLite le 2026-10-06.

Décisions (Max, 2026-10-06)

  • D1 — Solde signé par catégorie, partout. Pour une catégorie : solde = SUM(amount) sur la période, fractionnements et remboursements compris. Revenu = somme des soldes des catégories income. Dépense = − somme des soldes des catégories expense, donc une catégorie de dépense créditrice compte comme une dépense négative. Transfert = somme signée des catégories transfer. C'est la règle D (budget) généralisée. Plus aucun ABS ni filtre de signe dans une agrégation de montants. ABS ne sert qu'à l'affichage et au tri.
  • D2 — Portée : les six règles convergent dans #392, en une seule PR (choix de Max contre le découpage proposé). La PR est donc large : à découper en commits par couche (harnais → agrégat commun → services → réducteurs → UI → docs).
  • D3 — Tests sur une vraie base : node:sqlite dans vitest. Pas de dépendance ajoutée (SQLite est intégré à Node depuis la 22.13, vérifié en local en 22.22). Prérequis : check-frontend.yml installe Node 20 (:41-47), où node:sqlite n'existe pas. Passer ce workflow en Node 22, ce qui règle la moitié frontale de #376 (audit.yml y reste).
  • D4 — Transactions non classées : section « Non classé » à part, signée, ni revenu ni dépense, mais comptée dans le résultat.
  • Retrait de getDashboardSummary (sans appelant).

Définitions qui en découlent (choix d'analyse, à contester en revue)

  • Résultat avant transferts = revenus − dépenses + non classé.
  • Résultat net = résultat avant transferts + transferts (signés) = somme de tous les montants de la période. C'est un invariant à tester.
  • Taux d'épargne = résultat avant transferts / revenus (null si revenus ≤ 0). Les transferts n'entrent plus ni au numérateur ni au dénominateur.
  • Le KPI « Solde net » affiche le résultat net. Une note sous les KPI Revenus / Dépenses signale un montant non classé non nul.
  • La barre de résumé de Transactions est une vue de liste filtrée, pas un état de résultat. Elle garde l'agrégation par signe, mais ses libellés deviennent « Entrées » / « Sorties » / « Solde ». Elle exclut déjà les enfants de fractionnement (parent_transaction_id IS NULL, transactionService.ts:226), ce qui reste cohérent.

Travail à faire

  • CI : check-frontend.yml passe en Node 22 (nodesource setup_22.x). Commenter sur #376 la part réglée.
  • Harnais src/test/sqliteHarness.ts : DatabaseSync(':memory:') qui charge src-tauri/src/database/consolidated_schema.sql, un adaptateur select / execute compatible avec getDb() (placeholders $1…, à vérifier à l'implémentation), et un constructeur du jeu de reproduction (catégories des trois types, fractionnement réel via saveSplitAdjustment, remboursement, transfert équilibré, transfert à une jambe, crédit non classé, catégorie de dépense créditrice). Supprimer l'avertissement ExperimentalWarning dans le setup vitest pour ne pas polluer la sortie.
  • Agrégat commun : un fragment SQL unique pour le solde signé par catégorie et par mois (type COALESCE(c.type, 'expense'), category_id IS NULL → non classé), aujourd'hui dupliqué partout. Un module pur incomeStatement.ts qui transforme ces soldes en revenus / dépenses / non classé / transferts / résultats.
  • Règle A : fetchMonthlyFlows, fetchSeasonality et getMonthlyTrends passent sur l'agrégat commun (jointure sur categories). Le taux d'épargne suit la définition ci-dessus. Supprimer getDashboardSummary et son type s'il n'est plus utilisé.
  • Règle B : compareDeltaSql en solde signé, sans le WHERE amount < 0. compareResults.computeResults : dépense = −solde, section non classée.
  • Règle C : getCategoryOverTime et getExpensesByCategory en solde signé. overTimeResults : transferts signés, section non classée.
  • Règle D : carte d'adhésion (reportService.ts:1475-1490), comparer des dépenses signées au lieu de Math.abs.
  • Règle E : getCategoryZoom (rollup, enfants, évolution) en solde signé. Garder ORDER BY ABS(t.amount) pour la liste des transactions (tri d'affichage).
  • Règle F : top movers de getHighlights, filtrés sur le type expense en solde signé, comme ceux de Cartes.
  • UI, valeurs négatives : une dépense ou une catégorie peut être négative. Dans les graphiques en parts ou en barres empilées (CategoryPieChart garde déjà total > 0, barres de dépenses du tableau de bord, IncomeExpenseOverlayChart), exclure ou borner à 0 avec une mention. Les tableaux affichent la valeur signée. Le tri des movers reste sur Math.abs(deltaAbs). Sections « Non classé » dans ComparePeriodTable et CategoryOverTimeTable.
  • i18n (FR + EN) : section « Non classé », note sous les KPI, libellés Entrées / Sorties / Solde de la barre de Transactions, mention des parts négatives exclues d'un graphique.
  • Docs : docs/guide-utilisateur.md:354-358 et :370 décrivent la règle A (à réécrire) et les clés docs.* correspondantes ; docs/architecture.md (agrégat commun, harnais) ; ADR « convention de signe des rapports » (D1, D4, définitions). CHANGELOG EN + FR sous [Unreleased] (Changed : les totaux changent pour tout utilisateur qui a des transferts, des remboursements ou des fractionnements ; Fixed : double comptage des fractionnements dans le comparable).

Fichiers concernés

  • src/services/reportService.ts : getMonthlyTrends, getCategoryOverTime, getHighlights, compareDeltaSql, getCategoryZoom, fetchMonthlyFlows, fetchSeasonality, getCartesSnapshot (KPI, adhésion, movers)
  • src/services/dashboardService.ts : getExpensesByCategory, retrait de getDashboardSummary
  • src/services/budgetService.ts : référence de la règle D, réutiliser l'agrégat commun si c'est simple
  • src/services/transactionService.ts : barre de résumé (libellés seulement)
  • src/components/reports/compareResults.ts, overTimeResults.ts, budgetVsActualResults.ts
  • Composants : ComparePeriodTable, CategoryOverTimeTable, IncomeExpenseOverlayChart, CategoryPieChart, cartes de Cartes (BudgetAdherenceCard, KPI), HubTopMoversTile, HighlightsTopMoversTable, TransactionSummaryBar, DashboardPage
  • src/shared/types/index.ts (types de résultats, DashboardSummary)
  • src/i18n/locales/fr.json, en.json ; docs/ ; CHANGELOG.md, CHANGELOG.fr.md ; .forgejo/workflows/check-frontend.yml

Surface de test

  • Existants : reportService.test.ts, reportService.cartes.test.ts, dashboardService.test.ts, budgetService.test.ts, transactionService.test.ts mockent ./db et vérifient le texte SQL par toContain, sur des lignes déjà agrégées : aucun ne peut rougir sur une règle d'agrégation. Les assertions de texte SQL qui épinglent ABS ou amount < 0 sont à retirer ou réécrire. compareResults.test.ts, overTimeResults.test.ts et budgetVsActualResults.test.ts testent des réducteurs purs : à étendre (transferts signés, non classé, dépense négative).
  • Nouveaux, sur node:sqlite : pour chaque fonction de service listée, le jeu de reproduction donne revenus 1000 et dépenses 80, et le même résultat net. Plus un test par cas : fractionnement, remboursement, transfert équilibré, transfert à une jambe (signe conservé dans le net), crédit non classé (section à part), catégorie de dépense créditrice (dépense négative), filtre accountIds.
  • Invariant : résultat net = SUM(amount) de toutes les transactions de la période (avec et sans fractionnement).
  • Régression requise (type:bug) : oui. Passe de mutations : remettre une à une les six règles d'origine (le WHERE amount < 0, un ABS(SUM), un SUM(ABS), le Math.abs de l'adhésion, le transfert en magnitude) ; chaque mutation doit rougir avec un code de sortie 1.

Critères d'acceptation

  • Toutes les agrégations de montants des rapports passent par l'agrégat commun en solde signé (D1). Il ne reste aucun ABS ni filtre de signe hors affichage et tri.
  • Sur le jeu de reproduction, le tableau de bord, Cartes, Tendances mensuelles, Tendance par catégorie, le comparable, les Faits saillants, le zoom et le budget affichent revenus 1000, dépenses 80 et le même résultat net.
  • Les transactions non classées forment une section à part, comptée dans le résultat, pas dans les revenus ni les dépenses (D4).
  • Le taux d'épargne ne contient plus de transferts.
  • Une dépense ou catégorie négative ne casse aucun graphique : exclue ou bornée avec mention, signée dans les tableaux.
  • Le filtre accountIds reste appliqué partout où il l'était (#273, #279).
  • Les tests node:sqlite passent en CI (Node 22), et chaque mutation de la passe rougit.
  • La barre de Transactions dit « Entrées / Sorties / Solde ».
  • CHANGELOG EN + FR, guide utilisateur (+ clés docs.*), docs/architecture.md et ADR à jour.

Complexité

Complexe. Environ 10 requêtes, 3 réducteurs, une quinzaine de composants, un harnais de test nouveau et un changement de CI. Les chiffres changent chez tous les utilisateurs qui ont des transferts, des remboursements ou des fractionnements.

Risques et points ouverts

  • node:sqlite est encore marqué expérimental, avec une API stable en pratique depuis Node 22.13. Si le harnais bute (placeholders $n, types de retour), le repli est better-sqlite3, à décider à ce moment-là.
  • Le SQL de tauri-plugin-sql (sqlx) et celui de node:sqlite passent par le même moteur SQLite, mais à des versions qui peuvent différer : n'utiliser aucune fonction récente.
  • Une dépense négative dans les graphiques est un choix de présentation : la règle « exclue ou bornée avec mention » est une interprétation d'analyse, à confirmer en revue sur captures.
  • useCartes.ts:81 n'envoie pas accountIds (Cartes n'a pas de filtre de comptes) : comportement voulu, hors portée.

Liens

Epic #260 (rapports uniformes), #253, #256, #279, #243, #376 (Node 20 en CI). Point de suivi v0.11.0 sur les nets de transfert signés.

> Analyse `/analyze` du 2026-10-06, lue sur `8907eef` (branche `issue-390-manual-flag-profile-migrations`, aucun écart avec `main` sur les fichiers cités). Décisions de Max consignées plus bas (D1 à D4). ## Contexte Les revenus, les dépenses et le résultat d'un même mois changent de valeur selon l'écran. Le corps d'origine décrivait trois règles d'agrégation. L'exploration en trouve **six**, et aucune ne traite de la même façon les transferts, les remboursements, les fractionnements et les transactions non classées. Dans le cas le plus grave, le comparable réel-vs-réel compte deux fois chaque montant fractionné. Fractionnement (`saveSplitAdjustment`, `src/services/transactionService.ts:398`) : le parent garde son montant ; chaque part devient un enfant (`parent_transaction_id`, `is_split = 1`), plus un **enfant de compensation positif** dans la catégorie du parent. Aucune requête de rapport ne filtre `parent_transaction_id` : la compensation est la seule chose qui rend ces sommes justes, et toute règle qui écarte les montants positifs ou prend des `ABS` ligne à ligne la casse. ## Les six règles actuelles | Règle | Fonction (fichier) | Formule | Consommateurs | |---|---|---|---| | A. Signe du montant, sans catégorie | `fetchMonthlyFlows` (`reportService.ts:1185`), `fetchSeasonality` (`:1213`), `getMonthlyTrends` (`:30`), totaux de `transactionService.ts:256`, `getDashboardSummary` (`dashboardService.ts:12`, **sans appelant**) | revenus = `SUM(amount > 0)`, dépenses = `ABS(SUM(amount < 0))` | KPI, sparklines, `IncomeExpenseOverlayChart`, saisonnalité (tableau de bord et Cartes) ; Tendances mensuelles ; barre de résumé de Transactions | | B. Type, débits seulement pour les dépenses | `compareDeltaSql` (`reportService.ts:895`) → `getCompareMonthOverMonth` / `YearOverYear` → `compareResults.ts` | dépense = `ABS(amount)` ligne à ligne avec `WHERE amount < 0 OR type IN (transfer, income)` ; revenu et transfert signés | Comparable réel-vs-réel ; top movers du tableau de bord et de Cartes (`~:1445`) | | C. Type, magnitude par catégorie | `getCategoryOverTime` (`:74`) → `overTimeResults.ts` ; `getExpensesByCategory` (`dashboardService.ts:57`) | `ABS(SUM(amount))` par catégorie ; net = revenus − dépenses **+ transferts en magnitude** | Tendance par catégorie ; graphiques de dépenses du tableau de bord | | D. Type, solde signé par catégorie | `getActualsByCategoryRange` (`budgetService.ts:200`) → `getBudgetVsActualData` (`:224`) → `budgetVsActualResults.ts` | `SUM(amount)` signé | Budget vs réel. **Seule règle juste**, mais la carte d'adhésion de Cartes (`reportService.ts:1475-1490`) la repasse en `Math.abs`, ce qui affiche un remboursement comme un dépassement | | E. `SUM(ABS(amount))` ligne à ligne, ni signe ni type | `getCategoryZoom` (`:1031`, rollup `:1067`, enfants `:1081`, évolution `:1104`) | une catégorie à +50 / −50 vaut 100 | Zoom catégorie | | F. Net signé + movers par débits, sans type | `getHighlights` (`:289`) : net, YTD, sparkline en `SUM(amount)` (juste) ; top movers en `ABS(amount)` sur `amount < 0` (`:375-379`) | — | Faits saillants (`HubNetBalanceTile`, `HubTopMoversTile`, `HighlightsTopMoversTable`) : transferts et débits de revenus mêlés aux dépenses | Sur l'exemple de reproduction (salaire +1000 ; épicerie −100 dont 30 fractionnés en Pharmacie ; remboursement +20 en Épicerie ; virement −500 / +500), avec une dépense réelle de 80 : A donne revenus 1550 / dépenses 630, B donne 1000 / **130**, C et D donnent 1000 / 80. Rejoué en SQLite le 2026-10-06. ## Décisions (Max, 2026-10-06) - **D1 — Solde signé par catégorie, partout.** Pour une catégorie : `solde = SUM(amount)` sur la période, fractionnements et remboursements compris. Revenu = somme des soldes des catégories `income`. Dépense = **−** somme des soldes des catégories `expense`, donc une catégorie de dépense créditrice compte comme une dépense **négative**. Transfert = somme signée des catégories `transfer`. C'est la règle D (budget) généralisée. Plus aucun `ABS` ni filtre de signe dans une agrégation de montants. `ABS` ne sert qu'à l'affichage et au tri. - **D2 — Portée : les six règles convergent dans #392**, en une seule PR (choix de Max contre le découpage proposé). La PR est donc large : à découper en commits par couche (harnais → agrégat commun → services → réducteurs → UI → docs). - **D3 — Tests sur une vraie base : `node:sqlite` dans vitest.** Pas de dépendance ajoutée (SQLite est intégré à Node depuis la 22.13, vérifié en local en 22.22). Prérequis : `check-frontend.yml` installe **Node 20** (`:41-47`), où `node:sqlite` n'existe pas. Passer ce workflow en Node 22, ce qui règle la moitié frontale de #376 (`audit.yml` y reste). - **D4 — Transactions non classées : section « Non classé » à part**, signée, ni revenu ni dépense, mais comptée dans le résultat. - **Retrait de `getDashboardSummary`** (sans appelant). ### Définitions qui en découlent (choix d'analyse, à contester en revue) - Résultat avant transferts = revenus − dépenses + non classé. - Résultat net = résultat avant transferts + transferts (signés) = somme de tous les montants de la période. C'est un invariant à tester. - Taux d'épargne = résultat avant transferts / revenus (`null` si revenus ≤ 0). Les transferts n'entrent plus ni au numérateur ni au dénominateur. - Le KPI « Solde net » affiche le résultat net. Une note sous les KPI Revenus / Dépenses signale un montant non classé non nul. - La barre de résumé de Transactions est une vue de liste filtrée, pas un état de résultat. Elle garde l'agrégation par signe, mais ses libellés deviennent « Entrées » / « Sorties » / « Solde ». Elle exclut déjà les enfants de fractionnement (`parent_transaction_id IS NULL`, `transactionService.ts:226`), ce qui reste cohérent. ## Travail à faire - [ ] **CI** : `check-frontend.yml` passe en Node 22 (nodesource `setup_22.x`). Commenter sur #376 la part réglée. - [ ] **Harnais** `src/test/sqliteHarness.ts` : `DatabaseSync(':memory:')` qui charge `src-tauri/src/database/consolidated_schema.sql`, un adaptateur `select` / `execute` compatible avec `getDb()` (placeholders `$1`…, à vérifier à l'implémentation), et un constructeur du jeu de reproduction (catégories des trois types, fractionnement réel via `saveSplitAdjustment`, remboursement, transfert équilibré, transfert à une jambe, crédit non classé, catégorie de dépense créditrice). Supprimer l'avertissement `ExperimentalWarning` dans le setup vitest pour ne pas polluer la sortie. - [ ] **Agrégat commun** : un fragment SQL unique pour le solde signé par catégorie et par mois (type `COALESCE(c.type, 'expense')`, `category_id IS NULL` → non classé), aujourd'hui dupliqué partout. Un module pur `incomeStatement.ts` qui transforme ces soldes en revenus / dépenses / non classé / transferts / résultats. - [ ] **Règle A** : `fetchMonthlyFlows`, `fetchSeasonality` et `getMonthlyTrends` passent sur l'agrégat commun (jointure sur `categories`). Le taux d'épargne suit la définition ci-dessus. Supprimer `getDashboardSummary` et son type s'il n'est plus utilisé. - [ ] **Règle B** : `compareDeltaSql` en solde signé, sans le `WHERE amount < 0`. `compareResults.computeResults` : dépense = −solde, section non classée. - [ ] **Règle C** : `getCategoryOverTime` et `getExpensesByCategory` en solde signé. `overTimeResults` : transferts signés, section non classée. - [ ] **Règle D** : carte d'adhésion (`reportService.ts:1475-1490`), comparer des dépenses signées au lieu de `Math.abs`. - [ ] **Règle E** : `getCategoryZoom` (rollup, enfants, évolution) en solde signé. Garder `ORDER BY ABS(t.amount)` pour la liste des transactions (tri d'affichage). - [ ] **Règle F** : top movers de `getHighlights`, filtrés sur le type `expense` en solde signé, comme ceux de Cartes. - [ ] **UI, valeurs négatives** : une dépense ou une catégorie peut être négative. Dans les graphiques en parts ou en barres empilées (`CategoryPieChart` garde déjà `total > 0`, barres de dépenses du tableau de bord, `IncomeExpenseOverlayChart`), exclure ou borner à 0 avec une mention. Les tableaux affichent la valeur signée. Le tri des movers reste sur `Math.abs(deltaAbs)`. Sections « Non classé » dans `ComparePeriodTable` et `CategoryOverTimeTable`. - [ ] **i18n** (FR + EN) : section « Non classé », note sous les KPI, libellés Entrées / Sorties / Solde de la barre de Transactions, mention des parts négatives exclues d'un graphique. - [ ] **Docs** : `docs/guide-utilisateur.md:354-358` et `:370` décrivent la règle A (à réécrire) et les clés `docs.*` correspondantes ; `docs/architecture.md` (agrégat commun, harnais) ; **ADR** « convention de signe des rapports » (D1, D4, définitions). CHANGELOG EN + FR sous `[Unreleased]` (Changed : les totaux changent pour tout utilisateur qui a des transferts, des remboursements ou des fractionnements ; Fixed : double comptage des fractionnements dans le comparable). ## Fichiers concernés - `src/services/reportService.ts` : `getMonthlyTrends`, `getCategoryOverTime`, `getHighlights`, `compareDeltaSql`, `getCategoryZoom`, `fetchMonthlyFlows`, `fetchSeasonality`, `getCartesSnapshot` (KPI, adhésion, movers) - `src/services/dashboardService.ts` : `getExpensesByCategory`, retrait de `getDashboardSummary` - `src/services/budgetService.ts` : référence de la règle D, réutiliser l'agrégat commun si c'est simple - `src/services/transactionService.ts` : barre de résumé (libellés seulement) - `src/components/reports/compareResults.ts`, `overTimeResults.ts`, `budgetVsActualResults.ts` - Composants : `ComparePeriodTable`, `CategoryOverTimeTable`, `IncomeExpenseOverlayChart`, `CategoryPieChart`, cartes de Cartes (`BudgetAdherenceCard`, KPI), `HubTopMoversTile`, `HighlightsTopMoversTable`, `TransactionSummaryBar`, `DashboardPage` - `src/shared/types/index.ts` (types de résultats, `DashboardSummary`) - `src/i18n/locales/fr.json`, `en.json` ; `docs/` ; `CHANGELOG.md`, `CHANGELOG.fr.md` ; `.forgejo/workflows/check-frontend.yml` ## Surface de test - **Existants** : `reportService.test.ts`, `reportService.cartes.test.ts`, `dashboardService.test.ts`, `budgetService.test.ts`, `transactionService.test.ts` mockent `./db` et vérifient le texte SQL par `toContain`, sur des lignes déjà agrégées : **aucun ne peut rougir sur une règle d'agrégation**. Les assertions de texte SQL qui épinglent `ABS` ou `amount < 0` sont à retirer ou réécrire. `compareResults.test.ts`, `overTimeResults.test.ts` et `budgetVsActualResults.test.ts` testent des réducteurs purs : à étendre (transferts signés, non classé, dépense négative). - **Nouveaux, sur `node:sqlite`** : pour chaque fonction de service listée, le jeu de reproduction donne revenus 1000 et dépenses 80, et le même résultat net. Plus un test par cas : fractionnement, remboursement, transfert équilibré, transfert à une jambe (signe conservé dans le net), crédit non classé (section à part), catégorie de dépense créditrice (dépense négative), filtre `accountIds`. - **Invariant** : résultat net = `SUM(amount)` de toutes les transactions de la période (avec et sans fractionnement). - **Régression requise** (type:bug) : oui. Passe de mutations : remettre une à une les six règles d'origine (le `WHERE amount < 0`, un `ABS(SUM)`, un `SUM(ABS)`, le `Math.abs` de l'adhésion, le transfert en magnitude) ; chaque mutation doit rougir avec un code de sortie 1. ## Critères d'acceptation - [ ] Toutes les agrégations de montants des rapports passent par l'agrégat commun en solde signé (D1). Il ne reste aucun `ABS` ni filtre de signe hors affichage et tri. - [ ] Sur le jeu de reproduction, le tableau de bord, Cartes, Tendances mensuelles, Tendance par catégorie, le comparable, les Faits saillants, le zoom et le budget affichent revenus 1000, dépenses 80 et le même résultat net. - [ ] Les transactions non classées forment une section à part, comptée dans le résultat, pas dans les revenus ni les dépenses (D4). - [ ] Le taux d'épargne ne contient plus de transferts. - [ ] Une dépense ou catégorie négative ne casse aucun graphique : exclue ou bornée avec mention, signée dans les tableaux. - [ ] Le filtre `accountIds` reste appliqué partout où il l'était (#273, #279). - [ ] Les tests `node:sqlite` passent en CI (Node 22), et chaque mutation de la passe rougit. - [ ] La barre de Transactions dit « Entrées / Sorties / Solde ». - [ ] CHANGELOG EN + FR, guide utilisateur (+ clés `docs.*`), `docs/architecture.md` et ADR à jour. ## Complexité **Complexe.** Environ 10 requêtes, 3 réducteurs, une quinzaine de composants, un harnais de test nouveau et un changement de CI. Les chiffres changent chez tous les utilisateurs qui ont des transferts, des remboursements ou des fractionnements. ## Risques et points ouverts - `node:sqlite` est encore marqué expérimental, avec une API stable en pratique depuis Node 22.13. Si le harnais bute (placeholders `$n`, types de retour), le repli est `better-sqlite3`, à décider à ce moment-là. - Le SQL de `tauri-plugin-sql` (sqlx) et celui de `node:sqlite` passent par le même moteur SQLite, mais à des versions qui peuvent différer : n'utiliser aucune fonction récente. - Une dépense négative dans les graphiques est un choix de présentation : la règle « exclue ou bornée avec mention » est une interprétation d'analyse, à confirmer en revue sur captures. - `useCartes.ts:81` n'envoie pas `accountIds` (Cartes n'a pas de filtre de comptes) : comportement voulu, hors portée. ## Liens Epic #260 (rapports uniformes), #253, #256, #279, #243, #376 (Node 20 en CI). Point de suivi v0.11.0 sur les nets de transfert signés.
maximus added the
status:triage
type:bug
source:human
labels 2026-10-06 23:43:58 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-10-07 00:00:05 +00:00
Author
Owner

Mise à jour de la décision D3 (Max, 2026-10-10, pendant l'analyse de #376) : la CI passe à Node 24, pas 22. node:sqlite existe en Node 24. La bascule des workflows (check-frontend, audit, release) est portée par #376. La tâche « CI : check-frontend.yml passe en Node 22 » de cette issue est donc remplacée par une dépendance : #376 doit être mergée avant (ou avec) #392. Le harnais node:sqlite doit tourner en local sous Node 22.22 et en CI sous Node 24 ; n'utiliser aucune API propre à l'une des deux versions.

Mise à jour de la décision D3 (Max, 2026-10-10, pendant l'analyse de #376) : la CI passe à **Node 24**, pas 22. `node:sqlite` existe en Node 24. La bascule des workflows (`check-frontend`, `audit`, `release`) est portée par **#376**. La tâche « CI : `check-frontend.yml` passe en Node 22 » de cette issue est donc remplacée par une dépendance : #376 doit être mergée avant (ou avec) #392. Le harnais `node:sqlite` doit tourner en local sous Node 22.22 et en CI sous Node 24 ; n'utiliser aucune API propre à l'une des deux versions.
Sign in to join this conversation.
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#392
No description provided.