import db from '@adonisjs/lucid/services/db' /** * Stats agrégées d'un client. Calculées on-the-fly à partir des invoices * (V1 : pas de cache, le volume reste raisonnable). Si le perf devient un * sujet, on cachera dans Redis avec invalidation post-mutation invoice. */ 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 en une seule * requête agrégée par client_id. Les clients sans facture reçoivent EMPTY. * * @returns Map clientId → ClientStats */ export async function bulkComputeClientStats( organizationId: string, clientIds: string[] ): Promise> { const map = new Map() for (const id of clientIds) { map.set(id, { ...EMPTY_CLIENT_STATS }) } if (clientIds.length === 0) return map const today = new Date() today.setHours(0, 0, 0, 0) const ACTIVE = "('pending','in_relance','awaiting_user_confirmation')" const rows = await db .from('invoices') .where('organization_id', organizationId) .whereIn('client_id', clientIds) .select('client_id') .select(db.raw('count(*)::int as invoice_count')) .select(db.raw(`count(*) filter (where status::text in ${ACTIVE})::int as active_count`)) .select( db.raw( `count(*) filter (where status::text in ${ACTIVE} and due_date < ?)::int as late_count`, [today] ) ) .select(db.raw(`count(*) filter (where status = 'paid')::int as paid_count`)) .select( db.raw(`coalesce(sum(amount_ttc_cents) filter (where status = 'paid'), 0)::int as paid_cents`) ) .select( db.raw( `coalesce(sum(amount_ttc_cents) filter (where status::text in ${ACTIVE}), 0)::int as pending_cents` ) ) .select(db.raw('max(updated_at) as last_activity')) .groupBy('client_id') for (const r of rows) { map.set(r.client_id, { invoiceCount: r.invoice_count, activeInvoiceCount: r.active_count, lateInvoiceCount: r.late_count, paidInvoiceCount: r.paid_count, paidLifetimeCents: r.paid_cents, pendingLifetimeCents: r.pending_cents, lastActivityAt: r.last_activity instanceof Date ? r.last_activity.toISOString() : (r.last_activity as string | null), }) } return map }