Totaux revenus/dépenses incohérents entre le tableau de bord et les rapports de résultat (trois règles d'agrégation) #392
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#392
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 filtreparent_transaction_id: la compensation est la seule chose qui rend ces sommes justes, et toute règle qui écarte les montants positifs ou prend desABSligne à ligne la casse.Les six règles actuelles
fetchMonthlyFlows(reportService.ts:1185),fetchSeasonality(:1213),getMonthlyTrends(:30), totaux detransactionService.ts:256,getDashboardSummary(dashboardService.ts:12, sans appelant)SUM(amount > 0), dépenses =ABS(SUM(amount < 0))IncomeExpenseOverlayChart, saisonnalité (tableau de bord et Cartes) ; Tendances mensuelles ; barre de résumé de TransactionscompareDeltaSql(reportService.ts:895) →getCompareMonthOverMonth/YearOverYear→compareResults.tsABS(amount)ligne à ligne avecWHERE amount < 0 OR type IN (transfer, income); revenu et transfert signés~:1445)getCategoryOverTime(:74) →overTimeResults.ts;getExpensesByCategory(dashboardService.ts:57)ABS(SUM(amount))par catégorie ; net = revenus − dépenses + transferts en magnitudegetActualsByCategoryRange(budgetService.ts:200) →getBudgetVsActualData(:224) →budgetVsActualResults.tsSUM(amount)signéreportService.ts:1475-1490) la repasse enMath.abs, ce qui affiche un remboursement comme un dépassementSUM(ABS(amount))ligne à ligne, ni signe ni typegetCategoryZoom(:1031, rollup:1067, enfants:1081, évolution:1104)getHighlights(:289) : net, YTD, sparkline enSUM(amount)(juste) ; top movers enABS(amount)suramount < 0(:375-379)HubNetBalanceTile,HubTopMoversTile,HighlightsTopMoversTable) : transferts et débits de revenus mêlés aux dépensesSur 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)
solde = SUM(amount)sur la période, fractionnements et remboursements compris. Revenu = somme des soldes des catégoriesincome. Dépense = − somme des soldes des catégoriesexpense, donc une catégorie de dépense créditrice compte comme une dépense négative. Transfert = somme signée des catégoriestransfer. C'est la règle D (budget) généralisée. Plus aucunABSni filtre de signe dans une agrégation de montants.ABSne sert qu'à l'affichage et au tri.node:sqlitedans 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.ymlinstalle Node 20 (:41-47), oùnode:sqliten'existe pas. Passer ce workflow en Node 22, ce qui règle la moitié frontale de #376 (audit.ymly reste).getDashboardSummary(sans appelant).Définitions qui en découlent (choix d'analyse, à contester en revue)
nullsi revenus ≤ 0). Les transferts n'entrent plus ni au numérateur ni au dénominateur.parent_transaction_id IS NULL,transactionService.ts:226), ce qui reste cohérent.Travail à faire
check-frontend.ymlpasse en Node 22 (nodesourcesetup_22.x). Commenter sur #376 la part réglée.src/test/sqliteHarness.ts:DatabaseSync(':memory:')qui chargesrc-tauri/src/database/consolidated_schema.sql, un adaptateurselect/executecompatible avecgetDb()(placeholders$1…, à vérifier à l'implémentation), et un constructeur du jeu de reproduction (catégories des trois types, fractionnement réel viasaveSplitAdjustment, remboursement, transfert équilibré, transfert à une jambe, crédit non classé, catégorie de dépense créditrice). Supprimer l'avertissementExperimentalWarningdans le setup vitest pour ne pas polluer la sortie.COALESCE(c.type, 'expense'),category_id IS NULL→ non classé), aujourd'hui dupliqué partout. Un module purincomeStatement.tsqui transforme ces soldes en revenus / dépenses / non classé / transferts / résultats.fetchMonthlyFlows,fetchSeasonalityetgetMonthlyTrendspassent sur l'agrégat commun (jointure surcategories). Le taux d'épargne suit la définition ci-dessus. SupprimergetDashboardSummaryet son type s'il n'est plus utilisé.compareDeltaSqlen solde signé, sans leWHERE amount < 0.compareResults.computeResults: dépense = −solde, section non classée.getCategoryOverTimeetgetExpensesByCategoryen solde signé.overTimeResults: transferts signés, section non classée.reportService.ts:1475-1490), comparer des dépenses signées au lieu deMath.abs.getCategoryZoom(rollup, enfants, évolution) en solde signé. GarderORDER BY ABS(t.amount)pour la liste des transactions (tri d'affichage).getHighlights, filtrés sur le typeexpenseen solde signé, comme ceux de Cartes.CategoryPieChartgarde 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 surMath.abs(deltaAbs). Sections « Non classé » dansComparePeriodTableetCategoryOverTimeTable.docs/guide-utilisateur.md:354-358et:370décrivent la règle A (à réécrire) et les clésdocs.*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 degetDashboardSummarysrc/services/budgetService.ts: référence de la règle D, réutiliser l'agrégat commun si c'est simplesrc/services/transactionService.ts: barre de résumé (libellés seulement)src/components/reports/compareResults.ts,overTimeResults.ts,budgetVsActualResults.tsComparePeriodTable,CategoryOverTimeTable,IncomeExpenseOverlayChart,CategoryPieChart, cartes de Cartes (BudgetAdherenceCard, KPI),HubTopMoversTile,HighlightsTopMoversTable,TransactionSummaryBar,DashboardPagesrc/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.ymlSurface de test
reportService.test.ts,reportService.cartes.test.ts,dashboardService.test.ts,budgetService.test.ts,transactionService.test.tsmockent./dbet vérifient le texte SQL partoContain, 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 épinglentABSouamount < 0sont à retirer ou réécrire.compareResults.test.ts,overTimeResults.test.tsetbudgetVsActualResults.test.tstestent des réducteurs purs : à étendre (transferts signés, non classé, dépense négative).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), filtreaccountIds.SUM(amount)de toutes les transactions de la période (avec et sans fractionnement).WHERE amount < 0, unABS(SUM), unSUM(ABS), leMath.absde l'adhésion, le transfert en magnitude) ; chaque mutation doit rougir avec un code de sortie 1.Critères d'acceptation
ABSni filtre de signe hors affichage et tri.accountIdsreste appliqué partout où il l'était (#273, #279).node:sqlitepassent en CI (Node 22), et chaque mutation de la passe rougit.docs.*),docs/architecture.mdet 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:sqliteest 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 estbetter-sqlite3, à décider à ce moment-là.tauri-plugin-sql(sqlx) et celui denode:sqlitepassent par le même moteur SQLite, mais à des versions qui peuvent différer : n'utiliser aucune fonction récente.useCartes.ts:81n'envoie pasaccountIds(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.
Mise à jour de la décision D3 (Max, 2026-10-10, pendant l'analyse de #376) : la CI passe à Node 24, pas 22.
node:sqliteexiste en Node 24. La bascule des workflows (check-frontend,audit,release) est portée par #376. La tâche « CI :check-frontend.ymlpasse en Node 22 » de cette issue est donc remplacée par une dépendance : #376 doit être mergée avant (ou avec) #392. Le harnaisnode:sqlitedoit tourner en local sous Node 22.22 et en CI sous Node 24 ; n'utiliser aucune API propre à l'une des deux versions.