From ab75f1f9795b02d15cdc2e972d2885ed3c7277ff Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Thu, 7 May 2026 17:42:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(checkin):=20bump=20invoice.status=20pending?= =?UTF-8?q?=20=E2=86=92=20awaiting=5Fuser=5Fconfirmation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug V1 documenté dans flow.md mais jamais corrigé : le job send_checkin_job envoyait l'email + marquait la CheckinTask `sent`, mais ne touchait pas le statut de la facture. Conséquence : l'user reçoit le mail check-in dans sa boîte mais la modale in-app au refresh ne l'affiche pas (la modale liste uniquement les `awaiting_user_confirmation` côté DB). Fix : après l'envoi mail OK et le mark CheckinTask=sent, on bump `Invoice.status = 'awaiting_user_confirmation'` SI elle est encore en `pending`. Pas de bump si entre temps : - mark-paid (status=paid) - litigation/cancelled (transitions manuelles) - in_relance (impossible mais safe) Doc flow.md mise à jour pour refléter le nouveau comportement (effets de la transition pending → awaiting + déprécation de la note "TODO V1.5"). Pour les factures existantes en prod qui ont déjà reçu le mail mais restent en `pending` (cas pré-fix) : backfill manuel via SQL : UPDATE invoices SET status = 'awaiting_user_confirmation' WHERE status = 'pending' AND id IN ( SELECT invoice_id FROM checkin_tasks WHERE status = 'sent' ); Co-Authored-By: Claude Opus 4.7 --- apps/api/app/jobs/send_checkin_job.ts | 18 +++++++++++++++++- docs/flow.md | 14 ++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/api/app/jobs/send_checkin_job.ts b/apps/api/app/jobs/send_checkin_job.ts index 8d81fbf..a77cf71 100644 --- a/apps/api/app/jobs/send_checkin_job.ts +++ b/apps/api/app/jobs/send_checkin_job.ts @@ -63,7 +63,23 @@ export async function sendCheckinJob(jobData: { taskId: string; plain: string }) pendingUrl, }) + const sentAt = await clock.now(invoice.organizationId) task.status = 'sent' - task.sentAt = await clock.now(invoice.organizationId) + task.sentAt = sentAt await task.save() + + // Bascule le statut de la facture en `awaiting_user_confirmation`. Le + // check-in vient de partir, l'user doit répondre. C'est ce statut que + // la modale in-app (`/api/v1/checkin/inapp/pending`) liste pour rappeler + // à l'user qu'il y a des décisions à prendre. + // Ne touche pas si le statut a déjà bougé entre temps (in_relance, paid, + // litigation, etc.) — only bump from `pending`. + if (invoice.status === 'pending') { + invoice.status = 'awaiting_user_confirmation' + await invoice.save() + logger.info( + { invoiceId: invoice.id, numero: invoice.numero }, + 'sendCheckinJob: invoice status bumped pending → awaiting_user_confirmation' + ) + } } diff --git a/docs/flow.md b/docs/flow.md index 1293cc1..cb991a3 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -92,8 +92,11 @@ Transitions manuelles (par l'user, depuis la fiche facture) : #### `pending → awaiting_user_confirmation` - **Qui déclenche** : le scheduler `CheckinTask`, automatiquement, quand `dueDate` est atteinte. -- **Effet** : un email check-in part à l'user (pas au client) avec 2 liens *Oui (payée)* / *Non (toujours impayée)*. La `CheckinTask` est marquée `sent`. -- **Note V1** : dans la prod actuelle, le statut DB de l'invoice reste techniquement `pending` jusqu'à ce que l'user réponde — c'est le seed démo qui force `awaiting_user_confirmation` pour pré-peupler des cas. À aligner V1.5 (le job `send_checkin_job` devrait push le statut). +- **Effet** : + - Un email check-in part à l'user (pas au client) avec 2 liens *Oui (payée)* / *Non (toujours impayée)* + - La `CheckinTask` est marquée `sent` + - **L'`Invoice.status` passe de `pending` à `awaiting_user_confirmation`** (uniquement si encore en `pending` — pas de bump si la facture a été marquée payée ou autre entre temps) + - Côté SPA : la modale check-in `usePendingCheckins` voit désormais cette facture et l'affiche au prochain login / refocus #### `awaiting_user_confirmation → paid` (réponse "Oui") - **Qui déclenche** : l'user, via 4 surfaces possibles : @@ -223,8 +226,11 @@ Sans ça, le SaaS ferait peur (peur de relancer un client qui vient de payer). A ### 5.2 Architecture des `CheckinTask` - Un `CheckinTask` est créé à la création de l'invoice (sauf si `pending` future) — programmé pour `dueDate`. -- Au moment où le job tourne (queue `checkins`), il envoie l'email à l'user, marque la task `sent`, mais **ne change PAS le statut de l'invoice côté prod** (TODO V1.5 — bascule en `awaiting_user_confirmation` quand l'email est envoyé). -- L'user a 24h (TTL) pour cliquer un des 2 liens email. Au-delà, la task expire (status `expired`) — elle ne refire pas, mais l'user peut toujours répondre via la modale in-app ou la fiche. +- Au moment où le job tourne (queue `checkins`), il : + 1. Envoie l'email à l'user + 2. Marque la `CheckinTask.status = 'sent'` + 3. **Bump l'`Invoice.status` en `awaiting_user_confirmation`** si encore `pending` (la modale in-app la voit alors) +- L'user a 24h (TTL) pour cliquer un des 2 liens email. Au-delà, la task expire (status `expired`) — elle ne refire pas, mais l'user peut toujours répondre via la modale in-app ou la fiche (le statut reste `awaiting_user_confirmation` tant qu'il n'a pas répondu). ### 5.3 Architecture des `RelanceTask`