'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 } = 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 allMembers = result[1]; 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 = allMembers.find(member => member.name === 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, };