import { AppError } from '../errors/app-error'; 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 { return findEnCoursParUtilisateur(utilisateurId); } /* RG07/RG10/RG11/RG12 : création d'un emprunt avec sa checklist de départ. Les contrôles d'éligibilité (campus, disponibilité) sont faits ici ; l'écriture atomique est déléguée au repository. */ export async function creerEmprunt( utilisateurId: number, campusId: number, request: CreerEmpruntRequest, ): Promise { const materiel = await findMaterielById(request.materielId); if (!materiel) { throw new AppError(404, 'Materiel introuvable'); } if (materiel.campusId !== campusId) { throw new AppError(403, 'Materiel rattache a un autre campus'); } if (materiel.statut !== 'DISPONIBLE') { throw new AppError(409, 'Materiel non disponible'); } const poste = await findPosteById(request.posteEmpruntId); if (!poste) { throw new AppError(400, 'Poste introuvable'); } if (poste.sallePret.campusId !== campusId) { throw new AppError(403, 'Poste rattache a un autre campus'); } const dateRetourPrevue = new Date(); dateRetourPrevue.setDate(dateRetourPrevue.getDate() + DUREE_EMPRUNT_JOURS); return creerEmpruntComplet({ utilisateurId, materielId: request.materielId, campusId, sallePretId: poste.sallePretId, posteEmpruntId: request.posteEmpruntId, dateRetourPrevue, modeIdentification: request.modeIdentification, commentaireDepart: request.commentaireDepart, 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 { 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); }