import { DateTime } from 'luxon' import Organization from '#models/organization' /** * Clock — abstraction time-sensitive de l'app. * * En prod (`org.demoMode = false`, ou pas d'orgId fourni), retourne * `DateTime.utc()`. C'est l'API à utiliser **par défaut** dans tous les * services qui font des comparaisons de dates (relance scheduler, * checkin scheduler, dashboard KPIs, jobs BullMQ). * * En mode démo (`org.demoMode = true` ET `org.virtualNow != null`), * retourne le `virtualNow` stocké sur l'org. C'est ce qui permet * "d'avancer dans le temps" pendant une démo sans toucher au système. * * **Garde-fou prod** : si `orgId` est absent, on retourne toujours * l'horloge système — on ne fait JAMAIS un side-effect demoMode si * on n'a pas explicitement le contexte d'une org. * * Cette abstraction sert aussi aux tests (on pourra mock plus tard * via un singleton injectable, V2). En V1, on lit la DB à chaque * appel : c'est fonctionnel, et le cache n'a pas de sens vu que la * valeur peut bouger pendant une démo. * * Cache : on prend un cache mémoire très court (250ms) pour ne pas * spammer la DB quand un même handler appelle clock.now() plusieurs * fois. Invalidé à chaque écriture (cf. setVirtualNow). */ type CachedClock = { demoMode: boolean virtualNow: DateTime | null fetchedAt: number } const CACHE_TTL_MS = 250 const cache = new Map() async function loadOrg(orgId: string): Promise { const cached = cache.get(orgId) if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) return cached const org = await Organization.find(orgId) const entry: CachedClock = { demoMode: org?.demoMode ?? false, virtualNow: org?.virtualNow ?? null, fetchedAt: Date.now(), } cache.set(orgId, entry) return entry } /** * Heure courante. Async pour permettre la lecture DB en mode démo. * * const now = await clock.now(invoice.organizationId) * * Si `orgId` est `null` / `undefined`, retourne `DateTime.utc()` * synchronement-compatible (pas de side-effect démo). */ export async function now(orgId?: string | null): Promise { if (!orgId) return DateTime.utc() const c = await loadOrg(orgId) if (c.demoMode && c.virtualNow) return c.virtualNow return DateTime.utc() } /** * Variante sync — utile dans des chemins où on n'a pas d'orgId * (création de tokens auth par exemple). Comportement strictement * identique à `DateTime.utc()`. Existe pour faciliter une migration * incrémentale : remplacer `DateTime.utc()` ou `DateTime.now()` par * `clockSync()` rend explicite que c'est une horloge système. */ export function nowSync(): DateTime { return DateTime.utc() } /** * Met à jour `virtual_now` sur une org (utilisé par /demo/tick). * Invalide le cache pour que le prochain `now(orgId)` renvoie la * nouvelle valeur sans attendre le TTL. */ export async function setVirtualNow(orgId: string, virtualNow: DateTime): Promise { const org = await Organization.findOrFail(orgId) org.virtualNow = virtualNow await org.save() cache.delete(orgId) } /** * Active/désactive le mode démo. Côté `start`, on initialise * `virtualNow` à maintenant (UTC) si pas déjà set. */ export async function setDemoMode(orgId: string, enabled: boolean): Promise { const org = await Organization.findOrFail(orgId) org.demoMode = enabled if (enabled && !org.virtualNow) { org.virtualNow = DateTime.utc() } if (!enabled) { org.virtualNow = null } await org.save() cache.delete(orgId) } /** Invalidation manuelle (tests). */ export function clearClockCache(): void { cache.clear() }