feat(expenses): étape 1 — la ligne devient la cible, la suppression passe derrière une confirmation - #270
Merged
Conversation
…ropping paid_from
Three defects on the expenses path, none of them visible as an error, plus the
authorisation net that should have caught them.
The default date in the new-expense form came from
`new Date().toISOString().slice(0, 10)` — the UTC day. During Belgian summer
time, between midnight and 02:00 local, UTC is still on the previous day: the
form pre-filled YESTERDAY. On the first of the month that files the expense
under the previous month, where the current list will never show it, with no
signal that anything went wrong. ANKORA_TIMEZONE moves out of
workspace-snapshot into src/lib/date/tz.ts and becomes the single source; the
implementation is lifted verbatim from ChargesClient, which had already solved
this locally.
formatDate had no timezone while formatMonth, twenty lines below in the same
file, passes one. Expense dates are date-only values, and `new Date('2026-07-18')`
parses as midnight UTC, so the runtime zone rendered the 17th anywhere west of
Greenwich — wrong on screen, and a hydration mismatch between a Vercel server on
UTC and the browser.
The fix is narrow on purpose. A blanket UTC would have corrupted the three
callers that pass a real instant: the account deletion dates, both timestamptz.
A Belgian user requesting deletion late in the evening would have been shown an
erasure date off by one day — a legally binding figure, broken by an expenses
change. UTC therefore applies only to date-only strings. plan-reviewer caught
this before a line was written.
createExpenseAction listed every column except paid_from, which updateExpenseAction
honours. Nothing is broken today — no UI picks an account and the DB default
covers it — so this is a boundary fix, not a user-visible bug. Left alone it
would surface the day an account picker lands, on the create path only.
The net comes with them. tests/actions/expenses.test.ts already covered these two
actions, but for the revalidatePath contract, not for authorisation: nothing
proved that a caller without a session, without a membership, or over the rate
limit is turned away. Create is the most-used path and delete the only
irreversible one. The insert mock now captures its payload the way update
already did — without that there is no way to assert what create writes, which
is exactly where a dropped column hides.
Verified falsifiable rather than assumed: removing the paid_from line turns two
specs red, and removing the date-only predicate turns the three date-only
timezone specs red while the two instant specs stay green — pinning both halves
of the contract.
src/lib/actions/expenses.ts joins coverage with an 80 % per-glob floor. Adding it
under the global 90 % bar would have failed CI instead of raising the floor;
Server Actions are mostly guard clauses and one Supabase call, and the last
percent is the client's own error branches.
… a confirmation Editing an expense already worked. ExpenseEditDrawer is 217 lines, mounted, tested, operational — but its trigger was a text-muted-foreground pencil glued to a text-danger bin. The eye goes to the red one, and @Thierry spent weeks believing expenses could not be edited at all. An affordance defect, not missing code. The whole row is now the target, as a real <button> rather than an onClick on the <li>: keyboard focus, accessible name and the 44px target survive, and nothing interactive is nested inside it. Deleting moves into the drawer. It was the more dangerous half of the same layout — an irreversible action one stray tap away in a scrolling list, with no confirmation at all. It now takes two steps, and the confirmation names what is about to disappear rather than asking "are you sure?" about nothing in particular: Supprimer « Intermarché » — 27 € ? Soft delete with an undo window belongs to a later step; until then, naming the expense and its amount is the whole safety net. Four keys across five locales, with a parity test asserting both {label} and {amount} survive translation — a confirmation that names nothing is a confirmation nobody reads. deleteAria is reused rather than orphaned. The existing spec required the row-level delete buttons to exist, so it went red exactly as the plan review predicted. Rewritten to the new contract: one test pins the row as the edit target, another proves nothing destructive is reachable from the list. That second one is structural, and verified falsifiable — re-adding a delete button to the row turns it red. Verified in a live signed-in session on iPhone 14 rather than assumed: the form defaults to 2026-07-26 (Belgian time, not UTC), creation yields one row, zero delete buttons remain in the list, the confirmation renders with the label and the amount, and confirming takes the list from 1 to 0. The e2e spec is seededUser-gated and auto-skips in CI, where the service_role key is deliberately absent. Checked both ways — 1 passed locally with the secrets loaded, 1 skipped without — so a green pipeline is not evidence this path works.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
🧙 Sourcery est en train de relire votre pull request ! Conseils et commandesInteragir avec Sourcery
Personnaliser votre expérienceAccédez à votre tableau de bord pour :
Obtenir de l’aide
Original review guide in English🧙 Sourcery is reviewing your pull request! Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Étape 1 / 17 du plan de refonte. Trois bugs réels, le filet d'autorisation qui aurait dû les attraper, et l'affordance réparée.
Ce que tu verras
La ligne entière est cliquable. L'édition fonctionnait déjà —
ExpenseEditDrawerfait 217 lignes, monté et testé — mais son déclencheur était un crayon gris collé à une corbeille rouge. L'œil va au rouge, d'où ta conviction qu'on ne pouvait pas modifier une dépense.La corbeille quitte la liste. C'était la moitié la plus dangereuse : une action irréversible à un tap de travers dans une liste qui défile, sans aucune confirmation. Elle vit maintenant dans le drawer, en deux temps, avec une confirmation qui nomme ce qui disparaît :
Trois bugs qui ne levaient aucune erreur
La date pré-remplie était en UTC. Entre minuit et 2 h en heure d'été belge, le formulaire proposait hier. Le 1er du mois, ta dépense partait sur le mois précédent — invisible dans la liste, sans le moindre signal.
formatDateignorait le fuseau, contrairement àformatMonthvingt lignes plus bas dans le même fichier. Une date pouvait s'afficher avec un jour de retard, et diverger entre le serveur et le navigateur.Le correctif est volontairement étroit, et c'est le point le plus important de cette PR. Un fuseau UTC global aurait corrompu les dates de suppression de compte RGPD (
timestamptz) : un utilisateur belge demandant la suppression en soirée se serait vu annoncer une date d'effacement décalée d'un jour — donnée juridiquement engageante, cassée par une PR « dépenses ».plan-reviewerl'a vu avant que j'écrive une ligne.createExpenseActionlaissait tomberpaid_from. Pas visible aujourd'hui (aucune UI ne choisit de compte), donc correctif de frontière, pas bug utilisateur.Falsifiabilité — vérifiée, pas supposée
paid_fromLa deuxième ligne prouve les deux moitiés du contrat : le correctif s'applique aux dates et pas aux instants.
Parcours joué en direct, iPhone 14, session connectée
2026-07-26— heure belge, pas UTCnpm run test1702 / 1702 ·typecheck0 ·lint0 ·coverageexit 0seededUser-gated et s'auto-skippe faute de secrets. Vérifié dans les deux sens — 1 passed avec les secrets chargés, 1 skipped sans.Une correction que la revue m'a imposée
J'avais affirmé que
createExpenseActionetdeleteExpenseActionn'avaient aucun test. Faux : ils en avaient, pour le contratrevalidatePath. Ce qui manquait, ce sont les tests de frontière d'autorisation — rien ne prouvait qu'un appelant sans session ou sans membership était refusé.Hors périmètre, délibérément
Catégories (étape 6) · soft delete (étape 12) · refonte visuelle du formulaire (étape 11) · navigation.
Rapport complet :
docs/prs/PR-refonte-01-depenses-report.md