feat(backend): add emprunt restitution with automatic anomaly detection

This commit is contained in:
SaidSoighiri94
2026-07-06 10:10:09 +02:00
parent 3215a3c11f
commit be7d095fc3
7 changed files with 306 additions and 12 deletions
+124 -1
View File
@@ -1,14 +1,24 @@
import { AppError } from '../errors/app-error';
import { CreerEmpruntRequest } from '../dtos/emprunt.dto';
import {
CreerEmpruntRequest,
RestituerEmpruntRequest,
ChecklistElementRequest,
} from '../dtos/emprunt.dto';
import {
findEnCoursParUtilisateur,
creerEmpruntComplet,
findByIdAvecChecklists,
restituerEmpruntComplet,
EmpruntAvecMateriel,
RestituerEmpruntData,
} from '../repositories/emprunt.repository';
import { findById as findMaterielById } from '../repositories/materiel.repository';
import { findById as findPosteById } from '../repositories/poste-emprunt.repository';
import { findResponsablesParCampus } from '../repositories/utilisateur.repository';
import { StatutEmprunt } from '../models/enums';
const DUREE_EMPRUNT_JOURS = 14;
const STATUTS_RESTITUABLES: readonly string[] = ['EN_COURS', 'EN_RETARD'];
/* RG15 : un étudiant ne consulte que ses propres emprunts non restitués. */
export function listerMesEmpruntsEnCours(utilisateurId: number): Promise<EmpruntAvecMateriel[]> {
@@ -57,3 +67,116 @@ export async function creerEmprunt(
elements: request.elements,
});
}
interface ElementDepart {
accessoireId: number | null;
nomElement: string;
etat: string;
}
interface ResultatComparaison {
conforme: boolean;
auMoinsUnDeteriore: boolean;
details: string[];
}
function trouverRetour(
depart: ElementDepart,
retour: ChecklistElementRequest[],
): ChecklistElementRequest | undefined {
if (depart.accessoireId !== null) {
const parId = retour.find((element) => element.accessoireId === depart.accessoireId);
if (parId) {
return parId;
}
}
return retour.find((element) => element.nomElement === depart.nomElement);
}
/* RG17 : compare la checklist de départ à celle de retour. Un élément présent au
départ mais absent ou détérioré au retour rend la restitution non conforme (RG20). */
function comparerChecklists(
depart: ElementDepart[],
retour: ChecklistElementRequest[],
): ResultatComparaison {
const details: string[] = [];
let auMoinsUnDeteriore = false;
for (const elementDepart of depart) {
if (elementDepart.etat !== 'PRESENT') {
continue;
}
const elementRetour = trouverRetour(elementDepart, retour);
if (!elementRetour || elementRetour.etat === 'ABSENT') {
details.push(`${elementDepart.nomElement} manquant au retour`);
} else if (elementRetour.etat === 'DETERIORE') {
details.push(`${elementDepart.nomElement} deteriore au retour`);
auMoinsUnDeteriore = true;
}
}
return { conforme: details.length === 0, auMoinsUnDeteriore, details };
}
/* RG14-RG20 : restitution d'un emprunt avec comparaison des checklists, clôture
ou passage en non conforme, et création automatique d'anomalie + notifications. */
export async function restituerEmprunt(
utilisateurId: number,
empruntId: number,
request: RestituerEmpruntRequest,
): Promise<EmpruntAvecMateriel> {
const emprunt = await findByIdAvecChecklists(empruntId);
if (!emprunt) {
throw new AppError(404, 'Emprunt introuvable');
}
if (emprunt.utilisateurId !== utilisateurId) {
throw new AppError(403, 'Cet emprunt ne vous appartient pas');
}
if (!STATUTS_RESTITUABLES.includes(emprunt.statut)) {
throw new AppError(409, 'Emprunt non restituable');
}
const checklistDepart = emprunt.checklists.find((checklist) => checklist.type === 'DEPART');
if (!checklistDepart) {
throw new AppError(409, 'Checklist de depart introuvable');
}
const comparaison = comparerChecklists(
checklistDepart.elements.map((element) => ({
accessoireId: element.accessoireId,
nomElement: element.nomElement,
etat: element.etat,
})),
request.elements,
);
const statutEmprunt: StatutEmprunt = comparaison.conforme ? 'CLOTURE' : 'RETOUR_NON_CONFORME';
let statutMateriel = 'DISPONIBLE';
if (!comparaison.conforme) {
statutMateriel = comparaison.auMoinsUnDeteriore ? 'DETERIORE' : 'NON_CONFORME';
}
const donnees: RestituerEmpruntData = {
empruntId,
utilisateurId,
materielId: emprunt.materielId,
statutEmprunt,
statutMateriel,
modeIdentification: request.modeIdentification,
commentaireRetour: request.commentaireRetour,
elements: request.elements,
};
if (!comparaison.conforme) {
const responsables = await findResponsablesParCampus(emprunt.campusId);
donnees.anomalie = {
type: 'RETOUR_NON_CONFORME',
description: `Retour non conforme : ${comparaison.details.join(', ')}.`,
responsablesIds: responsables.map((responsable) => responsable.id),
};
}
return restituerEmpruntComplet(donnees);
}