fix(checkin): bump invoice.status pending → awaiting_user_confirmation
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m6s

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 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-07 17:42:52 +02:00
parent 023f08c261
commit ab75f1f979
2 changed files with 27 additions and 5 deletions

View File

@ -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'
)
}
}

View File

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