diff --git a/services/doorLock/doorLock.js b/services/doorLock/doorLock.js index 2d96318..fdea6f7 100644 --- a/services/doorLock/doorLock.js +++ b/services/doorLock/doorLock.js @@ -18,6 +18,7 @@ const { const { fetchAllMembers } = require('../officeRnD/members'); const { getMappingsFromDatabase } = require('../officeRnD/resources'); +const { getFirstReservationInBlock } = require('../officeRnD/bookings'); const extractMappingFromFileName = (fileName) => { const contentBetweenBracketsRegex = /\[(.*?)\]/; @@ -189,10 +190,16 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => { const attributes = ['memberName', 'event', 'timestamp', 'resourceId']; - const fromTimestamp = previousReservation && previousReservation.end ? - previousReservation.end : moment.utc(reservation.start).tz(UI_TIMEZONE).startOf('Day').toISOString(); + const previousReservationEndMoment = previousReservation && previousReservation.end ? + moment.utc(previousReservation.end) : null; + const reservationStartMoment = moment.utc(reservation.start); + + const fromTimestamp = previousReservationEndMoment && previousReservationEndMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ? + previousReservation.end : reservationStartMoment.tz(UI_TIMEZONE).startOf('day').toISOString(); + const toTimestamp = reservation.end; + const filters = { memberId, timestamp: { @@ -216,21 +223,8 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => { let pairedLockEntry = null; let eventFound = false; - // console.log('\r\n=?== SEARCH UNLOCK =='); - // console.log('ReservatioN : ', reservation.start); - const entriesBeforeReservationStart = entries.filter((entry) => moment.utc(entry.timestamp).isBefore(reservation.start)); - // console.log('After end : '); - /*console.log(entriesBeforeReservationStart.map(e => { - return { - timestamp: e.timestamp, - event: e.event, - } - })); - - */ - entriesBeforeReservationStart.forEach((entry) => { if (!eventFound) { if (entry.event === doorLockEvents.USER_UNLOCKED) { @@ -249,35 +243,21 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => { }); if (eventFound){ - // console.log('FOUND : ', candidateUnlockEntry.timestamp, candidateUnlockEntry.event); resolve(candidateUnlockEntry); } else { - // console.log('NOT FOUND IN FIRST ROUND'); candidateUnlockEntry = null; const numberOfEntriesLeft = entries.length - entriesBeforeReservationStart.length; const entriesAfterReservationStart = entries.slice(0, numberOfEntriesLeft); - // console.log('Entries in reservation time : '); - /*console.log(entriesAfterReservationStart.map(e => { - return { - timestamp: e.timestamp, - event: e.event, - } - })); - - */ - entriesAfterReservationStart.forEach((entry) => { if (!eventFound) { if (entry.event === doorLockEvents.USER_UNLOCKED) { - //console.log('FOUND : ', entry.timestamp, entry.event); eventFound = true; candidateUnlockEntry = entry; } } }); - //console.log('===??=='); resolve(candidateUnlockEntry); } }) @@ -291,9 +271,13 @@ const getLockEntryForReservation = (reservation, nextReservation) => { const attributes = ['memberName', 'event', 'timestamp']; + const nextReservationStartMoment = nextReservation && nextReservation.start ? + moment.utc(nextReservation.start) : null; + const reservationStartMoment = moment.utc(reservation.start); + const fromTimestamp = reservation.start; - const toTimestamp = nextReservation && nextReservation.start ? - nextReservation.start : moment.utc(reservation.start).tz(UI_TIMEZONE).endOf('Day').toISOString(); + const toTimestamp = nextReservationStartMoment && nextReservationStartMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ? + nextReservation.start : reservationStartMoment.tz(UI_TIMEZONE).endOf('day').toISOString(); const filters = { memberId, @@ -360,9 +344,78 @@ const getLockEntryForReservation = (reservation, nextReservation) => { }); }; +const getEntriesBetween = (fromTimestamp, toTimestamp, resourceId) => { + return new Promise((resolve, reject) => { + if (!fromTimestamp || !toTimestamp || !resourceId){ + resolve([]); + }else { + const andTimestampFilters = []; + + if (fromTimestamp){ + andTimestampFilters.push({[Op.gt]: fromTimestamp}); + } + if (toTimestamp){ + andTimestampFilters.push({[Op.lt]: toTimestamp}); + } + + const filters = { + resourceId, + timestamp: { + [Op.and]: andTimestampFilters, + }, + }; + const order = [['timestamp', 'ASC']]; + + db.doorLockEvent.findAll({where: filters, order}) + .then((results) => resolve(results)) + .catch((error) => reject(error)); + } + }); +}; + +const getLastEntryForReservation = (reservation) => { + return new Promise ((resolve, reject) => { + getFirstReservationInBlock(reservation) + .then((firstReservationInBlock) => { + const { memberId, resourceId } = reservation; + let fromTimestamp = reservation.start; + const toTimestamp = reservation.end; + if (firstReservationInBlock){ + fromTimestamp = firstReservationInBlock.start; + } + + const filters = { + memberId, + resourceId, + timestamp: { + [Op.and]: [ + {[Op.gte]: fromTimestamp}, + {[Op.lte]: toTimestamp} + ] + }, + }; + + const order = [['timestamp', 'DESC']]; + + db.doorLockEvent.findAll({where: filters, order}) + .then((entries) => { + if (entries && entries.length > 0){ + resolve(entries[0]); + } else { + resolve (undefined); + } + }) + .catch((error) => reject(error)); + }) + .catch((error) => reject(error)); + }); +}; + module.exports = { parseDoorLockDataFile, writeDoorLockEvent, getUnlockEntryForReservation, getLockEntryForReservation, + getEntriesBetween, + getLastEntryForReservation, }; diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js index 8ead2f0..0c70262 100644 --- a/services/integration/doorLockCharges.js +++ b/services/integration/doorLockCharges.js @@ -3,9 +3,9 @@ const moment = require('moment-timezone'); const db = require('../../models/index'); -const { incidentType, unlockedIncidentLevelsPrices, UNSCHEDULED_CHARGE_PRICE, MAX_BACK_TO_BACK_DIFFERENCE, UNSCHEDULED_TIME_RESOLUTION } = require('../../constants/constants'); -const { getUnlockEntryForReservation, getLockEntryForReservation } = require('../doorLock/doorLock'); -const { getAllFinishedBookings, getFirstReservationInBlock, getFirstPreviousBooking, getFirstNextBooking } = require('../officeRnD/bookings'); +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']; @@ -169,45 +169,6 @@ const setUnlockedIncidentsLevel = (incidentReservations) => { }); }; -const getUnlockEntryForBlockReservations = (lastReservation, firstReservation) => { - return new Promise ((resolve, reject) => { - analyseReservation(firstReservation) - .then((result) => { - const reservationBeforeFirstReservationInBlock = result.previousReservation; - const unlockEntryForFirstReservation = result.unlockEntry; - - if (unlockEntryForFirstReservation){ - resolve(unlockEntryForFirstReservation); - }else{ - const fromTimestamp = reservationBeforeFirstReservationInBlock ? - reservationBeforeFirstReservationInBlock.end : - moment.utc(firstReservation.start).tz(UI_TIMEZONE).startOf('Day').toISOString(); - const toTimestamp = lastReservation.end; - const { memberId, reservationId } = lastReservation; - - const filters = { - memberId, - reservationId, - event: doorLockEvents.USER_UNLOCKED, - timestamp: { - [Op.and]: [ - {[Op.gt]: fromTimestamp}, - {[Op.lte]: toTimestamp} - ] - }, - }; - - db.doorLockEvent.findOne({where: filters}) - .then((anyUnlockEntryInBlock) => { - resolve(anyUnlockEntryInBlock); - }) - .catch((error) => reject(error)); - } - }) - .catch((error) => reject(error)); - }); -}; - const analyseReservation = (reservation) => { return new Promise ((resolve, reject) => { const getNearBookingsAsync = [ @@ -223,7 +184,7 @@ const analyseReservation = (reservation) => { ]; Promise.all(getRelatedDoorLockEntriesAsync) - .then(([unlockEntry, lockEntry]) => { + .then(([unlockEntry, lockEntry, entriesBetween]) => { const currentReservationStart = moment.utc(reservation.start); const currentReservationEnd = moment.utc(reservation.end); @@ -288,20 +249,6 @@ const analyseReservation = (reservation) => { }); }; -const checkIfMemberEverEntered = (reservation) => { - return new Promise ((resolve, reject) => { - getFirstReservationInBlock(reservation) - .then((firstReservationInBlock) => { - getUnlockEntryForBlockReservations(reservation, firstReservationInBlock) - .then((unlockEntry) => { - resolve(!!unlockEntry); - }) - .catch((error) => reject(error)); - }) - .catch((error) => reject(error)); - }); -}; - const getIncidentData = (reservation) => { return new Promise ((resolve, reject) => { analyseReservation(reservation) @@ -326,6 +273,113 @@ const getIncidentData = (reservation) => { 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) { @@ -351,15 +405,45 @@ const getIncidentData = (reservation) => { }); } - // 3. Check if member forgot to lock door - if (unlockEntry && !lockEntry && !nextReservationIsBackToBack){ - incidents.push({ - incidentType: incidentType.UNLOCKED_INCIDENT, - reservation, - }); + // 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))); + } + } } - resolve(incidents); + Promise.all(incidentsAsyncJobs) + .then(() => { + resolve(incidents); + }) + .catch((error) => reject(error)); + }) .catch((error) => reject(error)); }); diff --git a/services/officeRnD/bookings.js b/services/officeRnD/bookings.js index 51607cb..c31da33 100644 --- a/services/officeRnD/bookings.js +++ b/services/officeRnD/bookings.js @@ -116,10 +116,10 @@ const getFirstPreviousBooking = (reservation) => { const getFirstReservationInBlock = (reservation) => { return new Promise ((resolve, reject) => { - const {resourceId, memberId, start} = reservation; + const { resourceId, memberId, start } = reservation; const fromTimestamp = moment.utc(start).subtract(MAX_BACK_TO_BACK_DIFFERENCE).toISOString(); - const toTimestamp = reservation.end; + const toTimestamp = start; const filters = { resourceId,