rubis/apps/web/src/lib/checkin.ts
ordinarthur 92a9fac62b feat(checkin): modale in-app pour confirmer le paiement au login
Permet à l'user de répondre aux check-ins directement dans l'app, sans
passer par les liens email. Au mount du layout `_app`, on liste les
factures en `awaiting_user_confirmation` et on les présente une par une
dans une modale séquentielle :

  - "Oui, payée"     → mark paid + bonus rubis + cancel relances
  - "Non, en attente" → schedule relances + status → in_relance
  - "Plus tard"       → skip session-only

3 endpoints auth-protected sous /api/v1/checkin/inapp/ (déclarés AVANT
le groupe public à token sinon /:token/pending mange /inapp/pending).

La modale fait toujours confiance au serveur : queue = pending refetch,
display = queue[0], pas de cursor manuel — sinon on saute des factures
quand le serveur retire la réponse précédente.

Wording rassurant : "Aucune relance ne part sans votre validation".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 12:37:09 +02:00

63 lines
2.2 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { queryKeys } from "@/lib/queryKeys";
import type { InvoiceStatus } from "@rubis/shared";
/**
* Forme minimale renvoyée par GET /api/v1/checkin/inapp/pending — basée sur
* InvoiceTransformer côté API. On ne garde que ce dont la modale a besoin.
*/
export type PendingCheckinInvoice = {
id: string;
numero: string;
amountTtcCents: number;
issueDate: string;
dueDate: string;
status: InvoiceStatus;
clientName: string;
planName: string | null;
};
/** Liste des factures en attente de check-in pour l'org courante. */
export function usePendingCheckins() {
return useQuery({
queryKey: queryKeys.checkin.pending(),
queryFn: () =>
api.get<PendingCheckinInvoice[]>("/api/v1/checkin/inapp/pending"),
// Pas de polling — la liste change uniquement quand l'user répond ou
// qu'une nouvelle invoice arrive en awaiting_user_confirmation. On
// refetch sur mount + sur invalidate.
staleTime: 30_000,
});
}
/** Mutation : "oui, payée" — délègue à l'endpoint inappRespondPaid. */
export function useCheckinPaid() {
const qc = useQueryClient();
return useMutation({
mutationFn: (invoiceId: string) =>
api.post(`/api/v1/checkin/inapp/${invoiceId}/paid`),
onSuccess: () => {
// Tout l'écosystème dépend du statut : invalidate large (factures,
// dashboard KPIs, timeseries, pipeline, counts).
void qc.invalidateQueries({ queryKey: queryKeys.checkin.pending() });
void qc.invalidateQueries({ queryKey: queryKeys.invoices.all() });
void qc.invalidateQueries({ queryKey: queryKeys.dashboard.all() });
},
});
}
/** Mutation : "non, toujours impayée" — programme les relances. */
export function useCheckinStillPending() {
const qc = useQueryClient();
return useMutation({
mutationFn: (invoiceId: string) =>
api.post(`/api/v1/checkin/inapp/${invoiceId}/pending`),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: queryKeys.checkin.pending() });
void qc.invalidateQueries({ queryKey: queryKeys.invoices.all() });
void qc.invalidateQueries({ queryKey: queryKeys.dashboard.all() });
},
});
}