Migration clients (uuid id, organization_id FK uuid CASCADE, name, email REQUIS, phone, address, siret, notes). Index sur organization_id. Modèle Client avec belongsTo Organization. La relation hasMany Invoice est volontairement omise tant que le domaine Invoice n'est pas câblé. Validators Vine alignés sur le contrat MSW : - create : name 2-120, email requis avec format, siret 14 chiffres si fourni - update : tout optionnel - email REQUIS au create — pivot produit, pas de relance possible sans Endpoints (auth requise, scopés par organizationId du user courant) : - GET /clients?withStats=1&q= : liste filtrée + recherche, enrichissement stats optionnel, tri par actionnabilité (retards d'abord) quand withStats - GET /clients/:id : détail (id en UUID via router.matchers.uuid()) - POST /clients : 201 + détection doublon par nom case-insensitive → 409 avec payload `existing` (le SPA peut proposer "voir le client existant") - PATCH /clients/:id : merge partiel Service ClientStats avec interface bulkComputeClientStats() qui retourne EMPTY pour l'instant — sera vraiment branché quand Invoice arrive. Le contrat reste stable côté SPA, juste les compteurs à 0. Sérialisation : pour les listes avec stats per-item, on instancie le transformer manuellement (`new ClientTransformer(c).toObject()`) plutôt que de passer par BaseTransformer.transform() qui retourne un Item nested non-unwrappable hors clé directe de serialize().
44 lines
1.2 KiB
TypeScript
44 lines
1.2 KiB
TypeScript
/**
|
|
* Stats agrégées d'un client. Calculées on-the-fly à partir des invoices
|
|
* (V1 : pas de cache, le volume reste raisonnable).
|
|
*
|
|
* Tant que le domaine Invoice n'est pas câblé, on retourne EMPTY pour
|
|
* tous les clients — le contrat reste stable côté SPA.
|
|
*/
|
|
export type ClientStats = {
|
|
invoiceCount: number
|
|
activeInvoiceCount: number
|
|
lateInvoiceCount: number
|
|
paidInvoiceCount: number
|
|
paidLifetimeCents: number
|
|
pendingLifetimeCents: number
|
|
lastActivityAt: string | null
|
|
}
|
|
|
|
export const EMPTY_CLIENT_STATS: ClientStats = {
|
|
invoiceCount: 0,
|
|
activeInvoiceCount: 0,
|
|
lateInvoiceCount: 0,
|
|
paidInvoiceCount: 0,
|
|
paidLifetimeCents: 0,
|
|
pendingLifetimeCents: 0,
|
|
lastActivityAt: null,
|
|
}
|
|
|
|
/**
|
|
* Calcule les stats pour un ensemble de clients d'une org.
|
|
* @returns Map clientId → ClientStats
|
|
*
|
|
* @todo Implémenter quand Invoice arrive — pour l'instant tout le monde a 0.
|
|
*/
|
|
export async function bulkComputeClientStats(
|
|
_organizationId: string,
|
|
clientIds: string[]
|
|
): Promise<Map<string, ClientStats>> {
|
|
const map = new Map<string, ClientStats>()
|
|
for (const id of clientIds) {
|
|
map.set(id, EMPTY_CLIENT_STATS)
|
|
}
|
|
return map
|
|
}
|