'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, TRIM_DLOCK_NAMES_LENGTH } = require('../../constants/constants'); const { fetchAllMembers } = require('../officeRnD/members'); const { getMappingsFromDatabase } = require('../officeRnD/resources'); const { getFirstReservationInBlock, getFirstPreviousBooking } = 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) => { const limitedMemberName = member.name ? member.name.substring(0, TRIM_DLOCK_NAMES_LENGTH) || undefined : undefined; if (limitedMemberName){ membersMap[limitedMemberName] = 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]; const trimmedName = firstEntry && firstEntry.name ? firstEntry.name.substring(0, TRIM_DLOCK_NAMES_LENGTH) || undefined : undefined; if (firstEntry && trimmedName && (firstEntry.event === USER_ENTRY_EVENT)){ const memberObject = membersMap[trimmedName]; if (!memberObject){ //Check if member is already labeled as unknown const unknownMember = unknownMembersMap[trimmedName]; if (!unknownMember){ unknownMembersMap[trimmedName] = trimmedName; unknownMembersToReport.push({ error: csvParserErrors.UNKNOWN_MEMBER, details: trimmedName, 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: trimmedName, 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 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; // if (reservation.memberId === '5ce785af422bdd00967fb781') { // console.log('======================='); // console.log('\tStart : ', moment.tz(reservation.start, reservation.timezone).format('DD.MM, HH:mm')); // console.log('\tEnd : ', moment.tz(reservation.end, reservation.timezone).format('DD.MM, HH:mm')); // console.log('\t----------------------------------'); // console.log('\tFrom time : ', fromTimestamp); // console.log('\tTo time : ', toTimestamp); // } const filters = { memberId, timestamp: { [Op.and]: [ {[Op.gt]: fromTimestamp}, {[Op.lte]: toTimestamp} ] }, resourceId, }; const order = [['timestamp', 'DESC']]; db.doorLockEvent.findAll({ 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)); // if (memberId === '5ce785af422bdd00967fb781') { // console.log('Start : ', moment.tz(reservation.start, UI_TIMEZONE).format('DD.MM HH:mm')); // console.log('End : ', moment.tz(reservation.end, UI_TIMEZONE).format('DD.MM HH:mm')); // console.log('\tPrevious reservation '); // console.log('\tStart : ', previousReservation ? moment.tz(previousReservation.start, UI_TIMEZONE).format('DD.MM HH:mm') : '-'); // console.log('\tEnd : ', previousReservation ? moment.tz(previousReservation.end, UI_TIMEZONE).format('DD.MM HH:mm') : '-'); // console.log('\t---------------------------'); // console.log('\tSearch for entries : '); // console.log('\tFrom : ', fromTimestamp ? moment.tz(fromTimestamp, UI_TIMEZONE).format('DD.MM HH:mm') : '-'); // console.log('\tTo : ', toTimestamp ? moment.tz(toTimestamp, UI_TIMEZONE).format('DD.MM HH:mm') : '-'); // console.log('\t---------------------------'); // console.log('\tEntries before reservation start : '); // } entriesBeforeReservationStart.forEach((entry) => { // if (memberId === '5ce785af422bdd00967fb781') { // console.log('\t', entry.event, '\t', moment.tz(entry.timestamp, UI_TIMEZONE).format('DD.MM HH:mm')); // } 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){ // if (memberId === '5ce785af422bdd00967fb781') { // console.log('\t=> FOUND UNLOCK ENTRY - NO NEED TO LOOK AFTER <='); // } resolve(candidateUnlockEntry); } else { candidateUnlockEntry = null; const numberOfEntriesLeft = entries.length - entriesBeforeReservationStart.length; const entriesAfterReservationStart = entries.slice(0, numberOfEntriesLeft); const invertedEntriesAfterReservationStart = entriesAfterReservationStart.reverse(); // if (memberId === '5ce785af422bdd00967fb781') { // console.log('\t-----------------------------'); // console.log('\tEntries after reservation start : '); // } invertedEntriesAfterReservationStart.forEach((entry) => { // if (memberId === '5ce785af422bdd00967fb781') { // console.log('\t', entry.event, '\t', moment.tz(entry.timestamp, UI_TIMEZONE).format('DD.MM HH:mm')); // } 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', 'DESC']]; db.doorLockEvent.findAll({ attributes, where: filters, order, }) .then((entries) => { const entriesBeforeReservationEnd = entries.filter(entry => moment.utc(entry.timestamp).isBefore(reservation.end)); if (entriesBeforeReservationEnd.length > 0){ const lastEntryInReservationTime = entriesBeforeReservationEnd[0]; if (lastEntryInReservationTime.event === doorLockEvents.USER_LOCKED){ resolve(lastEntryInReservationTime); return; } } // Phase 2 const numberOfEntriesLeft = entries.length - entriesBeforeReservationEnd.length; const entriesAfterReservationEnd = entries.slice(0, numberOfEntriesLeft).reverse(); if (entriesAfterReservationEnd.length > 0){ const firstEntryAfterReservation = entriesAfterReservationEnd[0]; if (firstEntryAfterReservation.event === doorLockEvents.USER_LOCKED){ resolve(firstEntryAfterReservation); return; } } resolve(null); }) .catch((error) => reject(error)); }); }; const getEntriesBetween = (fromTimestamp, toTimestamp, resourceId) => { return new Promise((resolve, reject) => { if (!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(async(entries) => { if (entries && entries.length > 0){ resolve(entries[0]); } else { //No entry found in this block of reservations, now check if there is unlock entry for the first reservation in block, before reservation start time try { const firstPreviousBookingBeforeFirstInBlock = await getFirstPreviousBooking(firstReservationInBlock); const unlockEntryForFirstInBlock = await getUnlockEntryForReservation(firstReservationInBlock, firstPreviousBookingBeforeFirstInBlock); if (unlockEntryForFirstInBlock){ resolve(unlockEntryForFirstInBlock); }else{ resolve(undefined); } }catch (e) { reject(e); } } }) .catch((error) => reject(error)); }) .catch((error) => reject(error)); }); }; module.exports = { parseDoorLockDataFile, writeDoorLockEvent, getUnlockEntryForReservation, getLockEntryForReservation, getEntriesBetween, getLastEntryForReservation, };