Calculate door lock charges
This commit is contained in:
230
services/doorLock/doorLock.js
Normal file
230
services/doorLock/doorLock.js
Normal file
@@ -0,0 +1,230 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../models');
|
||||
const fs = require('fs');
|
||||
const csv = require('csv-parser');
|
||||
const moment = require('moment/moment');
|
||||
const Op = require('sequelize').Op;
|
||||
|
||||
const {
|
||||
USER_ENTRY_EVENT,
|
||||
ENABLE_PASSAGE_MODE,
|
||||
DISABLE_PASSAGE_MODE,
|
||||
VALID_CSV_HEADERS,
|
||||
doorLockEvents,
|
||||
csvParserErrors,
|
||||
} = require('../../constants/constants');
|
||||
|
||||
const { fetchAllMembers, findMember } = require('../officeRnD/members');
|
||||
const { getMappingsFromDatabase } = require('../officeRnD/resources');
|
||||
|
||||
const extractMappingFromFileName = (fileName) => {
|
||||
const contentBetweenBracketsRegex = /\[(.*?)\]/;
|
||||
const rawContent = fileName.match(contentBetweenBracketsRegex)[1];
|
||||
const mappingContent = rawContent.split('-').map(word => word.trim());
|
||||
return {
|
||||
officeSlug: mappingContent[0],
|
||||
resourceSlug: mappingContent[1],
|
||||
}
|
||||
};
|
||||
|
||||
const checkIfMappingExsists = (mappingFromFileName, mappings) => {
|
||||
const { officeSlug, resourceSlug } = mappingFromFileName;
|
||||
|
||||
return mappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug));
|
||||
};
|
||||
|
||||
const parseDoorLockDataFile = (file) => {
|
||||
return new Promise ((resolve, reject) => {
|
||||
const results = [];
|
||||
const errors = [];
|
||||
const unknownMembers = [];
|
||||
let isValidFile = true;
|
||||
|
||||
const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()];
|
||||
|
||||
Promise.all(prefetchDataJobs)
|
||||
.then(result => {
|
||||
const mappings = result[0];
|
||||
|
||||
const mappingFromFileName = extractMappingFromFileName(file.name);
|
||||
const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings);
|
||||
if (!mappingObject){
|
||||
reject('Error ! File contains unknown location');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.createReadStream(file.path)
|
||||
.pipe(csv({
|
||||
mapHeaders: ({ header, index }) => header.trim().toLowerCase(),
|
||||
mapValues: ({ header, index, value }) => value.trim()
|
||||
}))
|
||||
.on('headers', (headers) => {
|
||||
|
||||
const sortedValidHeadersArray = VALID_CSV_HEADERS.concat().sort();
|
||||
const sortedParsedHeadersArray = headers.map(header => header.trim()).sort();
|
||||
|
||||
let validHeaders = true;
|
||||
if (sortedParsedHeadersArray.length !== sortedValidHeadersArray.length) {
|
||||
validHeaders = false;
|
||||
}else {
|
||||
for (let i = 0; i < sortedValidHeadersArray.length; i++){
|
||||
validHeaders = validHeaders
|
||||
&& (sortedValidHeadersArray[i] === sortedParsedHeadersArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!validHeaders){
|
||||
isValidFile = false;
|
||||
errors.push({
|
||||
error: csvParserErrors.INVALID_HEADERS,
|
||||
details: `Expected headers : ${JSON.stringify(VALID_CSV_HEADERS)}`,
|
||||
file: file.name,
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('data', (data) => {
|
||||
if (!isValidFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventType = data.event.trim();
|
||||
if ([USER_ENTRY_EVENT, ENABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE].includes(eventType)){
|
||||
results.push(data);
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
const parsedData = [];
|
||||
let i = 0;
|
||||
while (i < results.length){
|
||||
//Verify pair
|
||||
//First entry type should be user entry and second should be enable / disable passage
|
||||
const firstEntry = results[i];
|
||||
const secondEntry = results[i+1];
|
||||
|
||||
if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){
|
||||
const memberObject = findMember(firstEntry.name);
|
||||
|
||||
if (!memberObject){
|
||||
//Check if member is already labeled as unknown
|
||||
const unknownMember = unknownMembers.find((member) => member.details === firstEntry.name);
|
||||
if (!unknownMember){
|
||||
unknownMembers.push({
|
||||
error: csvParserErrors.UNKNOWN_MEMBER,
|
||||
details: firstEntry.name,
|
||||
file: file.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (secondEntry && (secondEntry.event === ENABLE_PASSAGE_MODE || secondEntry.event === DISABLE_PASSAGE_MODE)){
|
||||
const event = (secondEntry.event === ENABLE_PASSAGE_MODE) ?
|
||||
doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED;
|
||||
|
||||
const dateTimeString = `${firstEntry.date} ${firstEntry.time}`;
|
||||
const timestamp = moment.utc(dateTimeString, 'MM/DD/YY HH:mm:ss A').toISOString();
|
||||
|
||||
//Verify that member is registered in OfficeRnD system
|
||||
if (memberObject){
|
||||
const entryData = {
|
||||
memberName: firstEntry.name,
|
||||
memberNumber: firstEntry['user no'],
|
||||
memberId: memberObject.memberId,
|
||||
timestamp,
|
||||
event,
|
||||
resourceId: mappingObject.resourceId,
|
||||
|
||||
};
|
||||
|
||||
parsedData.push(entryData);
|
||||
}
|
||||
i+=2;
|
||||
} else {
|
||||
errors.push({
|
||||
error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE,
|
||||
details: firstEntry,
|
||||
file: file.name,
|
||||
});
|
||||
i+=1;
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
error: csvParserErrors.INVALID_ENTRY_EXPECTED_USER,
|
||||
details: firstEntry,
|
||||
file: file.name,
|
||||
});
|
||||
i+=1;
|
||||
}
|
||||
}
|
||||
resolve({
|
||||
parsedData,
|
||||
unknownMembers,
|
||||
errors
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const writeDoorLockEvent = (entry) => {
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user