detect and save unscheduled and unlocked incidents

This commit is contained in:
Bilal Catic
2019-06-14 08:05:24 +02:00
parent 07061d3d19
commit 596336c41b
19 changed files with 929 additions and 78 deletions

View File

@@ -1,2 +1,2 @@
export * from './doorLockActions';
export * from './officeRnDActions';
export * from './integrationActions';

View File

@@ -11,7 +11,7 @@ import API from '../../utilities/api';
export const fetchMappings = (dispatch) => {
dispatch({type: FETCH_MAPPINGS_PENDING});
API.get('doorLock/mappings')
API.get('integration/mappings')
.then(response => {
dispatch({type: FETCH_MAPPINGS_SUCCESS, payload: response.data});
})
@@ -22,7 +22,7 @@ export const fetchMappings = (dispatch) => {
export const addNewMapping = (dispatch, mapping) => {
dispatch({type: ADD_NEW_MAPPING_PENDING});
API.post('doorLock/mappings', {
API.post('integration/mappings', {
mapping
})
.then(response => {

View File

@@ -3,8 +3,9 @@
"username": "docker",
"password": "docker",
"database": "CrmIntegration",
"port": "5432",
"dialect": "postgres"
"port": "5431",
"dialect": "postgres",
"logging": false
},
"test": {
"use_env_variable": "DATABASE_URL"

View File

@@ -4,22 +4,64 @@ const DISABLE_PASSAGE_MODE = 'Disable Passage Mode by Group 2';
const VALID_CSV_HEADERS = ['Date', 'Time', 'User No', 'Name', 'Event'];
const doorLockEvents = {
USER_LOCKED: 'locked',
USER_UNLOCKED: 'unlocked',
};
const doorChargeTypes = {
LEFT_UNLOCKED: 'unlocked',
UNSCHEDULED_USE: 'unscheduled'
const unlockedIncidentLevelsPrices = {
UNLOCKED_0: {
id: 0,
title: 'UNLOCKED_0',
price: parseInt(process.env.UNLOCK_0) || 0
},
UNLOCKED_1: {
id: 1,
title: 'UNLOCKED_1',
price: parseInt(process.env.UNLOCK_1) || 10
},
UNLOCKED_2: {
id: 2,
title: 'UNLOCKED_2',
price: parseInt(process.env.UNLOCK_2) || 20
},
UNLOCKED_3: {
id: 3,
title: 'UNLOCKED_3',
price: parseInt(process.env.UNLOCK_3) || 30
},
UNLOCKED_4: {
id: 4,
title: 'UNLOCKED_4',
price: parseInt(process.env.UNLOCK_4) || 40
},
UNLOCKED_5: {
id: 5,
title: 'UNLOCKED_5',
price: parseInt(process.env.UNLOCK_5) || 50
}
};
const csvParserErrors = {
INVALID_HEADERS: 'Invalid headers',
INVALID_ENTRY_EXPECTED_USER: 'Invalid entry type. Expected user entry type',
INVALID_ENTRY_EXPECTED_PASSAGE_MODE: 'Invalid entry type. Expected enable/disable passage mode following user entry',
UNKNOWN_MEMBER: 'Member is not registered in OfficeRnD system',
GENERIC_ERROR: 'There was error while parsing uploaded file(s)',
};
const officeRnDAPIErrors = {
FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members',
FAILED_TO_FETCH_BOOKINGS: 'Failed to fetch booking reservations'
};
const integrationServiceErrors = {
FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations',
FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries',
FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data',
};
const incidentType = {
NOT_AN_INCIDENT: 1,
UNLOCKED_INCIDENT: 2,
UNSCHEDULED_INCIDENT: 3,
};
module.exports = {
@@ -30,5 +72,7 @@ module.exports = {
csvParserErrors,
officeRnDAPIErrors,
doorLockEvents,
doorChargeTypes,
unlockedIncidentLevelsPrices,
integrationServiceErrors,
incidentType,
};

View File

@@ -1,9 +1,9 @@
'use strict';
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
const { parseDoorLockDataFile, writeDoorLockEvent } = require('../services/doorLock');
const { parseDoorLockDataFile, writeDoorLockEvent } = require('../services/doorLock/doorLock');
const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings');
const { officeRnDAPIErrors } = require('../constants/constants');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
const { integrationServiceErrors } = require('../constants/constants');
const IncomingForm = require('formidable').IncomingForm;
@@ -30,64 +30,42 @@ const uploadDoorLockData = (req, res) => {
unknownMembers.push(...parserResult.unknownMembers);
});
res.json({
parsedData,
parserErrors,
unknownMembers
});
const asyncJobs = [];
fetchAllBookings()
.then((bookingEntries) => {
bookingEntries.forEach((bookingEntry) => writeBookingReservation(bookingEntry));
bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry)));
})
.catch((error) => {
console.log('===> ERROR');
console.log(error);
res.status(500).send(error);
return;
});
parsedData.forEach((entry) => {
writeDoorLockEvent(entry);
});
parsedData.forEach((entry) => asyncJobs.push(writeDoorLockEvent(entry)));
Promise.all(asyncJobs)
.then(() => {
res.json({
parsedData,
parserErrors,
unknownMembers
});
calculateDoorLockCharges();
})
.catch((error) => {
console.log(`${integrationServiceErrors.FAILED_TO_SAVE_BOOKINGS} or ${integrationServiceErrors.FAILED_TO_SAVE_DOOR_LOCK_ENTRIES}`)
console.log(error);
res.status(500).send(integrationServiceErrors.FAILED_TO_SAVE_DATA_GENERIC);
});
})
.catch((error) => {
res.status(500).send(officeRnDAPIErrors.FAILED_TO_FETCH_MEMBERS);
res.status(500).send(error);
});
});
form.parse(req);
};
const getKnownOfficeResourceMappings = (req, res) => {
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
Promise.all(dataToFetch)
.then(result => {
res.send({
existingMappings: result[0],
offices: result[1],
resources: result[2],
});
})
.catch(error => {
res.status(500).send();
});
};
const addNewMapping = (req, res) => {
const newMapping = req.body && req.body.mapping ? req.body.mapping : null;
if (newMapping && newMapping.officeSlug && newMapping.resourceSlug && newMapping.officeId && newMapping.resourceId){
saveNewMappingToDatabase(newMapping)
.then(result => {
res.send(newMapping);
})
.catch(error => {
res.status(500).send(error);
});
}
};
module.exports = {
uploadDoorLockData,
getKnownOfficeResourceMappings,
addNewMapping,
};

View File

@@ -0,0 +1,37 @@
'use strict';
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
const getKnownOfficeResourceMappings = (req, res) => {
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
Promise.all(dataToFetch)
.then(result => {
res.send({
existingMappings: result[0],
offices: result[1],
resources: result[2],
});
})
.catch(error => {
res.status(500).send();
});
};
const addNewMapping = (req, res) => {
const newMapping = req.body && req.body.mapping ? req.body.mapping : null;
if (newMapping && newMapping.officeSlug && newMapping.resourceSlug && newMapping.officeId && newMapping.resourceId){
saveNewMappingToDatabase(newMapping)
.then(() => {
res.send(newMapping);
})
.catch(error => {
res.status(500).send(error);
});
}
};
module.exports = {
getKnownOfficeResourceMappings,
addNewMapping,
};

View File

@@ -1,2 +1,18 @@
BASIC_AUTH_USERNAME=username
BASIC_AUTH_PASSWORD=password
OFFICE_RnD_TOKEN=token for Office RnD API requests
MAX_BACK_TO_BACK_DIFFERENCE=Time in minutes
EARLIEST_UNLOCK=2
UNSCHEDULED_USE_TIME_RESOLUTION=Time in minutes
UNSCHEDULED_USE_CHARGE_FEE=Charge fee
UNLOCK_0=Price for unlocked door, first month
UNLOCK_1=Price for unlocked door, second month
UNLOCK_2=Price for unlocked door, third month
UNLOCK_3=Price for unlocked door, fourth month
UNLOCK_4=Price for unlocked door, fifth month
UNLOCK_5=Price for unlocked door, sixth month
UNLOCK_STREAK_REPAIR_AFTER=Number of months without incidents to reset user incident level

View File

@@ -0,0 +1,14 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn('bookingReservations', 'officeId', {
type: Sequelize.TEXT,
after: 'memberId',
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn('bookingReservations', 'officeId');
}
};

View File

@@ -0,0 +1,11 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.renameTable('doorLockIncidents', 'unscheduledIncidents');
},
down: (queryInterface, Sequelize) => {
return queryInterface.renameTable('unscheduledIncidents', 'doorLockIncidents');
}
};

View File

@@ -0,0 +1,35 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.removeColumn('unscheduledIncidents', 'chargeType'),
queryInterface.addColumn('unscheduledIncidents', 'chargePrice', {
type: Sequelize.FLOAT,
after: 'doorLockEventTimestamp'
}),
queryInterface.addColumn('unscheduledIncidents', 'timeIntervalsToCharge', {
type: Sequelize.INTEGER,
after: 'chargePrice'
}),
queryInterface.renameColumn('unscheduledIncidents', 'chargeFee', 'totalChargeFee')
]);
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.renameColumn('unscheduledIncidents', 'totalChargeFee', 'chargeFee'),
queryInterface.removeColumn('unscheduledIncidents', 'timeIntervalsToCharge'),
queryInterface.removeColumn('unscheduledIncidents', 'chargePrice'),
queryInterface.addColumn('unscheduledIncidents', 'chargeType', {
type: Sequelize.ENUM,
values: ['unlocked', 'unscheduled'],
after: 'doorLockEventTimestamp'
}),
]);
});
}
};

View File

@@ -0,0 +1,35 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('unlockedIncidents', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
reservationId: Sequelize.TEXT,
memberId: Sequelize.TEXT,
resourceId: Sequelize.TEXT,
bookingStart: Sequelize.DATE,
bookingEnd: Sequelize.DATE,
incidentLevel: {
type: Sequelize.ENUM,
values: ['UNLOCKED_0', 'UNLOCKED_1', 'UNLOCKED_2', 'UNLOCKED_3', 'UNLOCKED_4', 'UNLOCKED_5']
},
incidentLevelPrice: Sequelize.FLOAT,
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('doorLockIncidents');
}
};

View File

@@ -4,6 +4,7 @@ module.exports = (sequelize, DataTypes) => {
const bookingReservation = sequelize.define('bookingReservation', {
reservationId: DataTypes.TEXT,
memberId: DataTypes.TEXT,
officeId: DataTypes.TEXT,
resourceId: DataTypes.TEXT,
start: DataTypes.DATE,
end: DataTypes.DATE,

View File

@@ -0,0 +1,29 @@
'use strict';
const { unlockedIncidentLevelsPrices } = require('../constants/constants');
module.exports = (sequelize, DataTypes) => {
const unlockedIncident = sequelize.define('unlockedIncident', {
reservationId: DataTypes.TEXT,
memberId: DataTypes.TEXT,
resourceId: DataTypes.TEXT,
bookingStart: DataTypes.DATE,
bookingEnd: DataTypes.DATE,
incidentLevel: {
type: DataTypes.ENUM,
values: [
unlockedIncidentLevelsPrices.UNLOCKED_0.title,
unlockedIncidentLevelsPrices.UNLOCKED_1.title,
unlockedIncidentLevelsPrices.UNLOCKED_2.title,
unlockedIncidentLevelsPrices.UNLOCKED_3.title,
unlockedIncidentLevelsPrices.UNLOCKED_4.title,
unlockedIncidentLevelsPrices.UNLOCKED_5.title,
]
},
incidentLevelPrice: DataTypes.FLOAT,
}, {});
unlockedIncident.associate = function(models) {
// associations can be defined here
};
return unlockedIncident;
};

View File

@@ -1,9 +1,9 @@
'use strict';
const { doorLockEvents, doorChargeTypes } = require('../constants/constants');
const { doorLockEvents } = require('../constants/constants');
module.exports = (sequelize, DataTypes) => {
const doorLockIncident = sequelize.define('doorLockIncident', {
const unscheduledIncident = sequelize.define('unscheduledIncident', {
reservationId: DataTypes.TEXT,
memberId: DataTypes.TEXT,
resourceId: DataTypes.TEXT,
@@ -14,14 +14,12 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.ENUM,
values: [doorLockEvents.USER_LOCKED, doorLockEvents.USER_UNLOCKED]
},
chargeType: {
type: DataTypes.ENUM,
values: [doorChargeTypes.LEFT_UNLOCKED, doorChargeTypes.UNSCHEDULED_USE]
},
chargeFee: DataTypes.FLOAT,
chargePrice: DataTypes.FLOAT,
timeIntervalsToCharge: DataTypes.INTEGER,
totalChargeFee: DataTypes.FLOAT,
}, {});
doorLockIncident.associate = function(models) {
unscheduledIncident.associate = function(models) {
// associations can be defined here
};
return doorLockIncident;
return unscheduledIncident;
};

View File

@@ -8,7 +8,7 @@
"install-server": "npm install",
"install-client": "cd client && yarn install",
"docker-build": "docker build -t simaspace .",
"docker-start": "docker run -e POSTGRES_USER=docker -e POSTGRES_PASSWORD=docker -e POSTGRES_DB=CrmIntegration --name pg_simaspace -d -p 5432:5432 simaspace",
"docker-start": "docker run -e POSTGRES_USER=docker -e POSTGRES_PASSWORD=docker -e POSTGRES_DB=CrmIntegration --name pg_simaspace -d -p 5431:5432 simaspace",
"docker-stop": "docker stop pg_simaspace",
"setup": "npm run install-server && npm run install-client && npm run docker-build && npm run docker-start && sleep 5 && npm run migrate",
"migrate": "npx sequelize db:migrate",

View File

@@ -1,7 +1,9 @@
'use strict';
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
const { uploadDoorLockData, getKnownOfficeResourceMappings, addNewMapping } = require('../controllers/doorLock');
const { uploadDoorLockData } = require('../controllers/doorLock');
const { getKnownOfficeResourceMappings, addNewMapping } = require('../controllers/integration');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
const express = require('express');
const router = express.Router();
@@ -9,7 +11,10 @@ const router = express.Router();
router.get('/', apiStatusCheck);
router.post('/doorLock/upload', uploadDoorLockData);
router.get('/doorLock/mappings', getKnownOfficeResourceMappings);
router.post('/doorLock/mappings', addNewMapping);
router.get('/integration/mappings', getKnownOfficeResourceMappings);
router.post('/integration/mappings', addNewMapping);
// temporary route, manually trigger door lock charge calculations
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});
module.exports = router;

View File

@@ -1,9 +1,10 @@
'use strict';
const db = require('../models/index');
const db = require('../../models');
const fs = require('fs');
const csv = require('csv-parser');
const moment = require('moment');
const moment = require('moment/moment');
const Op = require('sequelize').Op;
const {
USER_ENTRY_EVENT,
@@ -12,10 +13,10 @@ const {
VALID_CSV_HEADERS,
doorLockEvents,
csvParserErrors,
} = require('../constants/constants');
} = require('../../constants/constants');
const { fetchAllMembers, findMember } = require('../services/officeRnD/members');
const { getMappingsFromDatabase } = require('../services/officeRnD/resources');
const { fetchAllMembers, findMember } = require('../officeRnD/members');
const { getMappingsFromDatabase } = require('../officeRnD/resources');
const extractMappingFromFileName = (fileName) => {
const contentBetweenBracketsRegex = /\[(.*?)\]/;
@@ -170,13 +171,60 @@ const parseDoorLockDataFile = (file) => {
};
const writeDoorLockEvent = (entry) => {
console.log('Write entry : ');
console.log(entry);
console.log('====');
return db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}});
};
const getUnlockEntryForReservation = (reservation) => {
const { start, end, memberId, resourceId } = reservation;
const attributes = ['memberName', 'event', 'timestamp'];
const earliestUnlock = parseInt(process.env.EARLIEST_UNLOCK) || 0;
const fromTimestamp = moment(start).subtract(earliestUnlock).toISOString();
const toTimestamp = end;
const filters = {
memberId,
timestamp: {
[Op.and]: [
{[Op.gt]: fromTimestamp},
{[Op.lte]: toTimestamp}
]
},
event: doorLockEvents.USER_UNLOCKED,
resourceId,
};
return db.doorLockEvent.findOne({
attributes,
where: filters,
})
};
const getRelatedDoorLockEntries = (fromTimestamp, toTimestamp, memberId, resourceId) => {
const attributes = ['memberName', 'event', 'timestamp'];
const filters = {
memberId,
timestamp: {
[Op.and]: [
{[Op.gt]: fromTimestamp},
{[Op.lte]: toTimestamp}
]
},
event: doorLockEvents.USER_LOCKED,
resourceId,
};
return db.doorLockEvent.findOne({
attributes,
where: filters
})
};
module.exports = {
parseDoorLockDataFile,
writeDoorLockEvent,
getRelatedDoorLockEntries,
getUnlockEntryForReservation,
};

View File

@@ -0,0 +1,502 @@
'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);
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE);
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);
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE);
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
};

View File

@@ -1,8 +1,11 @@
'use strict';
const db = require('../../models/index');
const moment = require('moment-timezone');
const Op = require('sequelize').Op;
const { API } = require('../../helpers/api');
const { officeRnDAPIErrors } = require('../../constants/constants');
const fetchAllBookings = () => {
return new Promise((resolve, reject) => {
@@ -15,19 +18,110 @@ const fetchAllBookings = () => {
cleanedBookingReservations.push({
reservationId: fullBookingEntry['_id'],
memberId: fullBookingEntry.member,
officeId: fullBookingEntry.office,
resourceId: fullBookingEntry.resourceId,
start: fullBookingEntry.start.dateTime,
end: fullBookingEntry.end.dateTime,
timezone: fullBookingEntry.timezone,
canceled: fullBookingEntry.canceled || false,
});
});
resolve(cleanedBookingReservations);
})
.catch((error) => {
reject(error);
console.log(officeRnDAPIErrors.FAILED_TO_FETCH_BOOKINGS);
console.log('Details : ', error);
reject(officeRnDAPIErrors.FAILED_TO_FETCH_BOOKINGS);
});
});
};
const getAllFinishedBookings = () => {
const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone'];
const filters = {
canceled: false,
end: {
[Op.lt]: moment().toISOString()
}
};
return db.bookingReservation.findAll({
attributes,
where: filters,
order: [
['start', 'ASC'],
]
})
};
const getFirstNextBooking = (reservation) => {
return new Promise ((resolve, reject) => {
const {resourceId, start, timezone} = reservation;
const endOfTheDay = moment.tz(start, timezone).endOf('Day').toISOString();
const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone'];
const filters = {
canceled: false,
start: {
[Op.gt]: start
},
end: {
[Op.lte]: endOfTheDay
},
resourceId,
};
const order = [['start', 'ASC']];
db.bookingReservation.findAll({
attributes,
where: filters,
order,
})
.then((reservations) => {
if (reservations && reservations[0]){
resolve(reservations[0]);
}else{
resolve(undefined);
}
})
.catch((error) => reject(error));
});
};
const getFirstPreviousBooking = (reservation) => {
return new Promise ((resolve, reject) => {
const {resourceId, start, timezone} = reservation;
const startOfTheDay = moment.tz(start, timezone).startOf('Day').toISOString();
const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone'];
const filters = {
canceled: false,
start: {
[Op.gte]: startOfTheDay
},
end: {
[Op.lte]: start
},
resourceId,
};
const order = [['end', 'DESC']];
db.bookingReservation.findAll({
attributes,
where: filters,
order,
})
.then((reservations) => {
if (reservations && reservations[0]){
resolve(reservations[0]);
}else{
resolve(undefined);
}
})
.catch((error) => reject(error));
});
};
const writeBookingReservation = (bookingReservation) => {
return db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}});
};
@@ -35,4 +129,7 @@ const writeBookingReservation = (bookingReservation) => {
module.exports = {
fetchAllBookings,
writeBookingReservation,
getAllFinishedBookings,
getFirstNextBooking,
getFirstPreviousBooking,
};