Skip to content

feat(expenses): étape 1 — la ligne devient la cible, la suppression passe derrière une confirmation - #270

Merged
thierryvm merged 2 commits into
mainfrom
fix/refonte-01-depenses-filet-affordance
Jul 26, 2026
Merged

feat(expenses): étape 1 — la ligne devient la cible, la suppression passe derrière une confirmation#270
thierryvm merged 2 commits into
mainfrom
fix/refonte-01-depenses-filet-affordance

Conversation

@thierryvm

@thierryvm thierryvm commented Jul 26, 2026

Copy link
Copy Markdown
Owner

É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à — ExpenseEditDrawer fait 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 :

Supprimer « Intermarché » — 27 € ? Cette dépense sera définitivement effacée.

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.

formatDate ignorait le fuseau, contrairement à formatMonth vingt 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-reviewer l'a vu avant que j'écrive une ligne.

createExpenseAction laissait tomber paid_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

Ce qu'on retire Ce qui devient rouge
la ligne paid_from 2 specs
le prédicat date-only 3 specs date-only ; les 2 specs d'instants restent vertes
le retrait de la corbeille 1 spec structurelle

La 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

Mesure Résultat
Date pré-remplie 2026-07-26 — heure belge, pas UTC
Lignes après création 1
Suppression depuis la liste 0
Confirmation « Supprimer « Intermarché » — 27 € ? … »
Lignes avant / après suppression 1 → 0

npm run test 1702 / 1702 · typecheck 0 · lint 0 · coverage exit 0

⚠️ La CI ne prouvera pas ce parcours : le spec e2e est seededUser-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 createExpenseAction et deleteExpenseAction n'avaient aucun test. Faux : ils en avaient, pour le contrat revalidatePath. 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

…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.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ankora Ready Ready Preview, Comment Jul 26, 2026 4:03pm

@github-actions github-actions Bot added status:review-needed Ready for review type:feat New user-facing feature labels Jul 26, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

🧙 Sourcery est en train de relire votre pull request !


Conseils et commandes

Interagir avec Sourcery

  • Lancer une nouvelle relecture : Commentez @sourcery-ai review sur la pull request.
  • Poursuivre les discussions : Répondez directement aux commentaires de relecture de Sourcery.
  • Créer une issue GitHub à partir d’un commentaire de relecture : Demandez à Sourcery de créer une
    issue à partir d’un commentaire de relecture en y répondant. Vous pouvez également répondre à un
    commentaire de relecture avec @sourcery-ai issue pour créer une issue à partir de ce commentaire.
  • Générer un titre de pull request : Écrivez @sourcery-ai n’importe où dans le titre de la pull
    request pour générer un titre à tout moment. Vous pouvez aussi commenter
    @sourcery-ai title sur la pull request pour (re)générer le titre à tout moment.
  • Générer un résumé de pull request : Écrivez @sourcery-ai summary n’importe où dans
    le corps de la pull request pour générer un résumé de PR à tout moment exactement à l’endroit
    souhaité. Vous pouvez aussi commenter @sourcery-ai summary sur la pull request pour
    (re)générer le résumé à tout moment.
  • Générer le guide du relecteur : Commentez @sourcery-ai guide sur la pull
    request pour (re)générer le guide du relecteur à tout moment.
  • Résoudre tous les commentaires Sourcery : Commentez @sourcery-ai resolve sur la pull
    request pour résoudre tous les commentaires Sourcery. Utile si vous avez déjà
    traité tous les commentaires et ne souhaitez plus les voir.
  • Rendre tous les avis Sourcery caducs : Commentez @sourcery-ai dismiss sur la pull
    request pour invalider toutes les relectures Sourcery existantes. Particulièrement utile si vous
    voulez repartir de zéro avec une nouvelle relecture – n’oubliez pas de commenter
    @sourcery-ai review pour lancer une nouvelle relecture !

Personnaliser votre expérience

Accédez à votre tableau de bord pour :

  • Activer ou désactiver des fonctionnalités de relecture telles que le résumé de pull request
    généré par Sourcery, le guide du relecteur, et d’autres.
  • Changer la langue de relecture.
  • Ajouter, supprimer ou modifier des instructions de relecture personnalisées.
  • Ajuster d’autres paramètres de relecture.

Obtenir de l’aide

Original review guide in English

🧙 Sourcery is reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@thierryvm
thierryvm merged commit 5e18507 into main Jul 26, 2026
11 checks passed
@thierryvm
thierryvm deleted the fix/refonte-01-depenses-filet-affordance branch July 26, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:review-needed Ready for review type:feat New user-facing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant