diff --git a/client/package.json b/client/package.json
index c87178e..16941b2 100644
--- a/client/package.json
+++ b/client/package.json
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"axios": "^0.18.0",
+ "fuse.js": "^3.4.5",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.0.3",
diff --git a/client/src/scenes/UploadDLockData/components/FileUpload.js b/client/src/scenes/UploadDLockData/components/FileUpload.js
index b185805..87b1038 100644
--- a/client/src/scenes/UploadDLockData/components/FileUpload.js
+++ b/client/src/scenes/UploadDLockData/components/FileUpload.js
@@ -1,37 +1,96 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
-import {Form} from "semantic-ui-react";
+import { Form } from "semantic-ui-react";
-import { uploadDoorLockData } from "../../../store/actions";
+import UnknownMapping from './UnknownMapping';
+
+import { uploadDoorLockData, fetchMappings } from "../../../store/actions";
class FileUpload extends Component {
constructor(props) {
super(props);
this.state = {
- file: null,
+ files: null,
+ unknownMappings: [],
};
this.onFileChange = this.onFileChange.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
}
+ componentDidMount() {
+ const { fetchMappings } = this.props;
+ fetchMappings();
+ }
+
+ componentWillReceiveProps(nextProps, nextContext) {
+ const { addedMapping } = nextProps;
+ const { unknownMappings } = this.state;
+
+ const filteredUnknownMappings = unknownMappings.filter(mapping => {
+ return mapping.officeSlug !== addedMapping.officeSlug
+ || mapping.resourceSlug !== addedMapping.resourceSlug
+ });
+
+ this.setState({unknownMappings: filteredUnknownMappings});
+ }
+
+ 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],
+ }
+ }
+
+ checkIfMappingExsists(mappingFromFileName) {
+ const { mappings } = this.props;
+ const { officeSlug, resourceSlug } = mappingFromFileName;
+
+ const { existingMappings } = mappings;
+
+ return existingMappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug));
+ }
+
onFileChange(event) {
- const file = event.target.files[0];
- this.setState({file});
+ const files = event.target.files;
+ const unknownMappings = [];
+
+
+ Array.from(files).forEach(file => {
+ const mappingFromFileName = this.extractMappingFromFileName(file.name);
+ if (!this.checkIfMappingExsists(mappingFromFileName)){
+ unknownMappings.push({
+ file: file.name,
+ officeId: null,
+ resourceId: null,
+ officeSlug: mappingFromFileName.officeSlug,
+ resourceSlug: mappingFromFileName.resourceSlug,
+ })
+ }
+ });
+
+ this.setState({files, unknownMappings});
};
onUploadClick() {
const { uploadDoorLockData } = this.props;
- const { file } = this.state;
+ const { files } = this.state;
- if (file) {
- uploadDoorLockData(file);
+ if (files) {
+ uploadDoorLockData(files);
}
};
render() {
- const { pending } = this.props;
+ const { pendingUpload } = this.props;
+ const { unknownMappings, files } = this.state;
+
+ const uploadDisabled = pendingUpload || unknownMappings.length > 0 || !files;
+
return (
- Upload
+ {
+ unknownMappings.map((mapping, index) =>
+ )
+ }
+
+ Upload
);
}
}
const mapStateToProps = (state) => ({
- pending: state.doorLockData.pending,
+ pendingUpload: state.doorLockData.pending,
+ pendingMappings: state.mappingsData.pending,
+ mappings: state.mappingsData.result,
+ addedMapping: state.addMapping.result,
});
const mapDispatchToProps = (dispatch) => ({
- uploadDoorLockData: (doorLockDataFile) => uploadDoorLockData(dispatch, doorLockDataFile)
+ uploadDoorLockData: (doorLockDataFiles) => uploadDoorLockData(dispatch, doorLockDataFiles),
+ fetchMappings: () => fetchMappings(dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(FileUpload);
diff --git a/client/src/scenes/UploadDLockData/components/UnknownMapping.js b/client/src/scenes/UploadDLockData/components/UnknownMapping.js
new file mode 100644
index 0000000..3bee93e
--- /dev/null
+++ b/client/src/scenes/UploadDLockData/components/UnknownMapping.js
@@ -0,0 +1,172 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import {Button, Dropdown, Message} from "semantic-ui-react";
+import Fuse from 'fuse.js';
+import { addNewMapping } from "../../../store/actions";
+
+class UnknownMapping extends Component {
+ constructor(props) {
+ super(props);
+
+ const guessedValues = this.guessDropdownValues(this.props);
+
+ this.state = {
+ selectedOfficeId: guessedValues.officeValue,
+ selectedResourceId: guessedValues.resourceValue,
+ }
+ }
+
+ componentWillReceiveProps(nextProps, nextContext) {
+ const guessedValues = this.guessDropdownValues(nextProps);
+ this.setState({selectedOfficeId: guessedValues.officeValue, selectedResourceId: guessedValues.resourceValue});
+ }
+
+ guessDropdownValues(props){
+ const { mappings, mapping } = props;
+
+ const offices = mappings && mappings.offices ? mappings.offices : [];
+ const resources = mappings && mappings.resources ? mappings.resources : [];
+
+ const fuzzySearchOptions = {
+ shouldSort: true,
+ threshold: 0.5,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: [
+ "officeName",
+ "resourceName",
+ ]
+ };
+
+ const officesFuse = new Fuse(offices, fuzzySearchOptions);
+ const fuzzyOfficeSearchResults = officesFuse.search(mapping.officeSlug);
+
+ let officeValue = null;
+ if (fuzzyOfficeSearchResults.length > 0){
+ officeValue = fuzzyOfficeSearchResults[0].officeId;
+ }else if (offices.length > 0){
+ officeValue = offices[0].officeId;
+ }
+
+ const filteredResources = resources.filter(resource => resource.officeId === officeValue);
+
+ const resourcesFuse = new Fuse(filteredResources, fuzzySearchOptions);
+ const fuzzyResourcesSearchResults = resourcesFuse.search(mapping.resourceSlug);
+
+ let resourceValue = null;
+ if (fuzzyResourcesSearchResults.length > 0){
+ resourceValue = fuzzyResourcesSearchResults[0].resourceId;
+ }else if (filteredResources.length > 0){
+ resourceValue = filteredResources[0].resourceId;
+ }
+
+ return {
+ officeValue,
+ resourceValue,
+ }
+ }
+
+ onOfficeChange(event, data) {
+ const { mappings } = this.props;
+
+ const selectedOfficeId = data.value || null;
+ const resources = mappings && mappings.resources ? mappings.resources : [];
+ const filteredResources = resources.filter(resource => resource.officeId === selectedOfficeId);
+ const selectedResourceId = filteredResources.length > 0 ? filteredResources[0].resourceId : null;
+ this.setState({selectedOfficeId, selectedResourceId});
+ }
+
+ onResourceChange(event, data) {
+ const selectedResourceId = data.value || null;
+ this.setState({selectedResourceId});
+ }
+
+ onSave(){
+ const { addNewMapping, mapping } = this.props;
+ const { selectedOfficeId, selectedResourceId } = this.state;
+ const officeSlug = mapping.officeSlug;
+ const resourceSlug = mapping.resourceSlug;
+
+ const newMapping = {
+ officeSlug,
+ resourceSlug,
+ officeId: selectedOfficeId,
+ resourceId: selectedResourceId,
+ };
+
+ addNewMapping(newMapping);
+ }
+
+ render() {
+ const { mapping, mappings } = this.props;
+ const { selectedOfficeId, selectedResourceId } = this.state;
+
+ const offices = mappings && mappings.offices ? mappings.offices : [];
+ const resources = mappings && mappings.resources ? mappings.resources : [];
+
+ const officeDropdownOptions = offices.map(office => {
+ return {
+ key: office.officeId,
+ value: office.officeId,
+ text: office.officeName,
+ }
+ });
+
+ const filteredResources = resources.filter(resource => resource.officeId === selectedOfficeId);
+
+ const resourceDropdownOptions = filteredResources.map(resource => {
+ return {
+ key: resource.resourceId,
+ value: resource.resourceId,
+ text: resource.resourceName,
+ }
+ });
+
+ const saveButtonDisabled = !selectedOfficeId || !selectedResourceId;
+
+ return (
+
+
+
+ {mapping.file}
+
+
+ This file contains the unknown location. Based on ORD data, it seems that this file is related to {' '}
+
+ {' '}
+ /
+ {' '}
+
+
+
+ Save
+
+
);
+ }
+}
+
+const mapStateToProps = (state) => ({
+ mappings: state.mappingsData.result,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ addNewMapping: (mapping) => addNewMapping(dispatch, mapping),
+});
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(UnknownMapping);
diff --git a/client/src/store/actions/doorLockActions.js b/client/src/store/actions/doorLockActions.js
index 00273f4..a5c5a01 100644
--- a/client/src/store/actions/doorLockActions.js
+++ b/client/src/store/actions/doorLockActions.js
@@ -6,9 +6,12 @@ import {
import API from '../../utilities/api';
-export const uploadDoorLockData = (dispatch, doorLockDataFile) => {
+export const uploadDoorLockData = (dispatch, doorLockDataFiles) => {
const formData = new FormData();
- formData.append('doorLockDataFile', doorLockDataFile);
+ const filesArray = Array.from(doorLockDataFiles) || [];
+ filesArray.forEach((file, index) => {
+ formData.append(`doorLockDataFile-${index}`, file);
+ });
const additionalConfig = {
headers: {'content-type': 'multipart/form-data'}
};
diff --git a/client/src/store/actions/index.js b/client/src/store/actions/index.js
index d21e20e..bc138de 100644
--- a/client/src/store/actions/index.js
+++ b/client/src/store/actions/index.js
@@ -1 +1,2 @@
export * from './doorLockActions';
+export * from './integrationActions';
diff --git a/client/src/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js
new file mode 100644
index 0000000..b61b58b
--- /dev/null
+++ b/client/src/store/actions/integrationActions.js
@@ -0,0 +1,34 @@
+import {
+ FETCH_MAPPINGS_PENDING,
+ FETCH_MAPPINGS_SUCCESS,
+ FETCH_MAPPINGS_FAILED,
+ ADD_NEW_MAPPING_PENDING,
+ ADD_NEW_MAPPING_SUCCESS,
+ ADD_NEW_MAPPING_FAILED,
+} from "../constants";
+
+import API from '../../utilities/api';
+
+export const fetchMappings = (dispatch) => {
+ dispatch({type: FETCH_MAPPINGS_PENDING});
+ API.get('integration/mappings')
+ .then(response => {
+ dispatch({type: FETCH_MAPPINGS_SUCCESS, payload: response.data});
+ })
+ .catch(error => {
+ dispatch({type: FETCH_MAPPINGS_FAILED, payload: error.response});
+ });
+};
+
+export const addNewMapping = (dispatch, mapping) => {
+ dispatch({type: ADD_NEW_MAPPING_PENDING});
+ API.post('integration/mappings', {
+ mapping
+ })
+ .then(response => {
+ dispatch({type: ADD_NEW_MAPPING_SUCCESS, payload: response.data});
+ })
+ .catch(error => {
+ dispatch({type: ADD_NEW_MAPPING_FAILED, payload: error.response});
+ });
+};
diff --git a/client/src/store/constants.js b/client/src/store/constants.js
index 5034375..aecc675 100644
--- a/client/src/store/constants.js
+++ b/client/src/store/constants.js
@@ -1,3 +1,11 @@
export const UPLOAD_DOOR_LOCK_DATA_PENDING = 'UPLOAD_DOOR_LOCK_DATA_PENDING';
export const UPLOAD_DOOR_LOCK_DATA_SUCCESS = 'UPLOAD_DOOR_LOCK_DATA_SUCCESS';
export const UPLOAD_DOOR_LOCK_DATA_FAILED = 'UPLOAD_DOOR_LOCK_DATA_FAILED';
+
+export const FETCH_MAPPINGS_PENDING = 'FETCH_MAPPINGS_PENDING';
+export const FETCH_MAPPINGS_SUCCESS = 'FETCH_MAPPINGS_SUCCESS';
+export const FETCH_MAPPINGS_FAILED = 'FETCH_MAPPINGS_FAILED';
+
+export const ADD_NEW_MAPPING_PENDING = 'ADD_NEW_MAPPING_PENDING';
+export const ADD_NEW_MAPPING_SUCCESS = 'ADD_NEW_MAPPING_SUCCESS';
+export const ADD_NEW_MAPPING_FAILED = 'ADD_NEW_MAPPING_FAILED';
diff --git a/client/src/store/reducers/addMappingReducer.js b/client/src/store/reducers/addMappingReducer.js
new file mode 100644
index 0000000..c360882
--- /dev/null
+++ b/client/src/store/reducers/addMappingReducer.js
@@ -0,0 +1,38 @@
+import {
+ ADD_NEW_MAPPING_PENDING,
+ ADD_NEW_MAPPING_SUCCESS,
+ ADD_NEW_MAPPING_FAILED,
+} from "../constants";
+
+const initialState = {
+ pending: false,
+ result: null,
+ error: null,
+};
+
+export const addMapping = (state, action) => {
+ state = state || initialState;
+ action = action || {};
+
+ switch(action.type){
+ case ADD_NEW_MAPPING_PENDING:
+ return Object.assign({}, state, {
+ pending: true,
+ error: null,
+ });
+ case ADD_NEW_MAPPING_SUCCESS:
+ return Object.assign({}, state, {
+ pending: false,
+ result: action.payload,
+ error: null,
+ });
+ case ADD_NEW_MAPPING_FAILED:
+ return Object.assign({}, state, {
+ pending: false,
+ result: {},
+ error: action.payload,
+ });
+ default:
+ return state;
+ }
+};
diff --git a/client/src/store/reducers/index.js b/client/src/store/reducers/index.js
index 274b7bd..ed2a547 100644
--- a/client/src/store/reducers/index.js
+++ b/client/src/store/reducers/index.js
@@ -1,8 +1,12 @@
import { combineReducers } from "redux";
import { doorLockData} from "./doorLockReducers";
+import { mappingsData } from "./mappingsReducer";
+import { addMapping } from './addMappingReducer';
export const rootReducer = combineReducers({
- doorLockData
+ doorLockData,
+ mappingsData,
+ addMapping,
});
diff --git a/client/src/store/reducers/mappingsReducer.js b/client/src/store/reducers/mappingsReducer.js
new file mode 100644
index 0000000..95e0d9d
--- /dev/null
+++ b/client/src/store/reducers/mappingsReducer.js
@@ -0,0 +1,38 @@
+import {
+ FETCH_MAPPINGS_PENDING,
+ FETCH_MAPPINGS_SUCCESS,
+ FETCH_MAPPINGS_FAILED,
+} from "../constants";
+
+const initialState = {
+ pending: false,
+ result: null,
+ error: null,
+};
+
+export const mappingsData = (state, action) => {
+ state = state || initialState;
+ action = action || {};
+
+ switch(action.type){
+ case FETCH_MAPPINGS_PENDING:
+ return Object.assign({}, state, {
+ pending: true,
+ error: null,
+ });
+ case FETCH_MAPPINGS_SUCCESS:
+ return Object.assign({}, state, {
+ pending: false,
+ result: action.payload,
+ error: null,
+ });
+ case FETCH_MAPPINGS_FAILED:
+ return Object.assign({}, state, {
+ pending: false,
+ result: {},
+ error: action.payload,
+ });
+ default:
+ return state;
+ }
+};
diff --git a/client/yarn.lock b/client/yarn.lock
index 8413b54..a426655 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -3680,6 +3680,10 @@ functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+fuse.js@^3.4.5:
+ version "3.4.5"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
diff --git a/config/config.json b/config/config.json
index 9d696a2..bb7a042 100644
--- a/config/config.json
+++ b/config/config.json
@@ -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"
diff --git a/constants/constants.js b/constants/constants.js
index db8cdc9..1650930 100644
--- a/constants/constants.js
+++ b/constants/constants.js
@@ -2,20 +2,66 @@ const USER_ENTRY_EVENT = 'User Entry';
const ENABLE_PASSAGE_MODE = 'Enable Passage Mode by Group 2';
const DISABLE_PASSAGE_MODE = 'Disable Passage Mode by Group 2';
-const USER_LOCKED_DOOR = 'locked';
-const USER_UNLOCKED_DOOR = 'unlocked';
-
const VALID_CSV_HEADERS = ['Date', 'Time', 'User No', 'Name', 'Event'];
+
+const doorLockEvents = {
+ USER_LOCKED: 'locked',
+ USER_UNLOCKED: 'unlocked',
+};
+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 = {
@@ -23,8 +69,10 @@ module.exports = {
USER_ENTRY_EVENT,
ENABLE_PASSAGE_MODE,
DISABLE_PASSAGE_MODE,
- USER_LOCKED_DOOR,
- USER_UNLOCKED_DOOR,
csvParserErrors,
officeRnDAPIErrors,
+ doorLockEvents,
+ unlockedIncidentLevelsPrices,
+ integrationServiceErrors,
+ incidentType,
};
diff --git a/controllers/doorLock.js b/controllers/doorLock.js
index 3e57137..68db008 100644
--- a/controllers/doorLock.js
+++ b/controllers/doorLock.js
@@ -1,23 +1,24 @@
'use strict';
-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;
const uploadDoorLockData = (req, res) => {
const form = new IncomingForm();
- const parsingResults = [];
+ const fileParsers = [];
form.on('file', (field, file) => {
if (file && file.type === 'text/csv') {
- parsingResults.push(parseDoorLockDataFile(file));
+ fileParsers.push(parseDoorLockDataFile(file));
}
});
form.on('end', () => {
- Promise.all(parsingResults)
+ Promise.all(fileParsers)
.then((parserResults) => {
const parsedData = [];
const parserErrors = [];
@@ -29,30 +30,39 @@ 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(() => {
- res.status(500).send(officeRnDAPIErrors.FAILED_TO_FETCH_MEMBERS);
+ .catch((error) => {
+ res.status(500).send(error);
});
});
-
form.parse(req);
};
diff --git a/controllers/integration.js b/controllers/integration.js
new file mode 100644
index 0000000..6d56b8c
--- /dev/null
+++ b/controllers/integration.js
@@ -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,
+};
diff --git a/environment.env b/environment.env
index b602a2b..4283c09 100644
--- a/environment.env
+++ b/environment.env
@@ -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
diff --git a/migrations/20190603114921-change-resource-column-name-in-tables.js b/migrations/20190603114921-change-resource-column-name-in-tables.js
new file mode 100644
index 0000000..a96138b
--- /dev/null
+++ b/migrations/20190603114921-change-resource-column-name-in-tables.js
@@ -0,0 +1,21 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.sequelize.transaction((t) => {
+ return Promise.all([
+ queryInterface.renameColumn('bookingReservations', 'resource', 'resourceId'),
+ queryInterface.renameColumn('doorLockIncidents', 'resource', 'resourceId'),
+ ]);
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.sequelize.transaction((t) => {
+ return Promise.all([
+ queryInterface.renameColumn('doorLockIncidents', 'resourceId', 'resource'),
+ queryInterface.renameColumn('bookingReservations', 'resourceId', 'resource'),
+ ]);
+ });
+ }
+};
diff --git a/migrations/20190603115116-add-columns-to-booking-reservations-table.js b/migrations/20190603115116-add-columns-to-booking-reservations-table.js
new file mode 100644
index 0000000..04c777c
--- /dev/null
+++ b/migrations/20190603115116-add-columns-to-booking-reservations-table.js
@@ -0,0 +1,27 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.sequelize.transaction((t) => {
+ return Promise.all([
+ queryInterface.addColumn('bookingReservations', 'timezone', {
+ type: Sequelize.TEXT,
+ after: 'end'
+ }),
+ queryInterface.addColumn('bookingReservations', 'canceled', {
+ type: Sequelize.BOOLEAN,
+ after: 'timezone'
+ })
+ ]);
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.sequelize.transaction((t) => {
+ return Promise.all([
+ queryInterface.removeColumn('bookingReservations', 'canceled'),
+ queryInterface.removeColumn('bookingReservations', 'timezone')
+ ]);
+ });
+ }
+};
diff --git a/migrations/20190608093226-create-office-resource-name-mapping.js.js b/migrations/20190608093226-create-office-resource-name-mapping.js.js
new file mode 100644
index 0000000..c158392
--- /dev/null
+++ b/migrations/20190608093226-create-office-resource-name-mapping.js.js
@@ -0,0 +1,41 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('officeResourceMappings', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ officeSlug: {
+ allowNull: false,
+ type: Sequelize.TEXT,
+ },
+ officeId: {
+ allowNull: false,
+ type: Sequelize.TEXT,
+ },
+ resourceSlug: {
+ allowNull: false,
+ type: Sequelize.TEXT,
+ },
+ resourceId: {
+ allowNull: false,
+ type: Sequelize.TEXT,
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('officeResourceMappings');
+ }
+};
diff --git a/migrations/20190608093618-add-resource-column-in-door-lock-events-table.js.js b/migrations/20190608093618-add-resource-column-in-door-lock-events-table.js.js
new file mode 100644
index 0000000..c716163
--- /dev/null
+++ b/migrations/20190608093618-add-resource-column-in-door-lock-events-table.js.js
@@ -0,0 +1,14 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn('doorLockEvents', 'resourceId', {
+ type: Sequelize.TEXT,
+ after: 'memberId',
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn('doorLockEvents', 'resourceId');
+ }
+};
diff --git a/migrations/20190611072735-add-office-column-in-booking-reservations-table.js b/migrations/20190611072735-add-office-column-in-booking-reservations-table.js
new file mode 100644
index 0000000..efbf82d
--- /dev/null
+++ b/migrations/20190611072735-add-office-column-in-booking-reservations-table.js
@@ -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');
+ }
+};
diff --git a/migrations/20190612123751-rename-door-lock-incidents-table.js b/migrations/20190612123751-rename-door-lock-incidents-table.js
new file mode 100644
index 0000000..f71ac1c
--- /dev/null
+++ b/migrations/20190612123751-rename-door-lock-incidents-table.js
@@ -0,0 +1,11 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.renameTable('doorLockIncidents', 'unscheduledIncidents');
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.renameTable('unscheduledIncidents', 'doorLockIncidents');
+ }
+};
diff --git a/migrations/20190612123950-alter-unscheduledIncidents-table.js b/migrations/20190612123950-alter-unscheduledIncidents-table.js
new file mode 100644
index 0000000..bcd18ea
--- /dev/null
+++ b/migrations/20190612123950-alter-unscheduledIncidents-table.js
@@ -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'
+ }),
+ ]);
+ });
+ }
+};
diff --git a/migrations/20190612125150-add-unlocked-incidents-table.js b/migrations/20190612125150-add-unlocked-incidents-table.js
new file mode 100644
index 0000000..721057b
--- /dev/null
+++ b/migrations/20190612125150-add-unlocked-incidents-table.js
@@ -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');
+ }
+};
diff --git a/models/bookingReservation.js b/models/bookingReservation.js
index 3a10d3e..d977de3 100644
--- a/models/bookingReservation.js
+++ b/models/bookingReservation.js
@@ -4,9 +4,13 @@ module.exports = (sequelize, DataTypes) => {
const bookingReservation = sequelize.define('bookingReservation', {
reservationId: DataTypes.TEXT,
memberId: DataTypes.TEXT,
- resource: DataTypes.TEXT,
+ officeId: DataTypes.TEXT,
+ resourceId: DataTypes.TEXT,
start: DataTypes.DATE,
end: DataTypes.DATE,
+ timezone: DataTypes.TEXT,
+ canceled: DataTypes.BOOLEAN,
+
}, {});
bookingReservation.associate = function(models) {
// associations can be defined here
diff --git a/models/doorLockEvent.js b/models/doorLockEvent.js
index e0a06e1..5a4a8d8 100644
--- a/models/doorLockEvent.js
+++ b/models/doorLockEvent.js
@@ -1,15 +1,16 @@
'use strict';
-const { USER_LOCKED_DOOR, USER_UNLOCKED_DOOR } = require('../constants/constants');
+const { doorLockEvents } = require('../constants/constants');
module.exports = (sequelize, DataTypes) => {
const doorLockEvent = sequelize.define('doorLockEvent', {
memberName: DataTypes.TEXT,
memberNumber: DataTypes.INTEGER,
memberId: DataTypes.TEXT,
+ resourceId: DataTypes.TEXT,
event: {
type: DataTypes.ENUM,
- values: [USER_LOCKED_DOOR, USER_UNLOCKED_DOOR]
+ values: [doorLockEvents.USER_LOCKED, doorLockEvents.USER_UNLOCKED]
},
timestamp: DataTypes.DATE,
}, {});
diff --git a/models/officeResourceMapping.js b/models/officeResourceMapping.js
new file mode 100644
index 0000000..7005307
--- /dev/null
+++ b/models/officeResourceMapping.js
@@ -0,0 +1,14 @@
+'use strict';
+
+module.exports = (sequelize, DataTypes) => {
+ const officeResourceMapping = sequelize.define('officeResourceMapping', {
+ officeSlug: DataTypes.TEXT,
+ officeId: DataTypes.TEXT,
+ resourceSlug: DataTypes.TEXT,
+ resourceId: DataTypes.TEXT,
+ }, {});
+ officeResourceMapping.associate = function(models) {
+ // associations can be defined here
+ };
+ return officeResourceMapping;
+};
diff --git a/models/unlockedIncident.js b/models/unlockedIncident.js
new file mode 100644
index 0000000..89c8c34
--- /dev/null
+++ b/models/unlockedIncident.js
@@ -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;
+};
diff --git a/models/unscheduledIncident.js b/models/unscheduledIncident.js
new file mode 100644
index 0000000..1f781b9
--- /dev/null
+++ b/models/unscheduledIncident.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const { doorLockEvents } = require('../constants/constants');
+
+module.exports = (sequelize, DataTypes) => {
+ const unscheduledIncident = sequelize.define('unscheduledIncident', {
+ reservationId: DataTypes.TEXT,
+ memberId: DataTypes.TEXT,
+ resourceId: DataTypes.TEXT,
+ bookingStart: DataTypes.DATE,
+ bookingEnd: DataTypes.DATE,
+ doorLockEventTimestamp: DataTypes.DATE,
+ doorLockEventType: {
+ type: DataTypes.ENUM,
+ values: [doorLockEvents.USER_LOCKED, doorLockEvents.USER_UNLOCKED]
+ },
+ chargePrice: DataTypes.FLOAT,
+ timeIntervalsToCharge: DataTypes.INTEGER,
+ totalChargeFee: DataTypes.FLOAT,
+ }, {});
+ unscheduledIncident.associate = function(models) {
+ // associations can be defined here
+ };
+ return unscheduledIncident;
+};
diff --git a/package-lock.json b/package-lock.json
index 85b4ea6..ec355d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -120,12 +120,19 @@
"dev": true
},
"axios": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
- "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
+ "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": {
- "follow-redirects": "^1.3.0",
- "is-buffer": "^1.1.5"
+ "follow-redirects": "1.5.10",
+ "is-buffer": "^2.0.2"
+ },
+ "dependencies": {
+ "is-buffer": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+ "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
+ }
}
},
"babel-runtime": {
@@ -1128,20 +1135,25 @@
}
},
"follow-redirects": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
- "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
- "debug": "^3.2.6"
+ "debug": "=3.1.0"
},
"dependencies": {
"debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
- "ms": "^2.1.1"
+ "ms": "2.0.0"
}
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
@@ -1982,7 +1994,8 @@
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
- "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
},
"is-ci": {
"version": "1.2.1",
diff --git a/package.json b/package.json
index b8e611e..c51ec9a 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -21,13 +21,14 @@
"node": "11.12.x"
},
"dependencies": {
- "axios": "^0.18.0",
+ "axios": "^0.19.0",
"csv-parser": "^2.3.0",
"dotenv": "^8.0.0",
"express": "^4.17.0",
"express-basic-auth": "^1.2.0",
"formidable": "^1.2.1",
"moment": "^2.24.0",
+ "moment-timezone": "^0.5.25",
"pg": "^7.11.0",
"sequelize": "^5.8.6",
"sequelize-cli": "^5.4.0"
diff --git a/routes/index.js b/routes/index.js
index f5b4e2c..6e007a2 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -2,11 +2,19 @@
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
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();
router.get('/', apiStatusCheck);
+
router.post('/doorLock/upload', uploadDoorLockData);
+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;
diff --git a/server.js b/server.js
index bbb332e..ff8a27d 100644
--- a/server.js
+++ b/server.js
@@ -13,6 +13,8 @@ const app = express();
const port = process.env.PORT || 5000;
+app.use(express.json());
+
app.use('/api', routes);
app.use(basicAuth({
diff --git a/services/doorLock/doorLock.js b/services/doorLock/doorLock.js
new file mode 100644
index 0000000..233df5d
--- /dev/null
+++ b/services/doorLock/doorLock.js
@@ -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,
+};
diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js
new file mode 100644
index 0000000..e5a0545
--- /dev/null
+++ b/services/integration/doorLockCharges.js
@@ -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
+};
diff --git a/services/officeRnD/bookings.js b/services/officeRnD/bookings.js
index ad834bd..e63d3d3 100644
--- a/services/officeRnD/bookings.js
+++ b/services/officeRnD/bookings.js
@@ -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,26 +18,118 @@ const fetchAllBookings = () => {
cleanedBookingReservations.push({
reservationId: fullBookingEntry['_id'],
memberId: fullBookingEntry.member,
- resource: fullBookingEntry.resourceId,
+ 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) => {
- db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}})
- .then()
- .catch();
+ return db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}});
};
module.exports = {
fetchAllBookings,
writeBookingReservation,
+ getAllFinishedBookings,
+ getFirstNextBooking,
+ getFirstPreviousBooking,
};
diff --git a/services/officeRnD/resources.js b/services/officeRnD/resources.js
new file mode 100644
index 0000000..95b1d64
--- /dev/null
+++ b/services/officeRnD/resources.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const db = require('../../models/index');
+
+const { API } = require('../../helpers/api');
+
+const fetchOffices = () => {
+ return new Promise((resolve, reject) => {
+ API.get('/offices')
+ .then((result) => {
+ const offices = result.data || [];
+ const cleanedOffices = [];
+ offices.forEach(office => {
+ cleanedOffices.push({
+ officeId: office['_id'],
+ officeName: office.name,
+ });
+ });
+ resolve(cleanedOffices);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+};
+
+const fetchResources = () => {
+ return new Promise((resolve, reject) => {
+ API.get('/resources')
+ .then((result) => {
+ const resources = result.data || [];
+ const cleanedResources = [];
+ resources.forEach(resource => {
+ cleanedResources.push({
+ resourceId: resource['_id'],
+ resourceName: resource.name,
+ officeId: resource.office,
+ });
+ });
+ resolve(cleanedResources);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+};
+
+const getMappingsFromDatabase = () => {
+ return db.officeResourceMapping.findAll();
+};
+
+const saveNewMappingToDatabase = (mapping) => {
+ return db.officeResourceMapping.findOrCreate({where: {...mapping}, defaults: {...mapping}});
+};
+
+module.exports = {
+ getMappingsFromDatabase,
+ fetchOffices,
+ fetchResources,
+ saveNewMappingToDatabase,
+};