fix(commitments): le formulaire ne demandait jamais la date de premiere echeance - #296
fix(commitments): le formulaire ne demandait jamais la date de premiere echeance#296thierryvm wants to merge 3 commits into
Conversation
Reported on the real « SPF impôt » plan: 2 407,93 € over 11 monthly instalments from 15/05/2026, 4 paid. The card announced « 11 échéances de 220 € · dernière en Mai 2027 » — two months late, and a wrong amount. The schedule arithmetic was never at fault. Read from production, the row carried `start_month = 7` (its creation month, 21/07/2026), not 5: `CommitmentsClient` hard-coded `startYear/startMonth` to the viewed period and `paymentDay` to 1, and the edit form never sent them back. The typed date had no way of reaching the database, so `first + (total - 1)` landed on May 2027 from an anchor nobody chose. - ADR-021 D3 said the anchor was the NEXT instalment; nothing implemented it. The ledger counts ticks from the anchor forward and `+` fills the oldest unpaid SCHEDULED period - both only make sense if the anchor is the FIRST instalment. D3 is corrected, with the reasoning written down. - The form now asks for the first instalment, the payment day (optional) and the cadence, and previews the resulting window before submission. - The final instalment is derived - `total - (n - 1) x instalment` - in Decimal: `2407.93 - 10 * 220` is 207.92999999999984 in float. The card reads « 10 x 220 € + 207,93 € » instead of claiming 11 identical ones. - `engagements-lisses.ts` carried a third, unguarded copy of the window's upper bound; it now consumes `endOrdinal()`. - The payment day renders only when explicitly chosen: the column is `not null default 1`, so showing the stored 1 would invent a date on every pre-existing row. No migration, no data correction: the anchor of existing rows is only knowable by their owner, and is now fixable from the edit form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
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.
|
Guide du·de la relecteur·riceCette PR corrige la façon dont les échéanciers de engagements sont ancrés et affichés en traitant l’ancre comme la date de la première échéance, en propageant cette date à travers le formulaire et le domaine, en calculant les montants de la dernière échéance avec Diagramme de séquence pour le nouveau formulaire d’engagement et l’aperçu de l’échéanciersequenceDiagram
actor User
participant CommitmentsClient
participant CommitmentsDomain as CommitmentsDomain(schedule.ts)
participant ServerActions as createCommitmentAction
User->>CommitmentsClient: Type startMonth, startYear, paymentDay, installmentsTotal
CommitmentsClient->>CommitmentsDomain: firstInstallmentDate(draft)
CommitmentsClient->>CommitmentsDomain: endInstallmentDate(draft)
CommitmentsDomain-->>CommitmentsClient: first / last InstallmentDate
CommitmentsClient-->>User: Render window preview (formatInstallmentDate)
User->>CommitmentsClient: Submit form
CommitmentsClient->>ServerActions: createCommitmentAction(payload with startYear, startMonth, paymentDay, frequency)
ServerActions-->>CommitmentsClient: ok
CommitmentsClient-->>User: Show success, list card uses endInstallmentDate/lastInstallmentAmount
Modifications au niveau des fichiers
Conseils et commandesInteragir avec Sourcery
Personnaliser votre expérienceAccédez à votre tableau de bord pour :
Obtenir de l’aide
Original review guide in EnglishReviewer's GuideThis PR fixes how commitments schedules are anchored and displayed by treating the anchor as the first installment date, wiring that date through the form and domain, deriving final installment amounts with Decimal, and aligning dashboard/card rendering and cockpit logic with the corrected schedule semantics. Sequence diagram for new commitment form and schedule previewsequenceDiagram
actor User
participant CommitmentsClient
participant CommitmentsDomain as CommitmentsDomain(schedule.ts)
participant ServerActions as createCommitmentAction
User->>CommitmentsClient: Type startMonth, startYear, paymentDay, installmentsTotal
CommitmentsClient->>CommitmentsDomain: firstInstallmentDate(draft)
CommitmentsClient->>CommitmentsDomain: endInstallmentDate(draft)
CommitmentsDomain-->>CommitmentsClient: first / last InstallmentDate
CommitmentsClient-->>User: Render window preview (formatInstallmentDate)
User->>CommitmentsClient: Submit form
CommitmentsClient->>ServerActions: createCommitmentAction(payload with startYear, startMonth, paymentDay, frequency)
ServerActions-->>CommitmentsClient: ok
CommitmentsClient-->>User: Show success, list card uses endInstallmentDate/lastInstallmentAmount
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - j'ai trouvé 1 problème et laissé quelques retours globaux :
- La convention
PAYMENT_DAY_UNSET = 1est actuellement dupliquée entre l'UI et le domaine (viahasExplicitPaymentDay) ; envisage de centraliser ce sentinelle dans le domaine des commitments et de le consommer depuis le client pour éviter toute divergence si la valeur par défaut change. - La logique de validation année/mois/jour pour la fenêtre de brouillon et le submit handler dans
CommitmentsClientest répétée avec des bornes en dur ; extraire un helper partagé ou s'appuyer sur un validateur au niveau du domaine rendrait les futures modifications des plages valides moins sujettes aux erreurs.
Prompt pour les agents IA
Merci de prendre en compte les commentaires de cette revue de code :
## Commentaires généraux
- La convention `PAYMENT_DAY_UNSET = 1` est actuellement dupliquée entre l'UI et le domaine (via `hasExplicitPaymentDay`) ; envisage de centraliser ce sentinelle dans le domaine des commitments et de le consommer depuis le client pour éviter toute divergence si la valeur par défaut change.
- La logique de validation année/mois/jour pour la fenêtre de brouillon et le submit handler dans `CommitmentsClient` est répétée avec des bornes en dur ; extraire un helper partagé ou s'appuyer sur un validateur au niveau du domaine rendrait les futures modifications des plages valides moins sujettes aux erreurs.
## Commentaires individuels
### Commentaire 1
<location path="src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx" line_range="286-295" />
<code_context>
+ it('sends the FIRST-instalment date the user typed, not the viewed period', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Ajoute des tests pour un mois/année de début et un jour de paiement invalides afin de couvrir les nouvelles branches de validation côté client.
Le test actuel ne couvre que le chemin de succès. Pour exercer les nouveaux garde-fous, ajoute des tests qui soumettent le formulaire avec :
- Une année invalide (par ex. `1999` ou `"abcd"`) et vérifie que `createMock` n'est pas appelé.
- Un mois invalide (par ex. `0` ou `13`) et vérifie que `createMock` n'est pas appelé.
- Un jour de paiement invalide (par ex. `0`, `32` ou non numérique) et vérifie que `createMock` n'est pas appelé.
Cela garantit que le formulaire n'est pas soumis lorsque ces validations côté client échouent.
Implémentation suggérée :
```typescript
// The regression that started all this: the form used to hard-code the anchor
// to the CREATION month, so a plan whose first instalment was two months
// earlier ended two months late. The typed date must reach the action.
it('does not submit when the start year is invalid', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
// Valid month so that only the year is invalid
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '5' } });
// Invalid year (too old)
fireEvent.change(screen.getByTestId('commitment-start-year'), { target: { value: '1999' } });
await act(async () => {
fireEvent.submit(screen.getByRole('button', { name: /^ajouter$/i }).closest('form')!);
});
expect(createMock).not.toHaveBeenCalled();
});
it('does not submit when the start month is invalid', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
fireEvent.change(screen.getByTestId('commitment-start-year'), { target: { value: '2026' } });
// Invalid month (13)
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '13' } });
await act(async () => {
fireEvent.submit(screen.getByRole('button', { name: /^ajouter$/i }).closest('form')!);
});
expect(createMock).not.toHaveBeenCalled();
});
it('does not submit when the payment day is invalid', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
fireEvent.change(screen.getByTestId('commitment-start-year'), { target: { value: '2026' } });
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '5' } });
// Invalid payment day (32)
fireEvent.change(screen.getByTestId('commitment-payment-day'), { target: { value: '32' } });
await act(async () => {
fireEvent.submit(screen.getByRole('button', { name: /^ajouter$/i }).closest('form')!);
});
expect(createMock).not.toHaveBeenCalled();
});
it('sends the FIRST-instalment date the user typed, not the viewed period', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '5' } });
```
- Ajuste les sélecteurs pour les champs année et jour de paiement s'ils diffèrent des `data-testid="commitment-start-year"` et `data-testid="commitment-payment-day"` supposés (par exemple s'ils utilisent `getByLabelText` au lieu de `getByTestId`).
- Si la validation côté client distingue les valeurs numériques hors plage des valeurs non numériques, il peut être utile d'ajouter un test supplémentaire pour un jour de paiement non numérique (par ex. `"abcd"`), en miroir du test `does not submit when the payment day is invalid`.
- Assure-toi que `createMock` est correctement importé/mocké dans ce fichier de test afin que `expect(createMock).not.toHaveBeenCalled()` fonctionne ; si le mock est réinitialisé entre les tests (par ex. `beforeEach(createMock.mockReset)`), il faut conserver ce comportement.
</issue_to_address>Sourcery est gratuit pour l'open source - si nos revues te plaisent, pense à les partager ✨
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- The
PAYMENT_DAY_UNSET = 1convention is currently duplicated between the UI and the domain (viahasExplicitPaymentDay); consider centralizing this sentinel in the commitments domain and consuming it from the client to avoid drift if the default ever changes. - Year/month/day validation logic for the draft window and submit handler in
CommitmentsClientis repeated with hard-coded bounds; extracting a shared helper or relying on a domain-level validator would make future changes to valid ranges less error-prone.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `PAYMENT_DAY_UNSET = 1` convention is currently duplicated between the UI and the domain (via `hasExplicitPaymentDay`); consider centralizing this sentinel in the commitments domain and consuming it from the client to avoid drift if the default ever changes.
- Year/month/day validation logic for the draft window and submit handler in `CommitmentsClient` is repeated with hard-coded bounds; extracting a shared helper or relying on a domain-level validator would make future changes to valid ranges less error-prone.
## Individual Comments
### Comment 1
<location path="src/app/[locale]/app/commitments/__tests__/CommitmentsClient.test.tsx" line_range="286-295" />
<code_context>
+ it('sends the FIRST-instalment date the user typed, not the viewed period', async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for invalid start month/year and payment day to cover the new client-side validation branches.
The current test only covers the successful path. To exercise the new guards, please add tests that submit the form with:
- An invalid year (e.g. `1999` or `"abcd"`) and assert `createMock` is not called.
- An invalid month (e.g. `0` or `13`) and assert `createMock` is not called.
- An invalid payment day (e.g. `0`, `32`, or non‑numeric) and assert `createMock` is not called.
This ensures the form does not submit when these client-side validations fail.
Suggested implementation:
```typescript
// The regression that started all this: the form used to hard-code the anchor
// to the CREATION month, so a plan whose first instalment was two months
// earlier ended two months late. The typed date must reach the action.
it('does not submit when the start year is invalid', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
// Valid month so that only the year is invalid
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '5' } });
// Invalid year (too old)
fireEvent.change(screen.getByTestId('commitment-start-year'), { target: { value: '1999' } });
await act(async () => {
fireEvent.submit(screen.getByRole('button', { name: /^ajouter$/i }).closest('form')!);
});
expect(createMock).not.toHaveBeenCalled();
});
it('does not submit when the start month is invalid', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
fireEvent.change(screen.getByTestId('commitment-start-year'), { target: { value: '2026' } });
// Invalid month (13)
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '13' } });
await act(async () => {
fireEvent.submit(screen.getByRole('button', { name: /^ajouter$/i }).closest('form')!);
});
expect(createMock).not.toHaveBeenCalled();
});
it('does not submit when the payment day is invalid', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
fireEvent.change(screen.getByTestId('commitment-start-year'), { target: { value: '2026' } });
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '5' } });
// Invalid payment day (32)
fireEvent.change(screen.getByTestId('commitment-payment-day'), { target: { value: '32' } });
await act(async () => {
fireEvent.submit(screen.getByRole('button', { name: /^ajouter$/i }).closest('form')!);
});
expect(createMock).not.toHaveBeenCalled();
});
it('sends the FIRST-instalment date the user typed, not the viewed period', async () => {
createMock.mockResolvedValue({ ok: true });
renderPage([], { currentPeriod: { year: 2026, month: 7 } });
fireEvent.click(screen.getByTestId('commitments-add-toggle'));
fireEvent.change(screen.getByLabelText('Libellé'), { target: { value: 'SPF impôt' } });
fireEvent.change(screen.getByLabelText(/Montant total dû/), { target: { value: '2407.93' } });
fireEvent.change(screen.getByLabelText(/Montant par échéance/), { target: { value: '220' } });
fireEvent.change(screen.getByLabelText(/Nombre total d'échéances/), {
target: { value: '11' },
});
fireEvent.change(screen.getByTestId('commitment-start-month'), { target: { value: '5' } });
```
- Adjust the selectors for the year and payment day inputs if they differ from the assumed `data-testid="commitment-start-year"` and `data-testid="commitment-payment-day"` (for example, if they use `getByLabelText` instead of `getByTestId`).
- If the client-side validation distinguishes between “out-of-range numeric” and “non-numeric” values, you may want to add an extra test for a non-numeric payment day (e.g. `"abcd"`) mirroring the `does not submit when the payment day is invalid` test.
- Ensure `createMock` is correctly imported/mocked in this test file so that `expect(createMock).not.toHaveBeenCalled()` works; if the mock is reset between tests (e.g. `beforeEach(createMock.mockReset)`), that should remain in place.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
`20260719000001_commitments.sql:28` reads "Anchor = the NEXT instalment (D3)". It stays: an applied migration on a shared production database is not retouched for a comment. Saying so in the ADR keeps a future reader from mistaking a historical trace for an unresolved contradiction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sourcery was right: the validation branches added with the date fields had no test. Six cases now exercise them — years below 2000, above 2100, non-numeric and empty; payment days of 0 and 32 — plus the case that an empty payment day is ACCEPTED, since it is optional and stores the "unset" 1. Two of the suggested cases were dropped after they failed against correct behaviour, which is the useful part of writing them: - A non-numeric payment day cannot exist. `type="number"` blanks the field, and a blank day legitimately means "not chosen". - An invalid month cannot come from the UI. It is a controlled <select> of exactly 1..12; pushing an absent value at it leaves a valid option selected (measured: React re-renders it back to 1). A test there could only have asserted that a valid month is accepted while looking like a guard. What replaces it asserts the property that actually holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le défaut
Signalé par @Thierry sur le plan réel « SPF impôt » : 2 407,93 € sur 11 mensualités depuis le 15/05/2026, 4 payées. La carte annonçait « 4/11 échéances de 220 € · dernière en Mai 2027 » — deux mois trop tard, et un montant faux.
Diagnostic — les trois hypothèses de départ sont réfutées
L'arithmétique n'a jamais été en cause.
installmentPeriodsfait bienfirst + i · cycleavecide 0 àtotal − 1; aucunnew Date(y, m, d)nisetMonth()sur ce chemin ;formatMonthdécale correctement.La ligne en base portait
start_month = 7— son mois de création (21/07/2026) — et non 5.CommitmentsClientcâblaitstartYear/startMonthsur la période affichée etpaymentDayà1, et le formulaire d'édition ne les renvoyait jamais. La date saisie par l'utilisateur n'avait aucun chemin vers la base.first + (total − 1)depuis une ancre que personne n'a choisie donne mai 2027. L'écart de +2 mois est l'écart entre mai et juillet.Le reste dû (1 527,93 €) et le compteur 4/11 étaient justes : ils lisent le calendrier, pas la documentation.
Ce que fait cette PR
1. ADR-021 D3 corrigé — l'ancre est la PREMIÈRE échéance. D3 disait « la prochaine » ; rien ne l'implémentait. Le grand livre compte les cochages depuis l'ancre et
+remplit la plus ancienne échéance non payée du calendrier — deux comportements qui n'ont de sens que si l'ancre ouvre l'historique payable. Le motif du choix est écrit dans l'ADR, pas seulement la conclusion.2. Le formulaire demande enfin la date. Première échéance (mois + année en deux contrôles, pas
input[type=month]que Safari iOS dégrade), jour de prélèvement optionnel, périodicité. Et il rend la conséquence avant validation : « Échéancier : 15 mai 2026 → 15 mars 2027 ».3. La dernière échéance est dérivée, en
Decimal.total − (n − 1) × mensualité. En flottant natif,2407.93 - 10 * 220vaut207.92999999999984. La carte lit « 10 × 220 € + 207,93 € » au lieu de prétendre 11 échéances identiques. L'en-tête de justification deschedule.ts— « un compte fixe d'échéances identiques » — est réécrit : il ne tenait plus dès qu'on dérive un solde résiduel.4. La troisième dérivation disparaît.
engagements-lisses.tsportait sa propre copie destart + (total − 1) · stepavec un helperordinal()privé, sans filet : rien n'aurait échoué si la définition avait changé. Elle consommeendOrdinal(). Les six recopies de(total % 12) + 1hors engagements (charges, prévisions) sont signalées et non touchées — autre concern.5. Le jour ne s'affiche que s'il a été saisi. La colonne est
not null default 1et l'ancien formulaire y écrivait 1 en dur : afficher ce 1 montrerait une date inventée à toutes les lignes existantes. Un 1er réellement choisi s'affiche donc en mois + année — le côté bénin assumé du compromis.Aucune migration, aucune correction de données
La ligne de production reste fausse après cette PR, délibérément. Seul son propriétaire sait que la vraie première échéance est le 15/05/2026 ; un script qui devinerait serait une correction silencieuse de données financières. La correction se fait depuis le formulaire d'édition, désormais capable de la porter.
Vérification
Toutes les portes vertes, plus un rendu réel :
npm run devcontre la stack Supabase locale, compte éphémère, cas SPF saisi via le formulaire et 4 échéances cochées via+.Échéancier : 15 mai 2026 → 15 mars 20274/11 échéances : 10 × 220 € + 207,93 € · dernière : 15 mars 2027—Reste 1 527,93 €4/11 instalments: 10 × €220 + €207.93 · last: March 15, 20277 échéances restantes · fin : 15 mars 2027—1 527,93 €lint0 erreur ·lint:use-server✅ ·typecheck0 ·test140 fichiers / 1871 cas, 100 % ·build✅ ·devdémarre et sert (.nextpurgé entre les deux).Tests ajoutés : le cas SPF de bout en bout, plan à une seule échéance, dernière échéance de montant différent, passage d'année (dont ancre décembre), fin de mois 31 → 28/29 février et 30 → février, plan entièrement soldé, plan sur-spécifié (mensualités > total → reste borné à 0, jamais négatif), et un verrou d'équivalence entre
endOrdinal()et la dernière période planifiée sur les quatre périodicités.Hors périmètre — deux constats mesurés pendant la vérification
aria-valuenow="36"etstyle="width:36%"bien présents, mais largeur calculée 1054 px sur une piste de 1054 px. La CSP (style-src 'self' 'nonce-…' 'unsafe-inline') bloque l'attributstyle— un nonce annule'unsafe-inline', et sansstyle-src-attrles attributs retombent surstyle-src. La barre lit donc toujours 100 % pour un utilisateur voyant, alors que le lecteur d'écran reçoit la bonne valeur. Code non touché ici ; correctif = PR dédiée (CSP)./et/login— pages non touchées, donc antérieur.🤖 Generated with Claude Code
Summary by Sourcery
Aligner le domaine des engagements et l’interface utilisateur sur un premier versement comme ancre, dériver précisément les derniers versements irréguliers, et mettre à jour le formulaire d’engagements ainsi que la documentation pour exposer et prévisualiser le calendrier complet.
Nouvelles fonctionnalités :
Corrections de bugs :
Decimalprécise pour le dernier versement et le solde résiduel.Améliorations :
Tests :
endOrdinalet la dernière période planifiée.CommitmentsClientet du formateur i18n pour couvrir les nouveaux champs du formulaire, la prévisualisation de fenêtre, le comportement du jour de paiement, et la cohérence du formatage des dates de versement.Original summary in English
Summary by Sourcery
Align commitments domain and UI on a first-installment anchor, derive irregular final installments precisely, and update the commitments form and documentation to expose and preview the full schedule.
New Features:
Bug Fixes:
Enhancements:
Tests: