508 lines
25 KiB
JavaScript
508 lines
25 KiB
JavaScript
'use strict';
|
|
|
|
const moment = require('moment-timezone');
|
|
const db = require('../../models/index');
|
|
|
|
const { doorLockEvents, incidentType, unlockedIncidentLevelsPrices, UNSCHEDULED_CHARGE_PRICE, MAX_BACK_TO_BACK_DIFFERENCE, UNSCHEDULED_TIME_RESOLUTION } = require('../../constants/constants');
|
|
const { getUnlockEntryForReservation, getLockEntryForReservation, getEntriesBetween, getLastEntryForReservation } = require('../doorLock/doorLock');
|
|
const { getAllFinishedBookings, getFirstPreviousBooking, getFirstNextBooking } = 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 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 analyseReservation = (reservation) => {
|
|
return new Promise ((resolve, reject) => {
|
|
const getNearBookingsAsync = [
|
|
getFirstPreviousBooking(reservation),
|
|
getFirstNextBooking(reservation)
|
|
];
|
|
|
|
Promise.all(getNearBookingsAsync)
|
|
.then(([previousReservation, nextReservation]) => {
|
|
const getRelatedDoorLockEntriesAsync = [
|
|
getUnlockEntryForReservation(reservation, previousReservation),
|
|
getLockEntryForReservation(reservation, nextReservation)
|
|
];
|
|
|
|
Promise.all(getRelatedDoorLockEntriesAsync)
|
|
.then(([unlockEntry, lockEntry, entriesBetween]) => {
|
|
const currentReservationStart = moment.utc(reservation.start);
|
|
const currentReservationEnd = moment.utc(reservation.end);
|
|
|
|
let timeDifferenceFromPreviousReservation = MAX_BACK_TO_BACK_DIFFERENCE + 100;
|
|
if (previousReservation) {
|
|
const previousReservationEnd = moment.utc(previousReservation.end);
|
|
timeDifferenceFromPreviousReservation = currentReservationStart.diff(previousReservationEnd, 'minutes');
|
|
}
|
|
const previousReservationIsBackToBack = timeDifferenceFromPreviousReservation < MAX_BACK_TO_BACK_DIFFERENCE;
|
|
|
|
|
|
let timeDifferenceBeforeNextReservation = MAX_BACK_TO_BACK_DIFFERENCE + 100;
|
|
if (nextReservation) {
|
|
const nextReservationStart = moment.utc(nextReservation.start);
|
|
timeDifferenceBeforeNextReservation = nextReservationStart.diff(currentReservationEnd, 'minutes');
|
|
}
|
|
const nextReservationIsBackToBack = timeDifferenceBeforeNextReservation < MAX_BACK_TO_BACK_DIFFERENCE;
|
|
|
|
let timeDifferenceFromUnlockEntry = 0;
|
|
if (unlockEntry) {
|
|
const unlockTime = moment.utc(unlockEntry.timestamp);
|
|
timeDifferenceFromUnlockEntry = currentReservationStart.diff(unlockTime, 'minutes');
|
|
}
|
|
const timeIntervalsToChargeBefore = Math.floor(timeDifferenceFromUnlockEntry / UNSCHEDULED_TIME_RESOLUTION);
|
|
const totalChargeFeeBefore = timeIntervalsToChargeBefore * UNSCHEDULED_CHARGE_PRICE;
|
|
const chargeBefore = totalChargeFeeBefore > 0;
|
|
|
|
let timeDifferenceFromLockEntry = 0;
|
|
if (lockEntry) {
|
|
const lockTime = moment.utc(lockEntry.timestamp);
|
|
timeDifferenceFromLockEntry = lockTime.diff(currentReservationEnd, 'minutes');
|
|
}
|
|
const timeIntervalsToChargeAfter = Math.floor(timeDifferenceFromLockEntry / UNSCHEDULED_TIME_RESOLUTION);
|
|
const totalChargeFeeAfter = timeIntervalsToChargeAfter * UNSCHEDULED_CHARGE_PRICE;
|
|
const chargeAfter = totalChargeFeeAfter > 0;
|
|
|
|
const result = {
|
|
currentReservation: reservation,
|
|
previousReservation,
|
|
nextReservation,
|
|
unlockEntry,
|
|
lockEntry,
|
|
previousReservationIsBackToBack,
|
|
nextReservationIsBackToBack,
|
|
timeDifferenceFromPreviousReservation,
|
|
timeDifferenceBeforeNextReservation,
|
|
timeDifferenceFromUnlockEntry,
|
|
timeDifferenceFromLockEntry,
|
|
chargeBefore,
|
|
chargeAfter,
|
|
totalChargeFeeBefore,
|
|
totalChargeFeeAfter,
|
|
timeIntervalsToChargeBefore,
|
|
timeIntervalsToChargeAfter,
|
|
};
|
|
|
|
resolve(result);
|
|
})
|
|
.catch((error) => reject(error));
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
const getIncidentData = (reservation) => {
|
|
return new Promise ((resolve, reject) => {
|
|
analyseReservation(reservation)
|
|
.then((result) => {
|
|
const {
|
|
currentReservation,
|
|
previousReservation,
|
|
nextReservation,
|
|
unlockEntry,
|
|
lockEntry,
|
|
previousReservationIsBackToBack,
|
|
nextReservationIsBackToBack,
|
|
timeDifferenceFromPreviousReservation,
|
|
timeDifferenceBeforeNextReservation,
|
|
timeDifferenceFromUnlockEntry,
|
|
timeDifferenceFromLockEntry,
|
|
chargeBefore,
|
|
chargeAfter,
|
|
totalChargeFeeBefore,
|
|
totalChargeFeeAfter,
|
|
timeIntervalsToChargeBefore,
|
|
timeIntervalsToChargeAfter,
|
|
} = result;
|
|
const incidents = [];
|
|
const incidentsAsyncJobs = [];
|
|
const { resourceId } = currentReservation;
|
|
|
|
// 0a. Check for unscheduled use between current and previous reservation
|
|
const analysePreviousJob = [];
|
|
if (previousReservation && !previousReservationIsBackToBack){
|
|
analysePreviousJob.push(analyseReservation(previousReservation));
|
|
}else {
|
|
analysePreviousJob.push(undefined);
|
|
}
|
|
|
|
incidentsAsyncJobs.push(
|
|
Promise.all(analysePreviousJob)
|
|
.then(([previousReservationResults]) => {
|
|
let fromTimestamp;
|
|
let toTimestamp;
|
|
|
|
if (previousReservationResults){
|
|
const previousReservationLockEntry = previousReservationResults.lockEntry;
|
|
|
|
fromTimestamp = previousReservationLockEntry && previousReservationLockEntry.timestamp ?
|
|
previousReservationLockEntry.timestamp : previousReservation.end;
|
|
toTimestamp = unlockEntry && unlockEntry.timestamp ?
|
|
unlockEntry.timestamp : reservation.start;
|
|
}else{
|
|
fromTimestamp = undefined;
|
|
toTimestamp = unlockEntry && unlockEntry.timestamp ?
|
|
unlockEntry.timestamp : reservation.start;
|
|
}
|
|
|
|
incidentsAsyncJobs.push(
|
|
getEntriesBetween(fromTimestamp, toTimestamp, resourceId)
|
|
.then((entriesBetween) => {
|
|
incidentsAsyncJobs.push(
|
|
new Promise((resolve, reject) => {
|
|
let pairUnlockEntry = null;
|
|
let pairLockEntry = null;
|
|
|
|
entriesBetween.forEach((entry) => {
|
|
if (entry && entry.event){
|
|
switch(entry.event){
|
|
case doorLockEvents.USER_UNLOCKED:
|
|
if (!pairUnlockEntry){
|
|
pairUnlockEntry = entry;
|
|
}else{
|
|
const virtualReservation = {
|
|
reservationId: '',
|
|
start: pairUnlockEntry.timestamp,
|
|
end: pairUnlockEntry.timestamp,
|
|
memberId: pairUnlockEntry.memberId,
|
|
resourceId,
|
|
};
|
|
|
|
incidents.push({
|
|
incidentType: incidentType.UNLOCKED_INCIDENT,
|
|
reservation: virtualReservation,
|
|
});
|
|
|
|
pairLockEntry = null;
|
|
pairUnlockEntry = entry;
|
|
}
|
|
break;
|
|
case doorLockEvents.USER_LOCKED:
|
|
if (pairUnlockEntry && !pairLockEntry){
|
|
pairLockEntry = entry;
|
|
const virtualReservation = {
|
|
reservationId: '',
|
|
start: pairUnlockEntry.timestamp,
|
|
end: pairLockEntry.timestamp,
|
|
memberId: pairUnlockEntry.memberId,
|
|
resourceId,
|
|
};
|
|
const unlockMoment = moment.utc(pairUnlockEntry.timestamp);
|
|
const lockMoment = moment.utc(pairLockEntry.timestamp);
|
|
const timeDifference = lockMoment.diff(unlockMoment, 'minutes');
|
|
const timeIntervalsToCharge = Math.floor(timeDifference / UNSCHEDULED_TIME_RESOLUTION);
|
|
const totalChargeFee = timeIntervalsToCharge * UNSCHEDULED_CHARGE_PRICE;
|
|
if (timeIntervalsToCharge > 0){
|
|
incidents.push({
|
|
incidentType: incidentType.UNSCHEDULED_INCIDENT,
|
|
reservation: virtualReservation,
|
|
doorLockEntry: pairLockEntry,
|
|
chargePrice: UNSCHEDULED_CHARGE_PRICE,
|
|
timeIntervalsToCharge,
|
|
totalChargeFee,
|
|
});
|
|
}
|
|
|
|
pairUnlockEntry = null;
|
|
pairLockEntry = null;
|
|
}else{
|
|
if (!pairUnlockEntry){
|
|
pairLockEntry = entry;
|
|
//Only lock entry, ignore now
|
|
}
|
|
pairLockEntry = null;
|
|
pairUnlockEntry = null;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
resolve(null);
|
|
}));
|
|
})
|
|
.catch((error) => reject(error)));
|
|
})
|
|
.catch((error) => reject(error)));
|
|
|
|
// 1. Check if member entered before reservation start time
|
|
if (unlockEntry && chargeBefore && !previousReservationIsBackToBack) {
|
|
incidents.push({
|
|
incidentType: incidentType.UNSCHEDULED_INCIDENT,
|
|
reservation,
|
|
doorLockEntry: unlockEntry,
|
|
chargePrice: UNSCHEDULED_CHARGE_PRICE,
|
|
timeIntervalsToCharge: timeIntervalsToChargeBefore,
|
|
totalChargeFee: totalChargeFeeBefore,
|
|
});
|
|
}
|
|
|
|
// 2. Check if member left after reservation end time
|
|
if (lockEntry && chargeAfter && !nextReservationIsBackToBack) {
|
|
incidents.push({
|
|
incidentType: incidentType.UNSCHEDULED_INCIDENT,
|
|
reservation,
|
|
doorLockEntry: lockEntry,
|
|
chargePrice: UNSCHEDULED_CHARGE_PRICE,
|
|
timeIntervalsToCharge: timeIntervalsToChargeAfter,
|
|
totalChargeFee: totalChargeFeeAfter,
|
|
});
|
|
}
|
|
|
|
// 3. Check if member forgot to lock the door
|
|
if (!lockEntry && !nextReservationIsBackToBack){
|
|
if (unlockEntry){
|
|
incidents.push({
|
|
incidentType: incidentType.UNLOCKED_INCIDENT,
|
|
reservation,
|
|
});
|
|
} else {
|
|
// No lock entry, no unlock entry and no reservation after this one
|
|
// This is either :
|
|
// 1. Last reservation in block of N reservations
|
|
// 1a. Member locked before entering this reservation
|
|
// 1b. Member forgot to lock the door <<< Only this is real incident
|
|
// 2. One reservation, but member never entered
|
|
|
|
if (previousReservationIsBackToBack){
|
|
// To ensure that this is last reservation in block (there is previous but no next reservation back to back)
|
|
// Now, just check if member actually locked before in the reservation time
|
|
incidentsAsyncJobs.push(
|
|
getLastEntryForReservation(reservation)
|
|
.then((lastEntry) => {
|
|
if (lastEntry && lastEntry.event === doorLockEvents.USER_UNLOCKED){
|
|
incidents.push({
|
|
incidentType: incidentType.UNLOCKED_INCIDENT,
|
|
reservation,
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => reject(error)));
|
|
}
|
|
}
|
|
}
|
|
|
|
Promise.all(incidentsAsyncJobs)
|
|
.then(() => {
|
|
resolve(incidents);
|
|
})
|
|
.catch((error) => reject(error));
|
|
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
const calculateDoorLockCharges = () => {
|
|
getAllFinishedBookings()
|
|
.then((reservations) => {
|
|
const unlockedIncidents = [];
|
|
const unscheduledIncidents = [];
|
|
|
|
const asyncCheckForIncidents = [];
|
|
|
|
reservations.forEach((reservation) => {
|
|
asyncCheckForIncidents.push(getIncidentData(reservation));
|
|
});
|
|
|
|
Promise.all(asyncCheckForIncidents)
|
|
.then((allReservationsIncidents) => {
|
|
allReservationsIncidents.forEach((reservationIncidents) => {
|
|
reservationIncidents.forEach((incident) => {
|
|
if (incident.error) {
|
|
console.log('Error checking incident : ', incident.error);
|
|
} else if (incident.incidentType) {
|
|
switch (incident.incidentType) {
|
|
case incidentType.UNLOCKED_INCIDENT:
|
|
unlockedIncidents.push(incident.reservation);
|
|
break;
|
|
case incidentType.UNSCHEDULED_INCIDENT:
|
|
const {reservation, doorLockEntry, chargePrice, timeIntervalsToCharge, totalChargeFee} = incident;
|
|
unscheduledIncidents.push({
|
|
reservation,
|
|
lockEntry: doorLockEntry,
|
|
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
|
|
};
|