'use strict'; const db = require('../../models'); const fs = require('fs'); const csv = require('csv-parser'); const moment = require('moment-timezone'); const Op = require('sequelize').Op; const { UI_TIMEZONE, USER_ENTRY_EVENT, ENABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE, VALID_CSV_HEADERS, doorLockEvents, csvParserErrors, } = require('../../constants/constants'); const { fetchAllMembers } = require('../officeRnD/members'); const { getMappingsFromDatabase } = require('../officeRnD/resources'); const { getFirstReservationInBlock } = require('../officeRnD/bookings'); 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 unknownMembersToReport = []; let isValidFile = true; const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()]; Promise.all(prefetchDataJobs) .then(result => { const mappings = result[0]; const allMembers = result[1]; const membersMap = {}; const unknownMembersMap = {}; allMembers.forEach((member) => membersMap[member.name] = member); 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 = membersMap[firstEntry.name]; if (!memberObject){ //Check if member is already labeled as unknown const unknownMember = unknownMembersMap[firstEntry.name]; if (!unknownMember){ unknownMembersMap[firstEntry.name] = firstEntry.name; unknownMembersToReport.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.tz(dateTimeString, 'MM/DD/YY HH:mm:ss A', UI_TIMEZONE).tz('UTC').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: unknownMembersToReport, errors }); }); }) .catch(error => { reject(error); }); }); }; const writeDoorLockEvent = (entry) => { return db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}}); }; const getUnlockEntryForReservation = (reservation, previousReservation) => { return new Promise((resolve, reject) => { const { memberId, resourceId } = reservation; const attributes = ['memberName', 'event', 'timestamp', 'resourceId']; 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: { [Op.and]: [ {[Op.gt]: fromTimestamp}, {[Op.lte]: toTimestamp} ] }, resourceId, }; const order = [['timestamp', 'DESC']]; db.doorLockEvent.findAll({ attributes, where: filters, order, }) .then((entries) => { let candidateUnlockEntry = null; let pairedLockEntry = null; let eventFound = false; const entriesBeforeReservationStart = entries.filter((entry) => moment.utc(entry.timestamp).isBefore(reservation.start)); entriesBeforeReservationStart.forEach((entry) => { if (!eventFound) { if (entry.event === doorLockEvents.USER_UNLOCKED) { if (pairedLockEntry) { pairedLockEntry = null; candidateUnlockEntry = null; } else { candidateUnlockEntry = entry; eventFound = true; } } if (entry.event === doorLockEvents.USER_LOCKED){ pairedLockEntry = entry; } } }); if (eventFound){ resolve(candidateUnlockEntry); } else { candidateUnlockEntry = null; const numberOfEntriesLeft = entries.length - entriesBeforeReservationStart.length; const entriesAfterReservationStart = entries.slice(0, numberOfEntriesLeft); entriesAfterReservationStart.forEach((entry) => { if (!eventFound) { if (entry.event === doorLockEvents.USER_UNLOCKED) { eventFound = true; candidateUnlockEntry = entry; } } }); resolve(candidateUnlockEntry); } }) .catch((error) => reject(error)); }); }; const getLockEntryForReservation = (reservation, nextReservation) => { return new Promise((resolve, reject) => { const { memberId, resourceId } = reservation; 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 = nextReservationStartMoment && nextReservationStartMoment.tz(UI_TIMEZONE).isSame(reservationStartMoment.tz(UI_TIMEZONE), 'day') ? nextReservation.start : reservationStartMoment.tz(UI_TIMEZONE).endOf('day').toISOString(); const filters = { memberId, timestamp: { [Op.and]: [ {[Op.gt]: fromTimestamp}, {[Op.lte]: toTimestamp} ] }, resourceId, }; const order = [['timestamp', 'ASC']]; db.doorLockEvent.findAll({ attributes, where: filters, order, }) .then((entries) => { let candidateLockEntry = null; let pairedUnlockEntry = null; let eventFound = false; const entriesAfterReservationEnd = entries.filter((entry) => moment.utc(entry.timestamp).isAfter(reservation.end)); entriesAfterReservationEnd.forEach((entry) => { if (!eventFound) { if (entry.event === doorLockEvents.USER_LOCKED) { if (pairedUnlockEntry) { pairedUnlockEntry = null; candidateLockEntry = null; } else { candidateLockEntry = entry; eventFound = true; } } if (entry.event === doorLockEvents.USER_UNLOCKED){ pairedUnlockEntry = entry; } } }); if (eventFound){ resolve(candidateLockEntry); } else { candidateLockEntry = null; const numberOfEntriesLeft = entries.length - entriesAfterReservationEnd.length; const entriesBeforeReservationEnd = entries.slice(0, numberOfEntriesLeft); entriesBeforeReservationEnd.reverse().forEach((entry) => { if (!eventFound) { if (entry.event === doorLockEvents.USER_LOCKED) { eventFound = true; candidateLockEntry = entry; } } }); resolve(candidateLockEntry); } }) .catch((error) => reject(error)); }); }; 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, };