const {onCall} = require("firebase-functions/v2/https"); const admin = require("firebase-admin"); const logger = require("firebase-functions/logger"); /** * Reverts the validation progress of an event to a target step. * Resets subsequent step statuses and validation flags of assigned equipment. * If rolling back from a completed return, it decrements the consumable stock quantities * that were restored during return validation. */ const handler = async (request) => { try { const {auth, data} = request; if (!auth) { throw new Error("L'utilisateur doit être authentifié"); } const {eventId, targetStep} = data; if (!eventId || !targetStep) { throw new Error("eventId et targetStep sont requis"); } const db = admin.firestore(); const eventRef = db.collection("events").doc(eventId); await db.runTransaction(async (transaction) => { const eventDoc = await transaction.get(eventRef); if (!eventDoc.exists) { throw new Error("Événement introuvable"); } const event = eventDoc.data(); const assignedEquipment = event.assignedEquipment || []; // Si le retour était complété et qu'on revient en arrière, on doit annuler la restauration des stocks const shouldRevertStocks = event.stocksRestored === true; if (shouldRevertStocks) { // Charger tous les équipements uniques de l'événement pour ajuster leur stock const equipmentIds = Array.from(new Set(assignedEquipment.map((eq) => eq.equipmentId).filter(Boolean))); const equipmentDocsMap = {}; for (const eqId of equipmentIds) { const eqRef = db.collection("equipments").doc(eqId); const eqDoc = await transaction.get(eqRef); if (eqDoc.exists) { equipmentDocsMap[eqId] = eqDoc.data(); } } for (const eq of assignedEquipment) { const eqId = eq.equipmentId; const equipmentData = equipmentDocsMap[eqId]; if (!equipmentData) continue; const hasQuantity = equipmentData.hasQuantity === true || equipmentData.category === "CABLE" || equipmentData.category === "CONSUMABLE"; if (hasQuantity) { // C'est un consommable, on doit déduire la quantité qui avait été restaurée const qtyAtRet = Number(eq.quantityAtReturn) || 0; if (qtyAtRet > 0) { const eqRef = db.collection("equipments").doc(eqId); const currentAvailable = Number(equipmentData.availableQuantity) || 0; // S'assurer de ne pas descendre en dessous de 0 (ou autoriser le négatif si stock virtuel) const newAvailable = Math.max(0, currentAvailable - qtyAtRet); transaction.update(eqRef, { availableQuantity: newAvailable, updatedAt: admin.firestore.FieldValue.serverTimestamp(), }); logger.info(`[rollbackEventStep] Annulé la restauration de ${qtyAtRet} pour ${eqId}. Ancien stock: ${currentAvailable}, Nouveau stock: ${newAvailable}`); } } } } // Préparer les nouvelles valeurs des étapes let prepStatus = event.preparationStatus; let loadStatus = event.loadingStatus; let unloadStatus = event.unloadingStatus; let retStatus = event.returnStatus; if (targetStep === 'PREPARATION') { prepStatus = 'IN_PROGRESS'; loadStatus = 'NOT_STARTED'; unloadStatus = 'NOT_STARTED'; retStatus = 'NOT_STARTED'; } else if (targetStep === 'LOADING') { loadStatus = 'IN_PROGRESS'; unloadStatus = 'NOT_STARTED'; retStatus = 'NOT_STARTED'; } else if (targetStep === 'UNLOADING') { unloadStatus = 'IN_PROGRESS'; retStatus = 'NOT_STARTED'; } else if (targetStep === 'RETURN') { retStatus = 'IN_PROGRESS'; } else { throw new Error("targetStep invalide. Doit être PREPARATION, LOADING, UNLOADING ou RETURN"); } // Nettoyer les champs de validation des équipements pour les étapes annulées const updatedEquipment = assignedEquipment.map((eq) => { let isPrepared = eq.isPrepared; let isMissingAtPreparation = eq.isMissingAtPreparation; let quantityAtPreparation = eq.quantityAtPreparation; let isLoaded = eq.isLoaded; let isMissingAtLoading = eq.isMissingAtLoading; let quantityAtLoading = eq.quantityAtLoading; let isUnloaded = eq.isUnloaded; let isMissingAtUnloading = eq.isMissingAtUnloading; let quantityAtUnloading = eq.quantityAtUnloading; let isReturned = eq.isReturned; let isMissingAtReturn = eq.isMissingAtReturn; let quantityAtReturn = eq.quantityAtReturn; if (targetStep === 'PREPARATION') { isLoaded = false; isMissingAtLoading = false; quantityAtLoading = null; isUnloaded = false; isMissingAtUnloading = false; quantityAtUnloading = null; isReturned = false; isMissingAtReturn = false; quantityAtReturn = null; } else if (targetStep === 'LOADING') { isUnloaded = false; isMissingAtUnloading = false; quantityAtUnloading = null; isReturned = false; isMissingAtReturn = false; quantityAtReturn = null; } else if (targetStep === 'UNLOADING') { isReturned = false; isMissingAtReturn = false; quantityAtReturn = null; } return { ...eq, isPrepared, isMissingAtPreparation, quantityAtPreparation, isLoaded, isMissingAtLoading, quantityAtLoading, isUnloaded, isMissingAtUnloading, quantityAtUnloading, isReturned, isMissingAtReturn, quantityAtReturn, }; }); // Mettre à jour le document de l'événement transaction.update(eventRef, { preparationStatus: prepStatus, loadingStatus: loadStatus, unloadingStatus: unloadStatus, returnStatus: retStatus, assignedEquipment: updatedEquipment, stocksRestored: false, updatedAt: admin.firestore.FieldValue.serverTimestamp(), }); }); logger.info(`[rollbackEventStep] Événement ${eventId} réinitialisé avec succès à l'étape ${targetStep}`); return {success: true}; } catch (error) { logger.error("[rollbackEventStep] Erreur:", error); throw error; } }; exports.rollbackEventStep = onCall({ cors: true, region: "europe-west9", }, handler); exports.handler = handler;