Skip to content

feat(charges): watched marker + dashboard bills card rework (THI-329 PR-C) - #227

Merged
thierryvm merged 5 commits into
mainfrom
feat/thi-329-watched-marker
Jul 18, 2026
Merged

feat(charges): watched marker + dashboard bills card rework (THI-329 PR-C)#227
thierryvm merged 5 commits into
mainfrom
feat/thi-329-watched-marker

Conversation

@thierryvm

@thierryvm thierryvm commented Jul 18, 2026

Copy link
Copy Markdown
Owner

PR-C — Marqueur « à surveiller » + card factures du dashboard remaniée (THI-329)

Dernier gros morceau de l'epic « Factures cohérentes ». Le dashboard répond enfin au besoin exprimé : « Ce mois-ci » = les factures du mois non payées + le reste à payer, et « À surveiller » = uniquement les factures que TU as marquées (fini le bucket « Mois prochain » automatique et bruyant).

La card ProchainesFacturesCard, réécrite

  • « Ce mois-ci » : factures du mois non payées (ancrées via currentPeriodDueDate — une facture passée non payée = badge « En retard », triée en tête), max 5 par date, montant « Reste à payer » live en tête (même définition que le bandeau de la page charges → cohérence totale). Tout payé → état succès « Tout est payé ce mois ».
  • « À surveiller » : uniquement les factures marquées (is_watched) non dues ce mois, avec leur vraie prochaine échéance non payée. Hint éducatif quand rien n'est marqué.
  • THI-348 soldé : plus de text-danger sur teinte ni text-brand-700 (fails AA dark) — badge blanc sur danger plein (4.84:1 les 2 thèmes), chips neutres, liens text-brand-text.

Le marqueur

  • Migration 20260718000001 : charges.is_watched boolean not null default false (RLS table intacte).
  • toggleWatchAction : garde uuid → authz workspace (lecture + écriture filtrées) → rate-limit → audit CHARGE_WATCH_TOGGLED → revalidate.
  • Page charges : bouton Bookmark inline par ligne (44px mobile, aria-pressed, optimiste, rempli brand quand marqué).

Gouvernance

⚠️ plan-reviewer indisponible (limite de session API en cours de revue) — @Thierry a explicitement mandaté une self-review Fable 5 à la place (2026-07-18, « je te demande de vraiment tout analyser… avec ton regard de Fable 5 »). Plan committé : docs/plans/pr-c-watched-marker-dashboard.md. Analyse A→Z faite sur la chaîne complète (domaine → action → snapshot → UI).

Tests

9 card (nouveau contrat, incl. cap 5, non-duplication watched/ce-mois, badge dark-safe) + 5 action (authz cross-workspace, audit, uuid, erreurs DB) + 3 client + parité i18n ×5. Suite complète 1474 verte, typecheck + lint + build clean.

⚠️ Migration données prod au merge

Additive (default false), zéro perte, RLS intacte — mais c'est ton merge (garde-fou).

Smoke @Thierry (desktop + mobile + dark)

Dashboard : « Ce mois-ci » liste tes non-payées avec le reste à payer (tout payé aujourd'hui → état succès) ; marque une facture 🔖 sur /app/charges → elle apparaît dans « À surveiller » (si pas due ce mois) ; démarque → disparaît.

🤖 Generated with Claude Code

Summary by Sourcery

Refonte de la carte des factures à venir du tableau de bord pour l’aligner avec l’épopée « Factures cohérentes » en affichant les factures impayées du mois en cours et une liste de surveillance contrôlée par l’utilisateur, le tout reposant sur un nouveau marqueur de surveillance persistant sur les charges.

Nouvelles fonctionnalités :

  • Ajout d’un marqueur manuel « à surveiller » (watched) sur les charges, activable depuis la page des charges et affiché dans une section dédiée « à surveiller » sur le tableau de bord.
  • Introduction d’une carte des factures repensée sur le tableau de bord, mettant en avant les factures impayées dues ce mois-ci avec un total restant à payer en temps réel et un message pédagogique.

Corrections de bugs :

  • Amélioration du style des factures en retard sur le tableau de bord en utilisant des badges de danger compatibles avec le thème sombre au lieu de textes peu contrastés.

Améliorations :

  • Extension de l’instantané de l’espace de travail et des types de charges de domaine pour véhiculer le marqueur « à surveiller » tout en conservant les calculs de domaine inchangés.
Original summary in English

Summary by Sourcery

Rework the dashboard upcoming bills card to align with the "Factures cohérentes" epic by showing unpaid current-month bills and a user-controlled watchlist, backed by a new persisted watch marker on charges.

New Features:

  • Add a manual "à surveiller" (watched) marker on charges, toggled from the charges page and surfaced as a dedicated watched section on the dashboard.
  • Introduce a redesigned dashboard bills card that highlights unpaid bills due this month with a live remaining-to-pay total and educative messaging.

Bug Fixes:

  • Improve overdue bill styling on the dashboard to use dark-theme-safe danger badges instead of low-contrast text tones.

Enhancements:

  • Extend the workspace snapshot and domain charge types to carry the watched marker while keeping domain math unchanged.

…PR-C)

The dashboard finally answers @Thierry's P5 verbatim. ProchainesFacturesCard
drops the fragmented J-7/J-14/J-30 buckets for two sections:

- « Ce mois-ci »: the month's UNPAID bills (anchored via currentPeriodDueDate,
  overdue surfaced + sorted first), capped at 5 by date, headed by the live
  'reste à payer' (same definition as the charges-page banner). All paid →
  success state.
- « À surveiller »: ONLY manually flagged bills (is_watched) not already due
  this month, with their real next unpaid occurrence (nextUnpaidDueDate).
  Replaces the rejected automatic 'Mois prochain' bucket. Educational hint
  when nothing is flagged yet.

DATA: migration 20260718000001 adds charges.is_watched (boolean default false,
table-level RLS untouched). New Server Action toggleWatchAction (uuid guard,
workspace authz read+write, rate-limit, CHARGE_WATCH_TOGGLED audit,
revalidate). Snapshot + domain Charge + supabase types carry isWatched.

UI charges page: inline Bookmark toggle per row (44px mobile, aria-pressed,
useOptimistic, brand fill when flagged) between frequency and edit.

a11y (THI-348 SOLDÉ): the rewritten card drops text-danger-on-tint chips and
text-brand-700 — overdue is white on solid danger (4.84:1 both themes), day
chips are neutral surface tokens, links use dark-safe text-brand-text.

i18n x5: dashboard.upcomingBills.{thisMonth,thisMonthHint,thisMonthEmpty,
remainingLabel,watched,watchedHint,watchedEmptyHint}, buckets j7/j14/j30
removed; app.charges.{watchAria,unwatchAria,toastWatched,toastUnwatched};
errors.charges.watchFailed.

Tests: 9 card (new contract) + 5 action + 3 client watch + i18n parity.
Full suite 1474 green, typecheck + lint + build clean.

Review: plan-reviewer hit the session limit mid-review; @Thierry explicitly
mandated a Fable 5 self-review pass instead (2026-07-18) — plan committed in
docs/plans/pr-c-watched-marker-dashboard.md.
@vercel

vercel Bot commented Jul 18, 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 18, 2026 8:53pm

@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.

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

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Guide du·de la relecteur·rice

Remanie la carte des prochaines factures sur le tableau de bord en sections « Ce mois-ci » et « À surveiller » basées sur les charges de la période en cours et les charges marquées à surveiller. Introduit en parallèle une chaîne complète pour le marqueur « à surveiller » (colonne en base de données, snapshot, action serveur, bouton de favoris côté client, tests et i18n), tout en corrigeant les problèmes de contraste des couleurs du tableau de bord.

Diagramme de séquence pour le nouveau flux de bascule du marqueur « à surveiller »

sequenceDiagram
  actor User
  participant ChargesClient
  participant toggleWatchAction
  participant Supabase
  participant AuditLog
  participant Dashboard as ProchainesFacturesCard

  User->>ChargesClient: click Bookmark (onToggleWatch)
  ChargesClient->>ChargesClient: applyOptimisticWatched
  ChargesClient->>toggleWatchAction: toggleWatchAction(chargeId)

  toggleWatchAction->>toggleWatchAction: uuidSchema.safeParse(id)
  toggleWatchAction->>toggleWatchAction: authorizedWorkspace()
  toggleWatchAction->>toggleWatchAction: rateLimit("mutation", userKey)
  toggleWatchAction->>Supabase: createClient()
  toggleWatchAction->>Supabase: from("charges").select("is_watched").eq("id").eq("workspace_id").maybeSingle()
  Supabase-->>toggleWatchAction: charge.is_watched
  toggleWatchAction->>Supabase: from("charges").update({ is_watched: !is_watched }).eq("id").eq("workspace_id")
  Supabase-->>toggleWatchAction: update ok
  toggleWatchAction->>AuditLog: logAuditEvent(CHARGE_WATCH_TOGGLED)
  toggleWatchAction->>Dashboard: revalidateDashboard()
  toggleWatchAction->>Dashboard: revalidateAppPath("charges")
  toggleWatchAction-->>ChargesClient: { ok: true, data: { watched } }
  ChargesClient->>ChargesClient: toast.success(toastWatched/toastUnwatched)
  User-->>Dashboard: sees updated "À surveiller" section
Loading

Modifications par fichier

Changement Détails Fichiers
Remplacement de la logique des prochaines factures basée sur des « buckets » par une carte à deux sections « Ce mois-ci » et « À surveiller » pilotée par les helpers de dates d’échéance du domaine et les données du registre de paiements.
  • Suppression de l’usage de getUpcomingCharges et implémentation d’une dérivation explicite des lignes « ce mois-ci » et « à surveiller » à l’aide de currentPeriodDueDate/nextUnpaidDueDate et paymentKey
  • Introduction d’un modèle Row et d’un helper BillList pour un rendu cohérent des lignes de facture, incluant les badges de retard, les pastilles de différence en jours et l’alignement des montants
  • Ajout d’un helper diffInDays pour une différence de dates ancrée en UTC et ajustement des états vide/succès/indice de « surveillé » ainsi que du style des liens pour répondre aux nouvelles exigences UX et accessibilité
  • Réécriture des tests de ProchainesFacturesCard pour valider le nouveau contrat : comportement « ce mois-ci » (limite à 5, montant restant, tri avec les factures en retard, état tout-payé) et comportement de la section « à surveiller » (non-duplication, indices, états vides)
src/components/dashboard/ProchainesFacturesCard.tsx
src/components/dashboard/__tests__/ProchainesFacturesCard.test.tsx
Ajout d’un marqueur manuel « à surveiller » pour les charges, connecté depuis le schéma de base de données jusqu’aux types de snapshot/domaine, à une nouvelle action serveur et à un toggle d’UI côté client.
  • Introduction d’une colonne booléenne is_watched sur charges via une migration et intégration dans les types générés par Supabase et dans le mapping du snapshot d’espace de travail sous le nom isWatched
  • Extension des types Charge/domaine et RawCharge/client avec un champ isWatched optionnel/requis tout en gardant la logique métier agnostique par rapport à ce champ
  • Implémentation de toggleWatchAction avec validation UUID, autorisation d’espace de travail, limitation de débit, bascule lecture-puis-mise-à-jour protégée par RLS, journalisation d’audit (CHARGE_WATCH_TOGGLED) et revalidation du tableau de bord/des charges
  • Mise à jour de ChargesClient pour maintenir un ensemble des charges surveillées en mode optimiste, appeler toggleWatchAction, rendre un bouton de favoris par ligne avec aria-pressed/libellés appropriés, et afficher des toasts de succès/erreur
  • Ajout de tests unitaires pour toggleWatchAction couvrant le cas nominal, déjà surveillé, les IDs invalides, les échecs d’autorisation inter-espaces de travail et les erreurs BD, ainsi que des tests côté client pour l’initialisation/la bascule de l’état « surveillé » et la parité des clés i18n
supabase/migrations/20260718000001_charges_is_watched.sql
src/lib/supabase/types.ts
src/lib/data/workspace-snapshot.ts
src/lib/domain/types.ts
src/lib/security/audit-log.ts
src/lib/actions/charges.ts
src/lib/actions/__tests__/charges.test.ts
src/app/[locale]/app/charges/ChargesClient.tsx
src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx
Mise à jour des messages i18n, constantes d’audit et de la documentation pour prendre en charge les nouvelles sémantiques du tableau de bord et la fonctionnalité de marqueur « à surveiller ».
  • Ajout/mise à jour des clés i18n dashboard.upcomingBills et app.charges dans toutes les locales prises en charge pour les nouveaux libellés de section, indices, texte aria et toasts, et suppression des anciens libellés de buckets devenus obsolètes
  • Extension de AuditEvent avec CHARGE_WATCH_TOGGLED et mise à jour des tests pour le mocker, afin d’aligner la journalisation avec la nouvelle action
  • Documentation de la conception, du périmètre et des risques du PR-C dans un fichier de plan dédié décrivant le nouveau comportement du tableau de bord et la gouvernance du marqueur
messages/fr-BE.json
messages/en.json
messages/nl-BE.json
messages/de-DE.json
messages/es-ES.json
src/lib/security/audit-log.ts
docs/plans/pr-c-watched-marker-dashboard.md

Conseils et commandes

Interagir avec Sourcery

  • Déclencher une nouvelle revue : Commentez @sourcery-ai review sur la pull request.
  • Poursuivre les discussions : Répondez directement aux commentaires de revue de Sourcery.
  • Générer une issue GitHub à partir d’un commentaire de revue : Demandez à Sourcery de créer une issue à partir d’un commentaire de revue en y répondant. Vous pouvez aussi répondre à un commentaire de revue avec @sourcery-ai issue pour créer une issue à partir de celui-ci.
  • 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 également 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 où vous le souhaitez. Vous pouvez également commenter @sourcery-ai summary sur la pull request pour (re)générer le résumé à tout moment.
  • Générer le guide du·de la relecteur·rice : Commentez @sourcery-ai guide sur la pull request pour (re)générer le guide du·de la relecteur·rice à 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 voulez plus les voir.
  • Ignorer toutes les revues Sourcery : Commentez @sourcery-ai dismiss sur la pull request pour ignorer toutes les revues Sourcery existantes. Particulièrement utile si vous voulez repartir de zéro avec une nouvelle revue — n’oubliez pas de commenter
    @sourcery-ai review pour déclencher une nouvelle revue !

Personnaliser votre expérience

Accédez à votre tableau de bord pour :

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

Obtenir de l’aide

Original review guide in English

Reviewer's Guide

Reworks the dashboard upcoming bills card into "Ce mois-ci" and "À surveiller" sections driven by current-period and watched charges, and introduces a full watched-marker pipeline (DB column, snapshot, server action, client bookmark button, tests, and i18n) while fixing dashboard color contrast issues.

Sequence diagram for the new watched marker toggle flow

sequenceDiagram
  actor User
  participant ChargesClient
  participant toggleWatchAction
  participant Supabase
  participant AuditLog
  participant Dashboard as ProchainesFacturesCard

  User->>ChargesClient: click Bookmark (onToggleWatch)
  ChargesClient->>ChargesClient: applyOptimisticWatched
  ChargesClient->>toggleWatchAction: toggleWatchAction(chargeId)

  toggleWatchAction->>toggleWatchAction: uuidSchema.safeParse(id)
  toggleWatchAction->>toggleWatchAction: authorizedWorkspace()
  toggleWatchAction->>toggleWatchAction: rateLimit("mutation", userKey)
  toggleWatchAction->>Supabase: createClient()
  toggleWatchAction->>Supabase: from("charges").select("is_watched").eq("id").eq("workspace_id").maybeSingle()
  Supabase-->>toggleWatchAction: charge.is_watched
  toggleWatchAction->>Supabase: from("charges").update({ is_watched: !is_watched }).eq("id").eq("workspace_id")
  Supabase-->>toggleWatchAction: update ok
  toggleWatchAction->>AuditLog: logAuditEvent(CHARGE_WATCH_TOGGLED)
  toggleWatchAction->>Dashboard: revalidateDashboard()
  toggleWatchAction->>Dashboard: revalidateAppPath("charges")
  toggleWatchAction-->>ChargesClient: { ok: true, data: { watched } }
  ChargesClient->>ChargesClient: toast.success(toastWatched/toastUnwatched)
  User-->>Dashboard: sees updated "À surveiller" section
Loading

File-Level Changes

Change Details Files
Replaced bucket-based upcoming bills logic with a two-section "Ce mois-ci" and "À surveiller" card driven by domain due-date helpers and payment ledger data.
  • Removed usage of getUpcomingCharges and implemented explicit this-month and watched-row derivation using currentPeriodDueDate/nextUnpaidDueDate and paymentKey
  • Introduced a Row model and BillList helper for consistent rendering of bill rows, including overdue badges, day-difference chips, and aligned amounts
  • Added a diffInDays helper for UTC-anchored date difference and adjusted empty/success/watched-hint states and link styling to match new UX and accessibility requirements
  • Rewrote ProchainesFacturesCard tests to validate the new contract: this-month behavior (cap 5, remaining amount, overdue ordering, all-paid state) and watched-section behavior (non-duplication, hints, empty states)
src/components/dashboard/ProchainesFacturesCard.tsx
src/components/dashboard/__tests__/ProchainesFacturesCard.test.tsx
Added a manual "à surveiller" marker for charges, wired from database schema through snapshot/domain types to a new server action and client UI toggle.
  • Introduced an is_watched boolean column on charges via a migration and wired it into Supabase generated types and workspace snapshot mapping as isWatched
  • Extended the Charge/domain and RawCharge/client types with an optional/required isWatched field while keeping domain math agnostic
  • Implemented toggleWatchAction with UUID validation, workspace authz, rate limiting, RLS-backed read-then-update flip, audit logging (CHARGE_WATCH_TOGGLED), and dashboard/charges revalidation
  • Updated ChargesClient to maintain an optimistic watched set, call toggleWatchAction, render a bookmark button per row with proper aria-pressed/labels, and show success/error toasts
  • Added unit tests for toggleWatchAction covering happy path, already-watched, invalid IDs, cross-workspace authz failures, and DB errors, plus client tests for seeding/toggling watch state and i18n key parity
supabase/migrations/20260718000001_charges_is_watched.sql
src/lib/supabase/types.ts
src/lib/data/workspace-snapshot.ts
src/lib/domain/types.ts
src/lib/security/audit-log.ts
src/lib/actions/charges.ts
src/lib/actions/__tests__/charges.test.ts
src/app/[locale]/app/charges/ChargesClient.tsx
src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx
Updated i18n messages, audit constants, and documentation to support the new dashboard semantics and watched marker feature.
  • Added/updated dashboard.upcomingBills and app.charges i18n keys in all supported locales for the new section labels, hints, aria text, and toasts, and removed now-obsolete bucket labels
  • Expanded AuditEvent with CHARGE_WATCH_TOGGLED and ensured tests mock it, aligning logging with the new action
  • Documented the PR-C design, scope, and risks in a dedicated plan file describing the new dashboard behavior and marker governance
messages/fr-BE.json
messages/en.json
messages/nl-BE.json
messages/de-DE.json
messages/es-ES.json
src/lib/security/audit-log.ts
docs/plans/pr-c-watched-marker-dashboard.md

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.

Hey - j’ai trouvé 1 problème et laissé quelques retours de haut niveau :

  • Dans ChargesClient et sa méthode onToggleWatch, l’appel optimiste à applyOptimisticWatched n’est jamais annulé quand toggleWatchAction échoue, donc l’UI peut rester bloquée dans un état différent de celui du serveur ; envisage de revenir en arrière sur le basculement optimiste (ou d’éviter l’optimisme) en cas d’erreur afin de garder l’ensemble synchronisé avec le backend.
  • Dans ProchainesFacturesCard, hasAnyDue est calculé à partir de toutes les factures dues ce mois-ci alors que le compteur affiché utilise uniquement les lignes impayées ; si toutes les factures sont payées, on obtient un chip itemCount à « 0 » à côté de l’état de succès « Tout est payé », ce qui peut être déroutant — aligner hasAnyDue sur les lignes impayées (ou cacher le chip lorsque l’état de succès est affiché) simplifierait l’UX.
Prompt pour les agents IA
Please address the comments from this code review:

## Overall Comments
- In `ChargesClient`’s `onToggleWatch`, the optimistic `applyOptimisticWatched` call is never reverted when `toggleWatchAction` fails, so the UI can get stuck in a different state from the server; consider reverting the optimistic flip (or avoiding optimism) on error to keep the set in sync with the backend.
- In `ProchainesFacturesCard`, `hasAnyDue` is computed from all due-this-month charges while the displayed count uses only unpaid rows; if all bills are paid, this yields a "0" itemCount chip next to the "Tout est payé" success state, which might be confusing—aligning `hasAnyDue` with unpaid rows (or hiding the chip when the success state shows) would simplify the UX.

## Individual Comments

### Comment 1
<location path="src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx" line_range="552-561" />
<code_context>
+  it('seeds the watch button state from isWatched and calls toggleWatchAction on tap', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting optimistic watch state and the thrown-error path for `toggleWatchAction`.

Your current tests cover initial `aria-pressed` seeding, action wiring, and success/error toasts, but two contract behaviors are still unverified:

1) Because `useOptimistic` is used, the button/icon should toggle immediately on click, before the server responds. You can assert that `aria-pressed` flips right after `fireEvent.click`, independent of the mocked result.

2) `onToggleWatch` has a `catch` branch that handles thrown errors differently from `{ ok: false }`. Adding a test where `toggleWatchMock` rejects would exercise this path and confirm we still show the `watchFailed` toast instead of silently swallowing exceptions.

Suggested implementation:

```typescript
  it('seeds the watch button state from isWatched, optimistically toggles, and calls toggleWatchAction on tap', async () => {
    toggleWatchMock.mockResolvedValue({ ok: true, data: { watched: true } });

    renderCharges([monthlyCharge], { currentPeriod: { year: 2026, month: 1 } });

    const watchBtn = screen.getByTestId(`charges-row-watch-${monthlyCharge.id}`);

    // initial state is seeded from isWatched
    expect(watchBtn).toHaveAttribute('aria-pressed', 'false');

    await act(async () => {
      fireEvent.click(watchBtn);

      // useOptimistic should flip the pressed state immediately on click
      expect(watchBtn).toHaveAttribute('aria-pressed', 'true');
    });

    await waitFor(() =>
      expect(toggleWatchMock).toHaveBeenCalledWith(monthlyCharge.id),
    );
    await waitFor(() => expect(toastSuccessMock).toHaveBeenCalled());
  });

  it('optimistically toggles watch state and shows watchFailed toast when toggleWatchAction rejects', async () => {
    const error = new Error('network error');
    toggleWatchMock.mockRejectedValueOnce(error);

    renderCharges([monthlyCharge], { currentPeriod: { year: 2026, month: 1 } });

    const watchBtn = screen.getByTestId(`charges-row-watch-${monthlyCharge.id}`);

    expect(watchBtn).toHaveAttribute('aria-pressed', 'false');

    await act(async () => {
      fireEvent.click(watchBtn);

      // even when the action rejects, we still optimistically toggle
      expect(watchBtn).toHaveAttribute('aria-pressed', 'true');
    });

    await waitFor(() =>
      expect(toggleWatchMock).toHaveBeenCalledWith(monthlyCharge.id),
    );

    await waitFor(() =>
      expect(toastErrorMock).toHaveBeenCalledWith(
        expect.objectContaining({ id: 'watchFailed' }),
      ),
    );
  });

```

If your `toastErrorMock` signature or message shape differs (for example, it receives just a string key or a full message object), adjust the final `expect(toastErrorMock)...` accordingly to match how the `watchFailed` toast is actually invoked in your production code.  
Also ensure `toastErrorMock` is defined in this test file and wired to whatever toast system the component uses for error paths; if not, add a corresponding jest mock similar to `toastSuccessMock`.
</issue_to_address>

Sourcery est gratuit pour l’open source – si nos revues vous plaisent, pensez à les partager ✨
Aidez-moi à être plus utile ! Cliquez sur 👍 ou 👎 sur chaque commentaire et j’utiliserai ces retours pour améliorer mes revues.
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In ChargesClient’s onToggleWatch, the optimistic applyOptimisticWatched call is never reverted when toggleWatchAction fails, so the UI can get stuck in a different state from the server; consider reverting the optimistic flip (or avoiding optimism) on error to keep the set in sync with the backend.
  • In ProchainesFacturesCard, hasAnyDue is computed from all due-this-month charges while the displayed count uses only unpaid rows; if all bills are paid, this yields a "0" itemCount chip next to the "Tout est payé" success state, which might be confusing—aligning hasAnyDue with unpaid rows (or hiding the chip when the success state shows) would simplify the UX.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ChargesClient`’s `onToggleWatch`, the optimistic `applyOptimisticWatched` call is never reverted when `toggleWatchAction` fails, so the UI can get stuck in a different state from the server; consider reverting the optimistic flip (or avoiding optimism) on error to keep the set in sync with the backend.
- In `ProchainesFacturesCard`, `hasAnyDue` is computed from all due-this-month charges while the displayed count uses only unpaid rows; if all bills are paid, this yields a "0" itemCount chip next to the "Tout est payé" success state, which might be confusing—aligning `hasAnyDue` with unpaid rows (or hiding the chip when the success state shows) would simplify the UX.

## Individual Comments

### Comment 1
<location path="src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx" line_range="552-561" />
<code_context>
+  it('seeds the watch button state from isWatched and calls toggleWatchAction on tap', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting optimistic watch state and the thrown-error path for `toggleWatchAction`.

Your current tests cover initial `aria-pressed` seeding, action wiring, and success/error toasts, but two contract behaviors are still unverified:

1) Because `useOptimistic` is used, the button/icon should toggle immediately on click, before the server responds. You can assert that `aria-pressed` flips right after `fireEvent.click`, independent of the mocked result.

2) `onToggleWatch` has a `catch` branch that handles thrown errors differently from `{ ok: false }`. Adding a test where `toggleWatchMock` rejects would exercise this path and confirm we still show the `watchFailed` toast instead of silently swallowing exceptions.

Suggested implementation:

```typescript
  it('seeds the watch button state from isWatched, optimistically toggles, and calls toggleWatchAction on tap', async () => {
    toggleWatchMock.mockResolvedValue({ ok: true, data: { watched: true } });

    renderCharges([monthlyCharge], { currentPeriod: { year: 2026, month: 1 } });

    const watchBtn = screen.getByTestId(`charges-row-watch-${monthlyCharge.id}`);

    // initial state is seeded from isWatched
    expect(watchBtn).toHaveAttribute('aria-pressed', 'false');

    await act(async () => {
      fireEvent.click(watchBtn);

      // useOptimistic should flip the pressed state immediately on click
      expect(watchBtn).toHaveAttribute('aria-pressed', 'true');
    });

    await waitFor(() =>
      expect(toggleWatchMock).toHaveBeenCalledWith(monthlyCharge.id),
    );
    await waitFor(() => expect(toastSuccessMock).toHaveBeenCalled());
  });

  it('optimistically toggles watch state and shows watchFailed toast when toggleWatchAction rejects', async () => {
    const error = new Error('network error');
    toggleWatchMock.mockRejectedValueOnce(error);

    renderCharges([monthlyCharge], { currentPeriod: { year: 2026, month: 1 } });

    const watchBtn = screen.getByTestId(`charges-row-watch-${monthlyCharge.id}`);

    expect(watchBtn).toHaveAttribute('aria-pressed', 'false');

    await act(async () => {
      fireEvent.click(watchBtn);

      // even when the action rejects, we still optimistically toggle
      expect(watchBtn).toHaveAttribute('aria-pressed', 'true');
    });

    await waitFor(() =>
      expect(toggleWatchMock).toHaveBeenCalledWith(monthlyCharge.id),
    );

    await waitFor(() =>
      expect(toastErrorMock).toHaveBeenCalledWith(
        expect.objectContaining({ id: 'watchFailed' }),
      ),
    );
  });

```

If your `toastErrorMock` signature or message shape differs (for example, it receives just a string key or a full message object), adjust the final `expect(toastErrorMock)...` accordingly to match how the `watchFailed` toast is actually invoked in your production code.  
Also ensure `toastErrorMock` is defined in this test file and wired to whatever toast system the component uses for error paths; if not, add a corresponding jest mock similar to `toastSuccessMock`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx Outdated
React 19 flushes the useOptimistic update asynchronously and reconciles to
the base once the transition settles — the flip is only observable while the
action promise is PENDING. Sourcery's inline-assert form was red; this holds
the promise, asserts the optimistic state, then resolves.
…ailures

Incident 2026-07-18: the #227 preview (code SELECTing is_watched) hit the
prod schema (column not migrated yet) — the whole charges query failed, the
'?? []' fallback swallowed it, and the dashboard + charges page rendered
empty, looking exactly like data loss (all 23 rows were intact, verified
read-only).

Two defenses:
- charges SELECT switches to '*': whatever columns exist are returned, the
  mapping defaults missing ones (isWatched ?? false) — a deploy/migration
  ordering window can never blank the page again.
- a failing charges read is now log.error'ed instead of silently coerced to
  an empty list.
@thierryvm
thierryvm merged commit 25a7464 into main Jul 18, 2026
9 checks passed
@thierryvm
thierryvm deleted the feat/thi-329-watched-marker branch July 18, 2026 21:22
thierryvm added a commit that referenced this pull request Jul 20, 2026
… domain (épic PR-1) (#233)

## PR-1 — Fondations « Engagements » (dettes, échéanciers, factures
futures)

Première brique de l'épic issu de tes retours sur ton Coda : *« pas les
factures futures, les dettes liées à des crédits, ou un remboursement
différé des impôts suite à un arrangement avec le SPF »*.

**Cette PR ne change RIEN de visible** — schéma + domaine + contrats
uniquement. L'UI arrive en PR-2.

### Le modèle : un seul objet pour tes 3 besoins
| | `debt` (crédit voiture) | `installment_plan` (SPF) | `one_off`
(facture future) |
|---|---|---|---|
| Ce que tu saisis | reste 4 200 € · 17 × 250 € | reste 1 600 € · 8 ×
200 € | 340 € en octobre |
| Ce qu'Ankora affiche (PR-2) | barre de progression + solde | « 3/8
payées · reste 1 000 € » | « 340 € le 12 oct. » |

**Tout est dérivé** (date de fin, solde restant, progression) — jamais
stocké, donc jamais faux. Les paiements réutilisent exactement le
mécanisme des factures : cocher une échéance = le geste que tu connais
déjà.

### Contenu
- **Migration** `commitments` + `commitment_payments` : RLS, policies,
index et trigger clonés 1:1 sur `charges`/`charge_payments`. Deux CHECKs
encodent les invariants. **Additive**, aucune table existante touchée.
- **Domaine pur** : `installmentPeriods`, `endPeriod`, `isDueInPeriod`,
`remainingBalance`, `installmentsPaid`, `isFinished`. Le solde est borné
à [0, total] : la dernière échéance absorbe l'arrondi (3 × 33,33 € sur
100 € tombe pile sur 0) et une coche parasite hors échéancier ne peut
pas fausser la progression.
- **Contrats Zod** miroirs des CHECKs SQL (la base ne peut pas refuser
ce que le schéma a accepté).
- **`supabase/types.ts` régénéré depuis la prod** via le CLI — remplace
l'édition manuelle de #227.

### Décisions verrouillées avec toi (spec :
`docs/plans/epic-dettes-echeanciers-spec.md`)
Nom « **Engagements** » · saisie « **solde restant + échéances
restantes** » · **bouton « convertir »** depuis une charge (PR-2) ·
**facture ponctuelle incluse**.

### Migration prod
**Déjà appliquée** (`supabase db push`, ledger vérifié). Comme aucune UI
ne la consomme, la prod est inchangée dans les deux sens.

### Tests
22 nouveaux (16 domaine + 6 schéma), suite complète **1513 verte**,
typecheck/lint/build clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

## Summary by Sourcery

Introduire un modèle de données fondamental et une logique métier pour
des engagements financiers finis (dettes, plans de paiement échelonné et
factures ponctuelles futures) sans aucun changement d’interface
utilisateur.

Nouvelles fonctionnalités :
- Ajouter les nouvelles tables `commitments` et `commitment_payments`,
ainsi que les types Supabase associés et les politiques RLS, afin de
modéliser les engagements financiers finis et leurs paiements.
- Définir des schémas Zod pour valider les engagements en conformité
avec les contraintes de la base de données.
- Ajouter des fonctions de domaine pures pour calculer les calendriers
d’engagement, les soldes restants et le statut de complétion.

Améliorations :
- Régénérer les types TypeScript de Supabase à partir du schéma de
production et supprimer les typings GraphQL manuels obsolètes.
- Documenter l’epic « Dettes & échéanciers », y compris l’énoncé du
problème, le modèle de données et le plan de déploiement.

Tests :
- Ajouter des tests unitaires pour la nouvelle logique de domaine des
calendriers d’engagements et les schémas Zod afin de garantir la
justesse des calculs et des règles de validation.

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Introduce a foundational data model and domain logic for finite
financial commitments (debts, installment plans, and one-off future
bills) without any UI changes.

New Features:
- Add new `commitments` and `commitment_payments` tables, plus related
Supabase types and RLS policies, to model finite financial engagements
and their payments.
- Define Zod schemas for validating commitments in alignment with
database constraints.
- Add pure domain functions for computing commitment schedules,
remaining balances, and completion status.

Enhancements:
- Regenerate Supabase TypeScript types from the production schema and
remove obsolete manual GraphQL typings.
- Document the "Dettes & échéanciers" epic, including problem statement,
data model, and rollout plan.

Tests:
- Add unit tests for the new commitments schedule domain logic and Zod
schemas to ensure correctness of calculations and validation rules.

</details>
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