feat(reports): plumb accountIds[] account filter into 7 report services #281
No reviewers
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#281
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-273-account-filter-services"
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?
Resolves #273
Generated autonomously by /autopilot run of 2026-07-11
Summary
accountIds?: number[]filter that narrows results to a subset of import sources (transactions.source_id), via a parameterizedIN (...)clause — one bound placeholder per id.getCompareMonthOverMonth,getCompareYearOverYear,getBudgetVsActualData,getCartesSnapshot(forwarded to itsgetCompareMonthOverMonth+getBudgetVsActualDatasub-calls so the Cartes dashboard's top-movers and budget-adherence cards don't silently ignore an active filter).getMonthlyTrends,getCategoryOverTime,getExpensesByCategory(sourceId?: number→accountIds?: number[]). No production caller passed a scalarsourceIdtoday, so only test call-sites needed updating.inPlaceholders()helper (src/utils/sqlFilters.ts) generates the placeholder list so the “one$Nper id” bookkeeping lives in exactly one place — this is the firstIN ($n, $n+1, ...)pattern in the codebase.<FilterPanel>and wire it into Trends/Compare/Budget).SQL-safety note
Every
IN (...)clause is built from bound placeholders ($9,$10, …) with the ids pushed into the params array — neveraccountIds.join(',')interpolated into SQL text (CWE-89). The clause is only added whenaccountIds.length > 0; omitted/emptyaccountIdsproduces the exact same SQL as before this PR, pinned by a regression test per service.Test plan
npm run build(tsc + vite) greennpm testgreen: 745 → 773 (28 new tests:sqlFilters.test.tsnew,budgetService.test.tsnew,dashboardService.test.tsnew, extendedreportService.test.ts/reportService.cartes.test.ts)accountIdsis byte-identical to pre-#273 behaviorRevue
/pr-review— APPROVERésumé : Plomberie backend propre et sûre. Chaque service de rapport reçoit un filtre
accountIds?: number[]optionnel via unIN (...)entièrement paramétré (un placeholder lié par id), factorisé dans un helper partagéinPlaceholders(). Aucun appelant de production ne passe encore le filtre, et l'omettre laisse le SQL byte-identique au comportement pré-#273.Vérifications
Sécurité (CWE-89) — Aucune injection possible. Toutes les clauses
IN (...)sont bâties à partir de placeholders liés ($9, $10, …), les ids poussés dans le tableau de params ; jamais deaccountIds.join(',')interpolé dans le texte SQL. Les seuls.joindu diff sont" AND "(assemblage de clauses) et", "sur des chaînes$Ndéjà sûres.sqlFilters.test.tsasserte explicitement que les valeurs brutes n'apparaissent jamais dans la chaîne.Correction
getActualsByCategoryRangedémarre à$3(params.length + 1),compareDeltaSql()à$9(après les 8 bornes de date), les autres suiventparamIndexde façon cohérente.getCategoryOverTime: le filtre atteint les deux requêtes (top-catégories ET détail mensuel) via unwhereSQL/paramspartagé — pas d'incohérence entre les deux vues. LeLIMIT $paramIndexreste aligné après les placeholders de comptes.deltaParamsne pousseaccountIdsque si non vide ; l'omission garde SQL/params byte-identiques (régression : 8 params attendus).getCartesSnapshotforwarde bienaccountIdsà ses deux sous-rapports qui le supportent (getCompareMonthOverMonth+getBudgetVsActualData) — le caveat de review d'origine (dashboard ignorant silencieusement un filtre actif) est couvert par test.getMonthlyTrends,getCategoryOverTime,getExpensesByCategory) : aucun appelant de production ne passaitsourceId. Vérifié surmainET sur la branche head — tous les call-sites (useTrends,useDashboard,useCompare,useCartes,CompareBudgetView) restent à 2-3 args. Rien ne casse.Tests — 28 nouveaux tests ; régression par service (omis/vide → pas de clause
source_id, nombre de params inchangé) ; cas single / multi / vide / undefined couverts. Aucunskip/only.sqlFilters.test.ts,budgetService.test.ts,dashboardService.test.tsnouveaux ;reportService.test.ts/.cartes.test.tsétendus.Data / conventions — Aucune migration, aucun changement de schéma. CHANGELOG FR + EN présents. Commits conventionnels (
feat(reports):+docs(reports):, ce dernier corrigeant les références de commentaireCOMPARE_DELTA_SQL→compareDeltaSql()— bonne hygiène).Suggestions (non bloquantes)
getCartesSnapshot— top-movers et budget-adherence respectent le filtre, mais les séries KPI/sparkline/seasonality (fetchMonthlyFlows,fetchSeasonality) non. Explicitement documenté hors-scope, et sans UI de filtre encore branchée il n'y a aucune incohérence visible. À surveiller quand le contrôle de filtre débarquera (#274-#276) : le dashboard Cartes montrerait alors un scoping mixte selon la carte.if (accountPlaceholders) params.push(...)(budget /compareDeltaSql) vsif (accountIds && accountIds.length > 0) …(fonctions compare). Équivalents et corrects, juste légèrement inconsistants. Trivial.Note pile : #281 cible
issue-272-report-filters-hook, donccheck.yml(déclenché uniquement versmain) ne l'a pas validée. Rejouer build + vitest + cargo sur le tip cumulé de la pile avant merge.Merged into
mainvia local fast-forward (commitfe9ae01) as a linear stack. Forgejo doesn't flag non-API merges as merged; closing manually.Pull request closed