Skip to content

feat(commitments): dashboard « Mes engagements » card + shared read (épic PR-3) - #235

Merged
thierryvm merged 3 commits into
mainfrom
feat/commitments-cockpit
Jul 22, 2026
Merged

feat(commitments): dashboard « Mes engagements » card + shared read (épic PR-3)#235
thierryvm merged 3 commits into
mainfrom
feat/commitments-cockpit

Conversation

@thierryvm

@thierryvm thierryvm commented Jul 20, 2026

Copy link
Copy Markdown
Owner

PR-3 — La carte « Mes engagements » sur le dashboard (épic Dettes & échéanciers)

Le cockpit répond maintenant à la question du long terme juste à côté de celle du mois : combien je dois encore au total, et ce qui tombe ce mois-ci.

La carte

  • Total restant dû de tous tes engagements, en tête.
  • « Ce mois : 250 € » — ce qui tombe ce mois-ci, qui disparaît une fois coché.
  • Une ligne par engagement : « Crédit voiture · 15 échéances restantes · jusqu'en mai 2027 » avec son solde.
  • Un engagement entièrement soldé disparaît — il ne doit plus peser sur le cockpit.
  • La carte entière se masque s'il n'y a rien à montrer : zéro bruit ajouté sur un workspace vide.

Elle se place juste après « Prochaines factures » : les factures répondent au mois, les engagements au long terme.

Cohérence garantie techniquement

Nouveau helper getCommitmentsWithLedger() utilisé par la page ET la carte — impossible que les deux surfaces affichent un solde différent. La requête inline de la page est remplacée par ce helper (mêmes garanties : RLS, select('*') anti-page-blanche).

Tests

8 tests de carte : netting du solde après coches, « ce mois » qui disparaît une fois coché, engagement soldé qui sort, facture ponctuelle sans jargon, inactif ignoré. Suite complète 1572 verte, typecheck/lint/build clean.

Smoke @Thierry

Dashboard → sous « Prochaines factures », la carte apparaît dès que tu as créé un engagement. Coche l'échéance du mois sur /app/commitments → reviens au dashboard : le total a baissé et « Ce mois » a disparu.

Reste de l'épic (PR-4, séparée volontairement)

L'effort lissé corrigé (une dette qui se termine ne doit plus être comptée à l'infini) touche le cœur du calcul budgétaire — il mérite sa propre PR avec les agents QA financiers, plutôt que d'être glissé ici. Le bouton « convertir une charge » (décision D4) l'accompagnera, puisqu'il change lui aussi ce qui entre dans le calcul.

🤖 Generated with Claude Code

Summary by Sourcery

Ajouter une carte récapitulative des engagements long terme au tableau de bord et centraliser le chargement des données d’engagements afin que le tableau de bord et la page des engagements partagent une vision cohérente de ce qui est dû.

Nouvelles fonctionnalités :

  • Ajouter une carte de tableau de bord « Mes engagements » affichant le total des engagements restants et les montants dus ce mois-ci, avec une ligne par engagement et un lien vers la page des engagements

Améliorations :

  • Introduire un helper de données partagé pour charger les engagements et leur registre de paiements une seule fois et le réutiliser sur la page des engagements et le tableau de bord afin de garder des totaux cohérents
  • Aligner les props client de la page des engagements avec la nouvelle structure partagée CommitmentRow traversant la frontière RSC

Tests :

  • Ajouter une suite de tests dédiée pour la carte des engagements du tableau de bord couvrant la dissimulation de l’état vide, la compensation des échéances déjà payées, la visibilité du montant dû mensuel, la suppression des engagements soldés, le rendu des engagements ponctuels, et l’exclusion des engagements inactifs
Original summary in English

Summary by Sourcery

Add a long-term commitments overview card to the dashboard and centralize commitments data loading so the dashboard and commitments page share a consistent view of what is owed.

New Features:

  • Add a "Mes engagements" dashboard card showing total remaining commitments and amounts due this month, with per-commitment rows and a link to the commitments page

Enhancements:

  • Introduce a shared data helper to load commitments and their payment ledger once and reuse it across the commitments page and dashboard to keep totals consistent
  • Align the commitments page client props with the new shared CommitmentRow shape crossing the RSC boundary

Tests:

  • Add a dedicated test suite for the dashboard engagements card covering empty-state hiding, netting of paid instalments, monthly due amount visibility, settled commitments removal, one-off commitment rendering, and inactive commitment exclusion

…épic PR-3)

The cockpit now answers the long-run question next to the monthly one: how
much do I still owe in total, and what falls due this month.

- New getCommitmentsWithLedger() data helper, used by BOTH /app/commitments and
  the dashboard card — the two surfaces read the same source and can never
  disagree on what is owed. The page's own inline query is replaced by it.
- EngagementsCard: total still owed, what is due this month (hidden once
  ticked), and one row per commitment with the instalments left and the end
  month. A fully settled commitment DISAPPEARS — it must stop weighing on the
  cockpit. The whole card self-hides when there is nothing to show, so an empty
  workspace gains no noise.
- Sits right after the bills card: bills answer the month, commitments the
  long run.

i18n x5: dashboard.commitments.* + the section heading.
8 card tests (netting, due-this-month, settled disappears, one-off, inactive).
Full suite 1572 green, typecheck/lint/build clean.
@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 20, 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 20, 2026 10:31pm

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

sourcery-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Guide du relecteur

Implémente la nouvelle carte de tableau de bord « Mes engagements » basée sur un helper de données partagé engagements+ledger, refactore la page des engagements pour utiliser ce helper, et ajoute des tests et le câblage i18n afin de garantir un affichage cohérent et auto-masqué des engagements long terme et des échéances de ce mois-ci.

Modifications par fichier

Changement Détails Fichiers
Introduire un helper de données partagé engagements+ledger utilisé à la fois par la page des engagements et par la carte du tableau de bord.
  • Créer getCommitmentsWithLedger pour lire les tables commitments et commitment_payments avec des requêtes Supabase soumises au RLS et un formatage partagé du ledger par clé de période.
  • Mapper les lignes de base de données vers un type de transport CommitmentRow avec des montants numériques et des enums sûres pour le domaine.
  • Construire une map paidKeysByCommitment indexée par id d’engagement vers des clés de période pour réutilisation sur plusieurs surfaces.
src/lib/data/commitments.ts
Refactoriser la page des engagements pour consommer le helper de données partagé et le type de ligne partagé.
  • Remplacer les requêtes Supabase inline et la logique de mapping manuelle par getCommitmentsWithLedger en utilisant l’id du snapshot d’espace de travail.
  • Supprimer le logging direct dans la page au profit d’un logging à l’intérieur du helper de données.
  • Mettre à jour le typage de CommitmentsClient RawCommitment pour réutiliser CommitmentRow comme type de frontière RSC.
src/app/[locale]/app/commitments/page.tsx
src/app/[locale]/app/commitments/CommitmentsClient.tsx
Ajouter la nouvelle carte de tableau de bord Mes engagements et l’intégrer dans la mise en page principale du tableau de bord.
  • Créer le composant serveur EngagementsCard qui convertit CommitmentRow en engagements de domaine, filtre les éléments inactifs ou entièrement soldés et calcule les soldes restants et les échéances.
  • Afficher le total restant dû et les montants dus ce mois-ci avec un formatage de devise localisé et un lien vers la page complète des engagements.
  • Rendre une ligne de liste par engagement avec soit le nombre d’échéances restantes plus la date de fin, soit une date ponctuelle, et masquer toute la carte lorsqu’il n’y a rien d’actif/à payer.
  • Insérer la carte dans le tableau de bord juste après ProchainesFacturesCard et la relier à getCommitmentsWithLedger et à la période courante.
src/components/dashboard/EngagementsCard.tsx
src/app/[locale]/app/page.tsx
Ajouter une couverture de tests pour le comportement de la nouvelle carte de tableau de bord et câbler les messages i18n.
  • Ajouter des tests EngagementsCard couvrant l’état vide, le calcul du total restant, la compensation des échéances déjà payées, la visibilité des montants dus ce mois-ci, la disparition des engagements soldés, le formatage des paiements ponctuels et le filtrage des engagements inactifs.
  • Mocker la navigation i18n et la traduction serveur next-intl pour un rendu déterministe dans les tests.
  • Ajouter ou ajuster des clés de traduction pour l’espace de noms du tableau de bord des engagements dans toutes les locales prises en charge.
src/components/dashboard/__tests__/EngagementsCard.test.tsx
messages/de-DE.json
messages/en.json
messages/es-ES.json
messages/fr-BE.json
messages/nl-BE.json

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 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 un 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.
  • 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 dashboard pour :

  • Activer ou désactiver des fonctionnalités de revue comme le résumé de pull request généré par Sourcery, le guide du relecteur, 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

Implements the new "Mes engagements" dashboard card powered by a shared commitments+ledger data helper, refactors the commitments page to use that helper, and adds tests and i18n wiring to ensure consistent, self-hiding display of long-term commitments and this-month dues.

File-Level Changes

Change Details Files
Introduce a shared commitments+ledger data helper used by both the commitments page and the dashboard card.
  • Create getCommitmentsWithLedger to read commitments and commitment_payments with RLS-scoped Supabase queries and shared period-key ledger formatting.
  • Map database rows into a CommitmentRow transport type with numeric amounts and domain-safe enums.
  • Build a paidKeysByCommitment map keyed by commitment id to period keys for reuse across surfaces.
src/lib/data/commitments.ts
Refactor the commitments page to consume the shared data helper and shared row type.
  • Replace inline Supabase queries and manual mapping logic with getCommitmentsWithLedger using the workspace snapshot id.
  • Remove direct logging in the page in favor of logging inside the data helper.
  • Update CommitmentsClient RawCommitment typing to reuse CommitmentRow as the RSC boundary type.
src/app/[locale]/app/commitments/page.tsx
src/app/[locale]/app/commitments/CommitmentsClient.tsx
Add the new Mes engagements dashboard card and integrate it into the main dashboard layout.
  • Create EngagementsCard server component that converts CommitmentRow to domain commitments, filters inactive or fully settled items, and computes remaining balances and dues.
  • Show total remaining owed and this-month due amounts with localized currency formatting and a link to the full commitments page.
  • Render a per-commitment list row with either installments-left plus end date or a one-off date, and hide the entire card when there is nothing active/owing.
  • Insert the card into the dashboard just after the ProchainesFacturesCard and wire it to getCommitmentsWithLedger and the current period.
src/components/dashboard/EngagementsCard.tsx
src/app/[locale]/app/page.tsx
Add test coverage for the new dashboard card behavior and wire i18n messages.
  • Add EngagementsCard tests covering empty state, total remaining calculation, netting of paid instalments, this-month due visibility, settled commitments dropping out, one-off formatting, and inactive commitment filtering.
  • Mock i18n navigation and next-intl server translation for deterministic rendering in tests.
  • Add or adjust translation keys for the commitments dashboard namespace across supported locales.
src/components/dashboard/__tests__/EngagementsCard.test.tsx
messages/de-DE.json
messages/en.json
messages/es-ES.json
messages/fr-BE.json
messages/nl-BE.json

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é 2 problèmes et laissé quelques retours de haut niveau :

  • EngagementsCard est défini comme un composant serveur async, mais les tests l'appellent comme une fonction simple ; envisagez de le rendre via du JSX dans un wrapper (ou d'extraire un sous‑composant purement présentational) pour mieux refléter le comportement en runtime et éviter les surprises s'il gagne des hooks plus tard.
  • Le mapping toDomain de CommitmentRow vers Commitment est maintenant dupliqué entre la carte du tableau de bord et la fonctionnalité des engagements ; envisagez d'extraire un mapper partagé dans la couche domaine ou data pour garder la transformation cohérente.
Prompt pour les agents IA
Veuillez traiter les commentaires de cette revue de code :

## Commentaires généraux
- EngagementsCard est défini comme un composant serveur async, mais les tests l'appellent comme une fonction simple ; envisagez de le rendre via du JSX dans un wrapper (ou d'extraire un sous‑composant purement présentational) pour mieux refléter le comportement en runtime et éviter les surprises s'il gagne des hooks plus tard.
- Le mapping `toDomain` de `CommitmentRow` vers `Commitment` est maintenant dupliqué entre la carte du tableau de bord et la fonctionnalité des engagements ; envisagez d'extraire un mapper partagé dans la couche domaine ou data pour garder la transformation cohérente.

## Commentaires individuels

### Commentaire 1
<location path="src/components/dashboard/__tests__/EngagementsCard.test.tsx" line_range="49-27" />
<code_context>
+  isActive: true,
+};
+
+async function renderCard(input: {
+  commitments: CommitmentRow[];
+  paidKeysByCommitment?: Record<string, string[]>;
+  currentPeriod?: { year: number; month: number };
+}) {
+  return render(
+    await EngagementsCard({
+      commitments: input.commitments,
+      paidKeysByCommitment: input.paidKeysByCommitment ?? {},
+      currentPeriod: input.currentPeriod ?? { year: 2026, month: 3 },
+      locale: 'fr-BE',
+    }),
+  );
+}
+
+describe('<EngagementsCard />', () => {
+  it('renders nothing at all when there is no commitment', async () => {
+    const { container } = await renderCard({ commitments: [] });
+    expect(container).toBeEmptyDOMElement();
+  });
+
+  it('shows the total still owed and links to the page', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Ajoutez un test couvrant plusieurs engagements pour valider l’agrégation des totaux et du montant dû mensuel

Les tests actuels ne couvrent qu’un seul engagement `carLoan`, donc nous ne vérifions jamais que `totalRemaining` et `dueAmount` sont correctement agrégés sur plusieurs engagements. Veuillez ajouter un test avec au moins deux engagements actifs (par exemple tous deux dus pour le mois courant) et vérifier que `engagements-total-remaining` et `engagements-due-this-month` reflètent la somme de toutes les lignes applicables, afin d’éviter les régressions où un seul élément serait compté.

Implémentation suggérée :

```typescript
  it('shows the total still owed and links to the page', async () => {
    await renderCard({ commitments: [carLoan] });
    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/4[  ]200/);
    expect(screen.getByTestId('engagements-card-link')).toHaveAttribute('href', '/app/commitments');
  });

  it('aggregates totals and due amount across multiple commitments', async () => {
    const secondCarLoan: CommitmentRow = {
      ...carLoan,
      key: 'car-2',
      name: 'Second car loan',
    };

    await renderCard({ commitments: [carLoan, secondCarLoan] });

    // total remaining should be the sum across both commitments
    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/8[  ]400/);

    // due this month should also be aggregated across both commitments
    expect(screen.getByTestId('engagements-due-this-month')).toHaveTextContent(/500/);
  });

  it('nets the ticked instalments out of the total', async () => {

```

Ce patch suppose :
1. `CommitmentRow` est déjà importé dans ce fichier de tests (car il est utilisé dans l’extrait que vous avez fourni).
2. Il existe un élément avec `data-testid="engagements-due-this-month"` qui affiche le montant mensuel dû pour tous les engagements, et pour un seul `carLoan` sa valeur est `250` (donc pour deux prêts identiques, le montant agrégé est `500`).

Si le montant réellement dû pour un seul `carLoan` est différent, ajustez la regex `/500/` en conséquence (deux fois la valeur pour un seul engagement).
Si le `data-testid` utilisé pour le montant dû mensuel diffère, remplacez `'engagements-due-this-month'` par le bon test id.
</issue_to_address>

### Commentaire 2
<location path="src/components/dashboard/__tests__/EngagementsCard.test.tsx" line_range="84-93" />
<code_context>
+    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/3[  ]700/);
+  });
+
+  it('surfaces what is due this month, and hides it once ticked', async () => {
+    const { unmount } = await renderCard({
+      commitments: [carLoan],
+      currentPeriod: { year: 2026, month: 3 },
+    });
+    expect(screen.getByTestId('engagements-due-this-month')).toHaveTextContent(/250/);
+    unmount();
+    await renderCard({
+      commitments: [carLoan],
+      paidKeysByCommitment: { car: ['2026-3'] },
+      currentPeriod: { year: 2026, month: 3 },
+    });
+    expect(screen.queryByTestId('engagements-due-this-month')).toBeNull();
+  });
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Couvrez le cas où certains engagements sont cochés pour le mois courant et d’autres non

Ce test ne vérifie que le comportement tout ou rien pour un seul engagement sur la période courante. Veuillez ajouter un test où deux engagements partagent le même `currentPeriod`, l’un est dû et non coché et l’autre est déjà coché, afin de confirmer que la bannière apparaît toujours et que `dueAmount` inclut uniquement les engagements non cochés. Cela protégera contre un bug où un paiement coché pour le mois masquerait à tort toute la ligne « Ce mois ».

Implémentation suggérée :

```typescript
  it('nets the ticked instalments out of the total', async () => {
    await renderCard({
      commitments: [carLoan],
      paidKeysByCommitment: { car: ['2026-1', '2026-2'] },
    });
    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/3[  ]700/);
  });

  it('includes only unticked commitments in the current-month due amount when some are already paid', async () => {
    const secondCommitment = {
      ...carLoan,
      key: 'car-2',
    };

    await renderCard({
      commitments: [carLoan, secondCommitment],
      paidKeysByCommitment: { car: ['2026-3'] },
      currentPeriod: { year: 2026, month: 3 },
    });

    const banner = screen.getByTestId('engagements-due-this-month');
    expect(banner).toBeInTheDocument();
    expect(banner).toHaveTextContent(/250/);
  });

import messages from '../../../../messages/fr-BE.json';

```

- Si `carLoan` ne représente pas un engagement de 250 par mois (ou si le montant « dû ce mois‑ci » est différent), ajustez l’assertion `toHaveTextContent(/250/)` au montant correct par période pour un seul engagement.
- Si `CommitmentRow` nécessite des champs uniques supplémentaires au‑delà de `key` pour distinguer les engagements (par exemple un `id`), dupliquez également ces champs dans `secondCommitment` en les surchargant pour éviter les collisions.
</issue_to_address>

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

Hey - I've found 2 issues, and left some high level feedback:

  • EngagementsCard is defined as an async server component but the tests call it as a plain function; consider rendering it via JSX in a wrapper (or factoring out a pure presentational subcomponent) to better mirror runtime behavior and avoid surprises if it gains hooks later.
  • The toDomain mapping from CommitmentRow to Commitment is now duplicated between the dashboard card and the commitments feature; consider extracting a shared mapper in the domain or data layer to keep the transformation consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- EngagementsCard is defined as an async server component but the tests call it as a plain function; consider rendering it via JSX in a wrapper (or factoring out a pure presentational subcomponent) to better mirror runtime behavior and avoid surprises if it gains hooks later.
- The `toDomain` mapping from `CommitmentRow` to `Commitment` is now duplicated between the dashboard card and the commitments feature; consider extracting a shared mapper in the domain or data layer to keep the transformation consistent.

## Individual Comments

### Comment 1
<location path="src/components/dashboard/__tests__/EngagementsCard.test.tsx" line_range="49-27" />
<code_context>
+  isActive: true,
+};
+
+async function renderCard(input: {
+  commitments: CommitmentRow[];
+  paidKeysByCommitment?: Record<string, string[]>;
+  currentPeriod?: { year: number; month: number };
+}) {
+  return render(
+    await EngagementsCard({
+      commitments: input.commitments,
+      paidKeysByCommitment: input.paidKeysByCommitment ?? {},
+      currentPeriod: input.currentPeriod ?? { year: 2026, month: 3 },
+      locale: 'fr-BE',
+    }),
+  );
+}
+
+describe('<EngagementsCard />', () => {
+  it('renders nothing at all when there is no commitment', async () => {
+    const { container } = await renderCard({ commitments: [] });
+    expect(container).toBeEmptyDOMElement();
+  });
+
+  it('shows the total still owed and links to the page', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test covering multiple commitments to validate aggregation of totals and monthly due amount

Current tests only cover a single `carLoan` commitment, so we never verify that `totalRemaining` and `dueAmount` are correctly aggregated across multiple commitments. Please add a test with at least two active commitments (e.g. both due in the current month) and assert that `engagements-total-remaining` and `engagements-due-this-month` reflect the sum of all applicable rows, to guard against regressions where only one item is counted.

Suggested implementation:

```typescript
  it('shows the total still owed and links to the page', async () => {
    await renderCard({ commitments: [carLoan] });
    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/4[  ]200/);
    expect(screen.getByTestId('engagements-card-link')).toHaveAttribute('href', '/app/commitments');
  });

  it('aggregates totals and due amount across multiple commitments', async () => {
    const secondCarLoan: CommitmentRow = {
      ...carLoan,
      key: 'car-2',
      name: 'Second car loan',
    };

    await renderCard({ commitments: [carLoan, secondCarLoan] });

    // total remaining should be the sum across both commitments
    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/8[  ]400/);

    // due this month should also be aggregated across both commitments
    expect(screen.getByTestId('engagements-due-this-month')).toHaveTextContent(/500/);
  });

  it('nets the ticked instalments out of the total', async () => {

```

This patch assumes:
1. `CommitmentRow` is already imported in this test file (as it is used in the snippet you provided).
2. There is an element with `data-testid="engagements-due-this-month"` that shows the monthly due amount for all commitments, and for a single `carLoan` its value is `250` (so for two identical loans the aggregated amount is `500`).

If the actual due amount for a single `carLoan` is different, adjust the `/500/` regex accordingly (twice the single-commitment value).
If the `data-testid` used for the monthly due amount differs, replace `'engagements-due-this-month'` with the correct test id.
</issue_to_address>

### Comment 2
<location path="src/components/dashboard/__tests__/EngagementsCard.test.tsx" line_range="84-93" />
<code_context>
+    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/3[  ]700/);
+  });
+
+  it('surfaces what is due this month, and hides it once ticked', async () => {
+    const { unmount } = await renderCard({
+      commitments: [carLoan],
+      currentPeriod: { year: 2026, month: 3 },
+    });
+    expect(screen.getByTestId('engagements-due-this-month')).toHaveTextContent(/250/);
+    unmount();
+    await renderCard({
+      commitments: [carLoan],
+      paidKeysByCommitment: { car: ['2026-3'] },
+      currentPeriod: { year: 2026, month: 3 },
+    });
+    expect(screen.queryByTestId('engagements-due-this-month')).toBeNull();
+  });
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Cover the case where some commitments are ticked for the current month and others are not

This only checks the all-or-nothing behavior for a single commitment in the current period. Please add a test where two commitments share the same `currentPeriod`, one is due and unticked and the other is already ticked, to confirm the banner still appears and `dueAmount` includes only the unticked commitment(s). This will protect against a bug where any ticked payment for the month incorrectly hides the entire “Ce mois” line.

Suggested implementation:

```typescript
  it('nets the ticked instalments out of the total', async () => {
    await renderCard({
      commitments: [carLoan],
      paidKeysByCommitment: { car: ['2026-1', '2026-2'] },
    });
    expect(screen.getByTestId('engagements-total-remaining')).toHaveTextContent(/3[  ]700/);
  });

  it('includes only unticked commitments in the current-month due amount when some are already paid', async () => {
    const secondCommitment = {
      ...carLoan,
      key: 'car-2',
    };

    await renderCard({
      commitments: [carLoan, secondCommitment],
      paidKeysByCommitment: { car: ['2026-3'] },
      currentPeriod: { year: 2026, month: 3 },
    });

    const banner = screen.getByTestId('engagements-due-this-month');
    expect(banner).toBeInTheDocument();
    expect(banner).toHaveTextContent(/250/);
  });

import messages from '../../../../messages/fr-BE.json';

```

- If `carLoan` does not represent a 250-per-month commitment (or if the “due this month” amount is different), adjust the `toHaveTextContent(/250/)` expectation to the correct per-period amount for a single commitment.
- If `CommitmentRow` requires additional unique fields beyond `key` to distinguish commitments (e.g. an `id`), also spread and override those in `secondCommitment` to avoid clashes.
</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/components/dashboard/__tests__/EngagementsCard.test.tsx
Comment thread src/components/dashboard/__tests__/EngagementsCard.test.tsx
…state (Sourcery #235)

Both suggestions were legitimate — the card's aggregation was only exercised
with a single commitment. Added: total summed across two commitments with
different ledgers (3 950 + 1 200 = 5 150 €); due-this-month counting ONLY the
untricked ones when some are already paid (200 €, not 450 €); the figure
disappearing when every commitment is ticked while the card itself stays; and a
settled commitment dropping out of both the list and the total while a live one
remains. 12 card tests.
thierryvm added a commit that referenced this pull request Jul 20, 2026
… read (Sourcery #235)

The row→domain projection was duplicated between the dashboard card and the
commitments page — exactly the kind of copy that drifts. Extracted to one
definition.

Extracting it into the data module naively broke every UI test: importing the
mapper pulled  → the Supabase client → server-only
env parsing into the component bundle. So the pure part (type + mapper) now
lives in its own  with ZERO Supabase import, and the impure
read next door consumes and re-exports it. UI and tests import the pure module.

Full suite 1576 green.
… read (Sourcery #235)

The row-to-domain projection was duplicated between the dashboard card and the
commitments page — exactly the kind of copy that drifts. Extracted to one
definition.

Extracting it into the data module naively broke every UI test: importing the
mapper pulled getCommitmentsWithLedger, hence the Supabase client and its
server-only env parsing, into the component bundle. So the pure part (type +
mapper) now lives in its own commitment-row.ts with ZERO Supabase import, and
the impure read next door consumes and re-exports it. UI and tests import the
pure module.

Full suite 1576 green.
@thierryvm
thierryvm force-pushed the feat/commitments-cockpit branch from 65771e3 to 135286a Compare July 20, 2026 22:30
@thierryvm

Copy link
Copy Markdown
Owner Author

Retours globaux traités (135286a) :

  1. toDomain dupliqué entre la carte et la page — appliqué, et le refactor a révélé un vrai piège d'architecture que je signale car il est instructif : extraire le mapper dans data/commitments.ts (à côté de la lecture DB) a cassé tous les tests UI — importer le mapper tirait getCommitmentsWithLedger → le client Supabase → le parsing d'env server-only dans le bundle composant. La bonne extraction sépare donc le pur de l'impur : commitment-row.ts porte le type + le mapper avec zéro import Supabase, et commitments.ts (la lecture) le consomme et le réexporte. L'UI et les tests importent le module pur.

  2. EngagementsCard async testé comme une fonction — décliné, avec rationale. render(await Component(props)) est la convention établie du repo pour tous les Server Components testés (ProchainesFacturesCard, ProvisionHealthGaugeCard, …) ; changer le pattern pour un seul composant créerait une incohérence de suite de tests sans gain. Le risque évoqué ("s'il gagne des hooks plus tard") ne s'applique pas ici : c'est un Server Component, il ne peut pas porter de hooks React — s'il en avait besoin un jour, il faudrait de toute façon le scinder en sous-composant client, et c'est ce changement-là qui appellerait un test différent.

Les 2 suggestions inline (agrégation multi-engagements + état mixte) ont été appliquées dans 12b4f14.

@thierryvm
thierryvm merged commit cd451d7 into main Jul 22, 2026
9 checks passed
@thierryvm
thierryvm deleted the feat/commitments-cockpit branch July 22, 2026 10:12
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