From fc4d374e234459b33c0d875c1951d693094f94f8 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 2 Jul 2019 10:12:56 +0200 Subject: [PATCH 1/3] fix door lock charges - unlocked incident --- services/doorLock/doorLock.js | 40 +++++++++ services/integration/doorLockCharges.js | 103 +++++++++--------------- services/officeRnD/bookings.js | 4 +- 3 files changed, 81 insertions(+), 66 deletions(-) diff --git a/services/doorLock/doorLock.js b/services/doorLock/doorLock.js index 2d96318..ea45109 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 = /\[(.*?)\]/; @@ -360,9 +361,48 @@ const getLockEntryForReservation = (reservation, nextReservation) => { }); }; +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, + getLastEntryForReservation, }; diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js index 8ead2f0..7cc5029 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, 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 = [ @@ -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) @@ -351,15 +298,43 @@ 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, + }); + resolve(incidents); + } 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 - resolve(incidents); + 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 + getLastEntryForReservation(reservation) + .then((lastEntry) => { + if (lastEntry && lastEntry.event === doorLockEvents.USER_UNLOCKED){ + incidents.push({ + incidentType: incidentType.UNLOCKED_INCIDENT, + reservation, + }); + } + resolve(incidents); + }) + .catch((error) => reject(error)); + } else { + resolve(incidents); + } + } + }else{ + resolve(incidents); + } }) .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, From 2b492025fb615d3135e70f7419f21ce43599c91d Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Wed, 3 Jul 2019 11:50:23 +0200 Subject: [PATCH 2/3] fix door lock charges --- services/doorLock/doorLock.js | 50 ++++++++++--------------- services/integration/doorLockCharges.js | 7 ++-- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/services/doorLock/doorLock.js b/services/doorLock/doorLock.js index ea45109..c50c6eb 100644 --- a/services/doorLock/doorLock.js +++ b/services/doorLock/doorLock.js @@ -190,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: { @@ -217,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) { @@ -250,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); } }) @@ -292,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, @@ -361,6 +344,10 @@ const getLockEntryForReservation = (reservation, nextReservation) => { }); }; +const getEntriesBetween = (reservation, previousReservation) => { + +}; + const getLastEntryForReservation = (reservation) => { return new Promise ((resolve, reject) => { getFirstReservationInBlock(reservation) @@ -404,5 +391,6 @@ module.exports = { writeDoorLockEvent, getUnlockEntryForReservation, getLockEntryForReservation, + getEntriesBetween, getLastEntryForReservation, }; diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js index 7cc5029..a6ac770 100644 --- a/services/integration/doorLockCharges.js +++ b/services/integration/doorLockCharges.js @@ -4,7 +4,7 @@ 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, getLastEntryForReservation } = require('../doorLock/doorLock'); +const { getUnlockEntryForReservation, getLockEntryForReservation, getEntriesBetween, getLastEntryForReservation } = require('../doorLock/doorLock'); const { getAllFinishedBookings, getFirstPreviousBooking, getFirstNextBooking } = require('../officeRnD/bookings'); const getSortedIncidentsForMember = (memberId) => { @@ -180,11 +180,12 @@ const analyseReservation = (reservation) => { .then(([previousReservation, nextReservation]) => { const getRelatedDoorLockEntriesAsync = [ getUnlockEntryForReservation(reservation, previousReservation), - getLockEntryForReservation(reservation, nextReservation) + getLockEntryForReservation(reservation, nextReservation), + getEntriesBetween(reservation, previousReservation) ]; Promise.all(getRelatedDoorLockEntriesAsync) - .then(([unlockEntry, lockEntry]) => { + .then(([unlockEntry, lockEntry, entriesBetween]) => { const currentReservationStart = moment.utc(reservation.start); const currentReservationEnd = moment.utc(reservation.end); From 23b0924b5a4fa79d12f1e10ff4a986aa5923278e Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Wed, 3 Jul 2019 14:34:34 +0200 Subject: [PATCH 3/3] include unscheduled use between reservations --- services/doorLock/doorLock.js | 27 ++++- services/integration/doorLockCharges.js | 126 ++++++++++++++++++++++-- 2 files changed, 143 insertions(+), 10 deletions(-) diff --git a/services/doorLock/doorLock.js b/services/doorLock/doorLock.js index c50c6eb..fdea6f7 100644 --- a/services/doorLock/doorLock.js +++ b/services/doorLock/doorLock.js @@ -344,8 +344,33 @@ const getLockEntryForReservation = (reservation, nextReservation) => { }); }; -const getEntriesBetween = (reservation, previousReservation) => { +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) => { diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js index a6ac770..0c70262 100644 --- a/services/integration/doorLockCharges.js +++ b/services/integration/doorLockCharges.js @@ -180,8 +180,7 @@ const analyseReservation = (reservation) => { .then(([previousReservation, nextReservation]) => { const getRelatedDoorLockEntriesAsync = [ getUnlockEntryForReservation(reservation, previousReservation), - getLockEntryForReservation(reservation, nextReservation), - getEntriesBetween(reservation, previousReservation) + getLockEntryForReservation(reservation, nextReservation) ]; Promise.all(getRelatedDoorLockEntriesAsync) @@ -274,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) { @@ -306,7 +412,6 @@ const getIncidentData = (reservation) => { incidentType: incidentType.UNLOCKED_INCIDENT, reservation, }); - resolve(incidents); } else { // No lock entry, no unlock entry and no reservation after this one // This is either : @@ -318,6 +423,7 @@ const getIncidentData = (reservation) => { 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){ @@ -326,16 +432,18 @@ const getIncidentData = (reservation) => { reservation, }); } - resolve(incidents); }) - .catch((error) => reject(error)); - } else { - resolve(incidents); + .catch((error) => reject(error))); } } - }else{ - resolve(incidents); } + + Promise.all(incidentsAsyncJobs) + .then(() => { + resolve(incidents); + }) + .catch((error) => reject(error)); + }) .catch((error) => reject(error)); });