Skip to main content

Journal d'activité (activity_log)

Dernière vérification contre le code

Vérifié le 2026-08-12 contre activity-log.service.js — exact.

Historique des modifications faites par le backoffice sur les objets métier (demandes de prêt, et progressivement les autres — voir "Élargir à un nouveau contrôleur"). Table générique polymorphe : une seule table sert pour tous les types d'objets, distingués par objet_type/objet_id.

Schéma

model activity_log {
id_activity_log String @id @default(uuid()) @db.Uuid
user_id String? @db.Uuid
action String? @db.VarChar(50)
objet_type String? @db.VarChar(50)
objet_id String? @db.VarChar(50)
meta Json?
created_at DateTime @default(now())
user users? @relation(fields: [user_id], references: [id_users])

@@index([user_id])
@@index([objet_id])
@@index([created_at(sort: Desc)])
}

objet_id est en varchar, pas uuid : les entités métier de ce projet n'utilisent pas toutes le même type de clé primaire (loan_requests.id_loan_requests et documents.id_documents sont des BigInt, agence.id_agence est un Int, seul users.id_users est un Uuid). Pas de contrainte FK réelle sur objet_id — normal, il pointe vers des tables différentes selon objet_type.

Pourquoi un diff plutôt qu'un snapshot

Stocker uniquement les champs qui ont changé ({ champ: { before, after } }) plutôt que l'état complet avant/après :

  • Répond directement à la question "qu'est-ce qui a changé exactement", sans avoir à comparer deux snapshots complets à la main.
  • Reste lisible même avec beaucoup de champs sur l'objet (loan_requests en a une vingtaine).
  • Évite de dupliquer des données sensibles (salaire, téléphone...) dans chaque entrée d'historique alors qu'elles n'ont pas bougé.

Le service (src/services/activity-log.service.js)

logActivity({ user_id, action, objet_type, objet_id, meta })

Insère une ligne. Toujours best-effort : enveloppé dans un try/catch interne, ne fait que console.error en cas d'échec. Ne doit jamais faire échouer l'opération métier qui l'appelle — un problème de journalisation ne doit pas empêcher un gestionnaire de sauvegarder son travail.

diffFields(before, after, keys)

Ne garde que les clés de keys dont la valeur a réellement changé entre before (objet chargé depuis la base avant modification) et after (valeurs soumises dans la requête). Si after[key] est undefined (champ absent du body de la requête), il est ignoré — ça permet à after d'être construit directement depuis les champs destructurés de req.body, sans avoir à filtrer manuellement ce qui a été envoyé.

normalizeForDiff (interne) gère les pièges de comparaison :

  • Dates : une Date Prisma (1990-01-01T00:00:00.000Z) doit matcher la chaîne "1990-01-01" envoyée par un formulaire — comparaison sur la partie date uniquement.
  • Decimal Prisma : comparé par sa valeur numérique (.toNumber()), pas par sa représentation textuelle. Sans ça, un Decimal("420000.00") (valeur en base) et un Number(420000) (valeur envoyée par le frontend) sont vus comme différents alors qu'ils représentent le même montant — bug réel rencontré et corrigé en pratique sur loan_requests.amount/salaire/charge_mensuel/debt_ratio.
  • Nombres/chaînes numériques : comparés par valeur numérique ("150000" vs 150000 doivent matcher).
  • Objets/tableaux : sérialisés en JSON pour comparaison.

getActivityLog(objetType, objetId)

Récupère les entrées pour un objet donné, triées created_at desc, avec le nom/email de l'utilisateur (jointure sur users). Utilisé par les panneaux d'historique intégrés à un objet précis (ex : Fiche détail prêt).

listActivity(...) / listObjectTypes()

Utilisés par la page globale "Journal d'activité" (SUPER_ADMIN uniquement) : liste paginée, filtrable par type d'objet / utilisateur / action / période (objetType/action acceptent une valeur unique ou une liste séparée par des virgules). listObjectTypes() peuple dynamiquement le filtre "type d'objet" depuis les valeurs réellement présentes en base, plutôt qu'une liste statique qui se désynchroniserait à chaque nouvelle entité branchée au système.

Lecture : route générique

GET /api/activity-log/:objetType/:objetId (protégée par verifyToken) — volontairement générique : un nouveau type d'objet n'a besoin d'aucune nouvelle route, juste d'un nouvel appel avec un autre objetType.

Exemple frontend (déjà utilisé dans LoanRequestDetailView.vue) :

const res = await api.get(`/activity-log/loan_request/${id}`)

Élargir à un nouveau contrôleur

Trois étapes, en suivant le modèle de loan.controller.js/documents.controller.js :

  1. Importer le service : const { logActivity, diffFields } = require('../services/activity-log.service');
  2. Pour une création : décider si elle mérite d'être loggée. Pour les demandes de prêt, la création n'est pas loggée (c'est le client public qui soumet, pas un gestionnaire — rien à auditer côté staff). Pour un objet créé par un gestionnaire (ex : un nouvel utilisateur via users.controller.js), logger avec meta = un résumé des champs clés (pas de diff possible, il n'y a pas d'"avant").
  3. Pour une modification : le contrôleur doit déjà avoir (ou aller chercher) l'état avant modification — prérequis pour calculer un diff utile. Si le contrôleur ne fait qu'un update() sans avoir relu l'objet avant, il faut ajouter ce findUnique/findFirst au préalable (voir updateLoanStatus pour un exemple complet).
  4. objet_type : choisir un identifiant stable et cohérent avec ce qui sera utilisé côté frontend pour la lecture (ex : 'user', 'role', 'agence'). user_id = req.user?.id (déjà présent dans le token décodé par verifyToken).

Ordre de priorité recommandé pour la suite

  1. account_requests.controller.js (même nature que les prêts, pattern quasi identique).
  2. users.controller.js (critique sécurité : accès, changement de rôle).
  3. role.controller.js (modification des permissions elles-mêmes).
  4. agence.controller.js / zone.controller.js (données structurelles/routage).
  5. site_settings.controller.js, loan_parameters.controller.js, loan_document_requirements.controller.js, loan_field_requirements.controller.js (configuration des formulaires publics).
  6. Le reste (contenu vitrine : bannières, pages, publications...) — enjeu bien plus faible, en dernier.

auth.controller.js est volontairement exclu de ce système : une tentative de connexion n'est pas une "modification d'objet", ce serait un autre type de journal (sécurité/accès) si besoin un jour.