'use strict'; const moment = require('moment-timezone'); const db = require('../../models/index'); const { incidentType, unlockedIncidentLevelsPrices } = require('../../constants/constants'); const { getUnlockEntryForReservation, getRelatedDoorLockEntries } = require('../doorLock/doorLock'); const { getFirstPreviousBooking, getFirstNextBooking, getAllFinishedBookings } = require('../officeRnD/bookings'); const getSortedIncidentsForMember = (memberId) => { const attributes = ['bookingStart', 'incidentLevel', 'incidentLevelPrice']; const filters = { memberId }; const order = [['bookingStart', 'DESC']]; return db.unlockedIncident.findAll({ attributes, where: filters, order, }) }; const createUnlockedIncident = (reservation) => { return new Promise((resolve, reject) => { const { reservationId, memberId, resourceId, start, end } = reservation; getLastIncidentForMember(memberId) .then(incidents => { const lastIncident = incidents && incidents[0] ? incidents[0] : undefined; const incident = { reservationId, memberId, resourceId, bookingStart: start, bookingEnd: end, incidentLevel: null, incidentLevelPrice: null, }; console.log('=> UNLOCKED INCIDENT'); console.log('\tMember : ', memberId); console.log('\tStart : ', start); console.log('\tEnd : ', end); console.log('\tMore details : '); /* if (lastIncident){ const lastIncidentLevel = lastIncident.incidentLevel; const lastIncidentBeginningOfTheMonth = moment(lastIncident.bookingStart).startOf('month'); const beginningOfTheMonth = moment.utc().startOf('month'); const timePassedFromLastIncident = Math.abs(beginningOfTheMonth.diff(lastIncidentBeginningOfTheMonth, 'months')); if (timePassedFromLastIncident >= 6){ console.log('\t\t-> This is first incident for this member in last 6 months'); incident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title; incident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price; } else { console.log('\t\t-> This member had incident(s) in past 6 months !!!'); incident.incidentLevel = lastIncidentLevel; incident.incidentLevelPrice = unlockedIncidentLevelsPrices[lastIncidentLevel].price; } console.log('\t\tLast incident details : '); console.log('\t\tStart : ', lastIncident.bookingStart); console.log('\t\tCalculated diff : ', timePassedFromLastIncident); console.log('\t\t------------------'); console.log('\tNew incident level : ', incident.incidentLevel); } else { console.log('\t\tThis is first incident for this member, EVER !'); incident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title; incident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price; } */ db.unlockedIncident.findOrCreate({ where: { reservationId, memberId, resourceId, bookingStart: start, bookingEnd: end, }, defaults: { ...incident } }) .then(()=>resolve()) .catch((error)=>reject(error)); }) .catch((error) => { reject(error); }); }); }; const createUnscheduledUseIncident = (reservation, doorLockEntry) => { return new Promise((resolve, reject) => { const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5; const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5; const reservationEndTime = moment(reservation.end); const lockedTime = moment(doorLockEntry.timestamp); const timeDifference = Math.abs(reservationEndTime.diff(lockedTime, 'minutes')); const timeIntervalsToCharge = Math.floor(timeDifference / timeResolution); const totalChargeFee = timeIntervalsToCharge * chargePrice; if (timeIntervalsToCharge > 0){ const incident = { reservationId: reservation.reservationId, memberId: reservation.memberId, resourceId: reservation.resourceId, bookingStart: reservation.start, bookingEnd: reservation.end, doorLockEventTimestamp: doorLockEntry.timestamp, doorLockEventType: doorLockEntry.event, chargePrice, timeIntervalsToCharge, totalChargeFee, }; db.unscheduledIncident.findOrCreate({where: {...incident}, defaults: {...incident}}) .then(()=>resolve()) .catch((error)=>reject(error)); }else{ resolve(); } }); }; const createDoorLockIncident = (reservation, doorLockEntry) => { return new Promise((resolve, reject) => { if (!doorLockEntry){ // Check if there is unlock entry for this reservation getUnlockEntryForReservation(reservation) .then((unlockEntry) => { if (!unlockEntry){ // check if there is back-to-back booking before current one getFirstPreviousBooking(reservation) .then((previousReservation) => { if (previousReservation){ const previousReservationEnd = moment(previousReservation.end); const currentReservationStart = moment(reservation.start); const timeDifference = Math.abs(currentReservationStart.diff(previousReservationEnd, 'minutes')); const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0; if (timeDifference <= maxBackToBackDifference) { createUnlockedIncident(reservation) .then(() => resolve()) .catch((error) => reject(error)); }else{ resolve(); } }else{ resolve(); } }) .catch((error)=>reject(error)); }else { createUnlockedIncident(reservation) .then(()=>resolve()) .catch((error)=>reject(error)); } }) .catch((error) => { reject(error); }); }else{ createUnscheduledUseIncident(reservation, doorLockEntry) .then(()=>resolve()) .catch((error) => reject(error)); } }); }; const insertUnscheduledIncidents = (incidents) => { const asyncJobs = []; incidents.forEach((incident) => { const { reservation, lockEntry, chargePrice, timeIntervalsToCharge, totalChargeFee } = incident; const { reservationId, memberId, resourceId, start, end } = reservation; const { timestamp, event } = lockEntry; const incidentForDB = { reservationId, memberId, resourceId, bookingStart: start, bookingEnd: end, doorLockEventTimestamp: timestamp, doorLockEventType: event, chargePrice, timeIntervalsToCharge, totalChargeFee, }; asyncJobs.push(db.unscheduledIncident.findOrCreate({ where: { reservationId, memberId, resourceId, bookingStart: start, bookingEnd: end, doorLockEventTimestamp: timestamp, doorLockEventType: event }, defaults: {...incidentForDB}, })); }); return Promise.all(asyncJobs); }; const insertUnlockedIncidents = (incidents) => { const asyncJobs = []; incidents.forEach((incident) => { const { reservationId, memberId, resourceId, bookingStart, bookingEnd } = incident; asyncJobs.push(db.unlockedIncident.findOrCreate({ where: { reservationId, memberId, resourceId, bookingStart, bookingEnd, }, defaults: {...incident}, })); }); return Promise.all(asyncJobs); }; const setUnlockedIncidentsLevel = (incidentReservations) => { return new Promise ((resolve, reject) => { const sortingFunction = (reservationA, reservationB) => { const sortCondition = moment.utc(reservationA.start).isBefore(moment.utc(reservationB.start)); return sortCondition ? -1 : 1; }; incidentReservations.sort(sortingFunction); const membersLastIncident = {}; incidentReservations.forEach((reservation) => { membersLastIncident[reservation.memberId] = { incidentLevel: null, incidentTimestamp: null, }; }); const asyncJobs = []; Object.keys(membersLastIncident).forEach((memberId) => { asyncJobs.push(getSortedIncidentsForMember(memberId)); }); Promise.all(asyncJobs) .then((results) => { results.forEach((result) => { const lastIncident = result && result[0] ? result[0] : null; if (lastIncident) { membersLastIncident[lastIncident.memberId] = { incidentLevel: lastIncident.incidentLevel, incidentTimestamp: lastIncident.bookingStart, } } }); const incidentsWithLevel = []; incidentReservations.forEach((reservation) => { const memberLastIncident = membersLastIncident[reservation.memberId]; const incident = { reservationId: reservation.reservationId, memberId: reservation.memberId, resourceId: reservation.resourceId, bookingStart: reservation.start, bookingEnd: reservation.end, incidentLevel: undefined, incidentLevelPrice: undefined, }; if (!memberLastIncident.incidentLevel) { incident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title; incident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price; } else { const lastIncidentTime = moment.utc(memberLastIncident.incidentTimestamp).startOf('month'); const currentIncidentTime = moment.utc(reservation.start).startOf('month'); const timeDiff = Math.abs(lastIncidentTime.diff(currentIncidentTime, 'months')); if (timeDiff >= (parseInt(process.env.UNLOCK_STREAK_REPAIR_AFTER) || 6)){ incident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title; incident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price; } else { const lastIncidentLevelId = unlockedIncidentLevelsPrices[memberLastIncident.incidentLevel].id; const maxId = 5; if ((lastIncidentLevelId && (lastIncidentLevelId >= maxId)) || (timeDiff === 0)){ incident.incidentLevel = memberLastIncident.incidentLevel; incident.incidentLevelPrice = unlockedIncidentLevelsPrices[incident.incidentLevel].price; } else { const nextId = lastIncidentLevelId + 1; Object.keys(unlockedIncidentLevelsPrices).forEach((key) => { if (unlockedIncidentLevelsPrices[key].id === nextId){ incident.incidentLevel = unlockedIncidentLevelsPrices[key].title; incident.incidentLevelPrice = unlockedIncidentLevelsPrices[key].price } }); } } } memberLastIncident.incidentLevel = incident.incidentLevel; memberLastIncident.incidentTimestamp = incident.bookingStart; incidentsWithLevel.push(incident); }); resolve(incidentsWithLevel); }) .catch((error) => reject(error)); }); }; const getIncidentData = (reservation) => { return new Promise ((resolve, reject) => { getFirstNextBooking(reservation) .then(nextReservation => { const endOfTheDay = moment.tz(reservation.end, reservation.timezone).endOf('Day').toISOString(); let doorLockEntriesEndTime = nextReservation ? nextReservation.start : endOfTheDay; if (nextReservation){ // Check if next reservations is immediately after (back to back reservation) // If yes, then there is no need to check door lock entries related to this booking const firstReservationEnd = moment(reservation.end); const secondReservationStart = moment(nextReservation.start); const timeDifference = Math.abs(secondReservationStart.diff(firstReservationEnd, 'minutes')); const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0; if (timeDifference <= maxBackToBackDifference){ // It is back to back reservation, no need to check door lock entries resolve({ incidentType: incidentType.NOT_AN_INCIDENT, }); return; } } // Find door lock entries related to this member, between booking start time and // next booking start time OR end of the day getRelatedDoorLockEntries(reservation.start, doorLockEntriesEndTime, reservation.memberId, reservation.resourceId) .then((lockEntry) => { if (lockEntry){ const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5 const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5; const reservationEndTime = moment(reservation.end); const lockedTime = moment(lockEntry.timestamp); const timeDifference = Math.abs(reservationEndTime.diff(lockedTime, 'minutes')); const timeIntervalsToCharge = Math.floor(timeDifference / timeResolution); const totalChargeFee = timeIntervalsToCharge * chargePrice; if (timeIntervalsToCharge > 0){ resolve({ incidentType: incidentType.UNSCHEDULED_INCIDENT, reservation, lockEntry, chargePrice, timeIntervalsToCharge, totalChargeFee, }) } else { resolve({ incidentType: incidentType.NOT_AN_INCIDENT, }); } } else { // Check if there is unlock entry for this reservation getUnlockEntryForReservation(reservation) .then((unlockEntry) => { if (unlockEntry){ // This is unlocked incident resolve({ incidentType: incidentType.UNLOCKED_INCIDENT, reservation, }); }else{ // Check if there is back-to-back booking before current one getFirstPreviousBooking(reservation) .then((previousReservation) => { if (previousReservation){ const previousReservationEnd = moment(previousReservation.end); const currentReservationStart = moment(reservation.start); const timeDifference = Math.abs(currentReservationStart.diff(previousReservationEnd, 'minutes')); const maxBackToBackDifference = parseInt(process.env.MAX_BACK_TO_BACK_DIFFERENCE) || 0; if (timeDifference <= maxBackToBackDifference) { resolve({ incidentType: incidentType.UNLOCKED_INCIDENT, reservation, }); }else{ resolve({ incidentType: incidentType.NOT_AN_INCIDENT, }); } }else{ resolve({ incidentType: incidentType.NOT_AN_INCIDENT, }); } }) .catch((error) => { console.log('Error finding first previous reservation', error); resolve({ error, }); }); } }) .catch((error) => { console.log('Error finding unlock entry', error); resolve({ error }); }); } }) .catch((error) => { console.log('Error finding related door lock entry', error); resolve({ error, }); }); }) .catch((error) => { console.log('Error finding first next booking', error); resolve({ error, }); }); }); }; const calculateDoorLockCharges = () => { getAllFinishedBookings() .then((reservations) => { const unlockedIncidents = []; const unscheduledIncidents = []; const asyncCheckForIncidents = []; reservations.forEach((reservation) => { asyncCheckForIncidents.push(getIncidentData(reservation)); }); Promise.all(asyncCheckForIncidents) .then((results) => { results.forEach((result) => { if (result.error){ console.log('Error checking incident : ', result.error); }else if(result.incidentType) { switch (result.incidentType) { case incidentType.UNLOCKED_INCIDENT: unlockedIncidents.push(result.reservation); break; case incidentType.UNSCHEDULED_INCIDENT: const { reservation, lockEntry, chargePrice, timeIntervalsToCharge, totalChargeFee } = result; unscheduledIncidents.push({ reservation, lockEntry, chargePrice, timeIntervalsToCharge, totalChargeFee, }); break; } } }); insertUnscheduledIncidents(unscheduledIncidents) .catch((error) => console.log(error)); setUnlockedIncidentsLevel(unlockedIncidents) .then((completedUnlockedIncidents) => { insertUnlockedIncidents(completedUnlockedIncidents) .catch((error) => console.log(error)); }) .catch((error) => console.log(error)); }) .catch((error) => console.log(error)); }) .catch((error) => console.log(error)); }; module.exports = { calculateDoorLockCharges };