Calculate door lock charges
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
"fuse.js": "^3.4.5",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-redux": "^7.0.3",
|
"react-redux": "^7.0.3",
|
||||||
|
|||||||
@@ -1,37 +1,96 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
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 {
|
class FileUpload extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
file: null,
|
files: null,
|
||||||
|
unknownMappings: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onFileChange = this.onFileChange.bind(this);
|
this.onFileChange = this.onFileChange.bind(this);
|
||||||
this.onUploadClick = this.onUploadClick.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) {
|
onFileChange(event) {
|
||||||
const file = event.target.files[0];
|
const files = event.target.files;
|
||||||
this.setState({file});
|
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() {
|
onUploadClick() {
|
||||||
const { uploadDoorLockData } = this.props;
|
const { uploadDoorLockData } = this.props;
|
||||||
const { file } = this.state;
|
const { files } = this.state;
|
||||||
|
|
||||||
if (file) {
|
if (files) {
|
||||||
uploadDoorLockData(file);
|
uploadDoorLockData(files);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { pending } = this.props;
|
const { pendingUpload } = this.props;
|
||||||
|
const { unknownMappings, files } = this.state;
|
||||||
|
|
||||||
|
const uploadDisabled = pendingUpload || unknownMappings.length > 0 || !files;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
@@ -39,21 +98,34 @@ class FileUpload extends Component {
|
|||||||
required
|
required
|
||||||
label="Select DLock file"
|
label="Select DLock file"
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
onChange={this.onFileChange}
|
onChange={this.onFileChange}
|
||||||
/>
|
/>
|
||||||
<Form.Button onClick={this.onUploadClick} disabled={pending} >Upload</Form.Button>
|
{
|
||||||
|
unknownMappings.map((mapping, index) =>
|
||||||
|
<UnknownMapping
|
||||||
|
key={`unknown-mapping-${index}`}
|
||||||
|
mapping={mapping}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
<br/>
|
||||||
|
<Form.Button onClick={this.onUploadClick} disabled={uploadDisabled} >Upload</Form.Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
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) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
uploadDoorLockData: (doorLockDataFile) => uploadDoorLockData(dispatch, doorLockDataFile)
|
uploadDoorLockData: (doorLockDataFiles) => uploadDoorLockData(dispatch, doorLockDataFiles),
|
||||||
|
fetchMappings: () => fetchMappings(dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(FileUpload);
|
export default connect(mapStateToProps, mapDispatchToProps)(FileUpload);
|
||||||
|
|||||||
172
client/src/scenes/UploadDLockData/components/UnknownMapping.js
Normal file
172
client/src/scenes/UploadDLockData/components/UnknownMapping.js
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<br/>
|
||||||
|
<Message>
|
||||||
|
<Message.Header>{mapping.file}</Message.Header>
|
||||||
|
<br/>
|
||||||
|
<span>
|
||||||
|
This file contains the unknown location. Based on ORD data, it seems that this file is related to {' '}
|
||||||
|
<Dropdown
|
||||||
|
inline
|
||||||
|
options={officeDropdownOptions}
|
||||||
|
onChange={this.onOfficeChange.bind(this)}
|
||||||
|
value={selectedOfficeId}
|
||||||
|
/>
|
||||||
|
{' '}
|
||||||
|
/
|
||||||
|
{' '}
|
||||||
|
<Dropdown
|
||||||
|
inline
|
||||||
|
options={resourceDropdownOptions}
|
||||||
|
onChange={this.onResourceChange.bind(this)}
|
||||||
|
value={selectedResourceId}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<br/>
|
||||||
|
<Button
|
||||||
|
disabled={saveButtonDisabled}
|
||||||
|
onClick={this.onSave.bind(this)}
|
||||||
|
>Save</Button>
|
||||||
|
</Message>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
mappings: state.mappingsData.result,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
addNewMapping: (mapping) => addNewMapping(dispatch, mapping),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(UnknownMapping);
|
||||||
@@ -6,9 +6,12 @@ import {
|
|||||||
|
|
||||||
import API from '../../utilities/api';
|
import API from '../../utilities/api';
|
||||||
|
|
||||||
export const uploadDoorLockData = (dispatch, doorLockDataFile) => {
|
export const uploadDoorLockData = (dispatch, doorLockDataFiles) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('doorLockDataFile', doorLockDataFile);
|
const filesArray = Array.from(doorLockDataFiles) || [];
|
||||||
|
filesArray.forEach((file, index) => {
|
||||||
|
formData.append(`doorLockDataFile-${index}`, file);
|
||||||
|
});
|
||||||
const additionalConfig = {
|
const additionalConfig = {
|
||||||
headers: {'content-type': 'multipart/form-data'}
|
headers: {'content-type': 'multipart/form-data'}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './doorLockActions';
|
export * from './doorLockActions';
|
||||||
|
export * from './integrationActions';
|
||||||
|
|||||||
34
client/src/store/actions/integrationActions.js
Normal file
34
client/src/store/actions/integrationActions.js
Normal file
@@ -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});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
export const UPLOAD_DOOR_LOCK_DATA_PENDING = 'UPLOAD_DOOR_LOCK_DATA_PENDING';
|
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_SUCCESS = 'UPLOAD_DOOR_LOCK_DATA_SUCCESS';
|
||||||
export const UPLOAD_DOOR_LOCK_DATA_FAILED = 'UPLOAD_DOOR_LOCK_DATA_FAILED';
|
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';
|
||||||
|
|||||||
38
client/src/store/reducers/addMappingReducer.js
Normal file
38
client/src/store/reducers/addMappingReducer.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
|
|
||||||
import { doorLockData} from "./doorLockReducers";
|
import { doorLockData} from "./doorLockReducers";
|
||||||
|
import { mappingsData } from "./mappingsReducer";
|
||||||
|
import { addMapping } from './addMappingReducer';
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
doorLockData
|
doorLockData,
|
||||||
|
mappingsData,
|
||||||
|
addMapping,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
38
client/src/store/reducers/mappingsReducer.js
Normal file
38
client/src/store/reducers/mappingsReducer.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3680,6 +3680,10 @@ functional-red-black-tree@^1.0.1:
|
|||||||
version "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"
|
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:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
"username": "docker",
|
"username": "docker",
|
||||||
"password": "docker",
|
"password": "docker",
|
||||||
"database": "CrmIntegration",
|
"database": "CrmIntegration",
|
||||||
"port": "5432",
|
"port": "5431",
|
||||||
"dialect": "postgres"
|
"dialect": "postgres",
|
||||||
|
"logging": false
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"use_env_variable": "DATABASE_URL"
|
"use_env_variable": "DATABASE_URL"
|
||||||
|
|||||||
@@ -2,20 +2,66 @@ const USER_ENTRY_EVENT = 'User Entry';
|
|||||||
const ENABLE_PASSAGE_MODE = 'Enable Passage Mode by Group 2';
|
const ENABLE_PASSAGE_MODE = 'Enable Passage Mode by Group 2';
|
||||||
const DISABLE_PASSAGE_MODE = 'Disable 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 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 = {
|
const csvParserErrors = {
|
||||||
INVALID_HEADERS: 'Invalid headers',
|
INVALID_HEADERS: 'Invalid headers',
|
||||||
INVALID_ENTRY_EXPECTED_USER: 'Invalid entry type. Expected user entry type',
|
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',
|
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',
|
UNKNOWN_MEMBER: 'Member is not registered in OfficeRnD system',
|
||||||
|
GENERIC_ERROR: 'There was error while parsing uploaded file(s)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const officeRnDAPIErrors = {
|
const officeRnDAPIErrors = {
|
||||||
FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members',
|
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 = {
|
module.exports = {
|
||||||
@@ -23,8 +69,10 @@ module.exports = {
|
|||||||
USER_ENTRY_EVENT,
|
USER_ENTRY_EVENT,
|
||||||
ENABLE_PASSAGE_MODE,
|
ENABLE_PASSAGE_MODE,
|
||||||
DISABLE_PASSAGE_MODE,
|
DISABLE_PASSAGE_MODE,
|
||||||
USER_LOCKED_DOOR,
|
|
||||||
USER_UNLOCKED_DOOR,
|
|
||||||
csvParserErrors,
|
csvParserErrors,
|
||||||
officeRnDAPIErrors,
|
officeRnDAPIErrors,
|
||||||
|
doorLockEvents,
|
||||||
|
unlockedIncidentLevelsPrices,
|
||||||
|
integrationServiceErrors,
|
||||||
|
incidentType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { parseDoorLockDataFile, writeDoorLockEvent } = require("../services/doorLock");
|
const { parseDoorLockDataFile, writeDoorLockEvent } = require('../services/doorLock/doorLock');
|
||||||
const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings');
|
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 IncomingForm = require('formidable').IncomingForm;
|
||||||
|
|
||||||
const uploadDoorLockData = (req, res) => {
|
const uploadDoorLockData = (req, res) => {
|
||||||
const form = new IncomingForm();
|
const form = new IncomingForm();
|
||||||
const parsingResults = [];
|
const fileParsers = [];
|
||||||
|
|
||||||
form.on('file', (field, file) => {
|
form.on('file', (field, file) => {
|
||||||
if (file && file.type === 'text/csv') {
|
if (file && file.type === 'text/csv') {
|
||||||
parsingResults.push(parseDoorLockDataFile(file));
|
fileParsers.push(parseDoorLockDataFile(file));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
form.on('end', () => {
|
form.on('end', () => {
|
||||||
Promise.all(parsingResults)
|
Promise.all(fileParsers)
|
||||||
.then((parserResults) => {
|
.then((parserResults) => {
|
||||||
const parsedData = [];
|
const parsedData = [];
|
||||||
const parserErrors = [];
|
const parserErrors = [];
|
||||||
@@ -29,30 +30,39 @@ const uploadDoorLockData = (req, res) => {
|
|||||||
unknownMembers.push(...parserResult.unknownMembers);
|
unknownMembers.push(...parserResult.unknownMembers);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
const asyncJobs = [];
|
||||||
parsedData,
|
|
||||||
parserErrors,
|
|
||||||
unknownMembers
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchAllBookings()
|
fetchAllBookings()
|
||||||
.then((bookingEntries) => {
|
.then((bookingEntries) => {
|
||||||
bookingEntries.forEach((bookingEntry) => writeBookingReservation(bookingEntry));
|
bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry)));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log('===> ERROR');
|
res.status(500).send(error);
|
||||||
console.log(error);
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
parsedData.forEach((entry) => {
|
parsedData.forEach((entry) => asyncJobs.push(writeDoorLockEvent(entry)));
|
||||||
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(() => {
|
.catch((error) => {
|
||||||
res.status(500).send(officeRnDAPIErrors.FAILED_TO_FETCH_MEMBERS);
|
res.status(500).send(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
form.parse(req);
|
form.parse(req);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
37
controllers/integration.js
Normal file
37
controllers/integration.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -1,2 +1,18 @@
|
|||||||
BASIC_AUTH_USERNAME=username
|
BASIC_AUTH_USERNAME=username
|
||||||
BASIC_AUTH_PASSWORD=password
|
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
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface, Sequelize) => {
|
||||||
|
return queryInterface.renameTable('doorLockIncidents', 'unscheduledIncidents');
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface, Sequelize) => {
|
||||||
|
return queryInterface.renameTable('unscheduledIncidents', 'doorLockIncidents');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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'
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
35
migrations/20190612125150-add-unlocked-incidents-table.js
Normal file
35
migrations/20190612125150-add-unlocked-incidents-table.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,9 +4,13 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
const bookingReservation = sequelize.define('bookingReservation', {
|
const bookingReservation = sequelize.define('bookingReservation', {
|
||||||
reservationId: DataTypes.TEXT,
|
reservationId: DataTypes.TEXT,
|
||||||
memberId: DataTypes.TEXT,
|
memberId: DataTypes.TEXT,
|
||||||
resource: DataTypes.TEXT,
|
officeId: DataTypes.TEXT,
|
||||||
|
resourceId: DataTypes.TEXT,
|
||||||
start: DataTypes.DATE,
|
start: DataTypes.DATE,
|
||||||
end: DataTypes.DATE,
|
end: DataTypes.DATE,
|
||||||
|
timezone: DataTypes.TEXT,
|
||||||
|
canceled: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
}, {});
|
}, {});
|
||||||
bookingReservation.associate = function(models) {
|
bookingReservation.associate = function(models) {
|
||||||
// associations can be defined here
|
// associations can be defined here
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { USER_LOCKED_DOOR, USER_UNLOCKED_DOOR } = require('../constants/constants');
|
const { doorLockEvents } = require('../constants/constants');
|
||||||
|
|
||||||
module.exports = (sequelize, DataTypes) => {
|
module.exports = (sequelize, DataTypes) => {
|
||||||
const doorLockEvent = sequelize.define('doorLockEvent', {
|
const doorLockEvent = sequelize.define('doorLockEvent', {
|
||||||
memberName: DataTypes.TEXT,
|
memberName: DataTypes.TEXT,
|
||||||
memberNumber: DataTypes.INTEGER,
|
memberNumber: DataTypes.INTEGER,
|
||||||
memberId: DataTypes.TEXT,
|
memberId: DataTypes.TEXT,
|
||||||
|
resourceId: DataTypes.TEXT,
|
||||||
event: {
|
event: {
|
||||||
type: DataTypes.ENUM,
|
type: DataTypes.ENUM,
|
||||||
values: [USER_LOCKED_DOOR, USER_UNLOCKED_DOOR]
|
values: [doorLockEvents.USER_LOCKED, doorLockEvents.USER_UNLOCKED]
|
||||||
},
|
},
|
||||||
timestamp: DataTypes.DATE,
|
timestamp: DataTypes.DATE,
|
||||||
}, {});
|
}, {});
|
||||||
|
|||||||
14
models/officeResourceMapping.js
Normal file
14
models/officeResourceMapping.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
29
models/unlockedIncident.js
Normal file
29
models/unlockedIncident.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
25
models/unscheduledIncident.js
Normal file
25
models/unscheduledIncident.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -120,12 +120,19 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
|
||||||
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
|
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.3.0",
|
"follow-redirects": "1.5.10",
|
||||||
"is-buffer": "^1.1.5"
|
"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": {
|
"babel-runtime": {
|
||||||
@@ -1128,20 +1135,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.7.0",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||||
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
|
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "^3.2.6"
|
"debug": "=3.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "3.2.6",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
"requires": {
|
"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": {
|
"is-buffer": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
"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": {
|
"is-ci": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"install-server": "npm install",
|
"install-server": "npm install",
|
||||||
"install-client": "cd client && yarn install",
|
"install-client": "cd client && yarn install",
|
||||||
"docker-build": "docker build -t simaspace .",
|
"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",
|
"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",
|
"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",
|
"migrate": "npx sequelize db:migrate",
|
||||||
@@ -21,13 +21,14 @@
|
|||||||
"node": "11.12.x"
|
"node": "11.12.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.19.0",
|
||||||
"csv-parser": "^2.3.0",
|
"csv-parser": "^2.3.0",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"express": "^4.17.0",
|
"express": "^4.17.0",
|
||||||
"express-basic-auth": "^1.2.0",
|
"express-basic-auth": "^1.2.0",
|
||||||
"formidable": "^1.2.1",
|
"formidable": "^1.2.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"moment-timezone": "^0.5.25",
|
||||||
"pg": "^7.11.0",
|
"pg": "^7.11.0",
|
||||||
"sequelize": "^5.8.6",
|
"sequelize": "^5.8.6",
|
||||||
"sequelize-cli": "^5.4.0"
|
"sequelize-cli": "^5.4.0"
|
||||||
|
|||||||
@@ -2,11 +2,19 @@
|
|||||||
|
|
||||||
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
|
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
|
||||||
const { uploadDoorLockData } = require('../controllers/doorLock');
|
const { uploadDoorLockData } = require('../controllers/doorLock');
|
||||||
|
const { getKnownOfficeResourceMappings, addNewMapping } = require('../controllers/integration');
|
||||||
|
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', apiStatusCheck);
|
router.get('/', apiStatusCheck);
|
||||||
|
|
||||||
router.post('/doorLock/upload', uploadDoorLockData);
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const app = express();
|
|||||||
const port = process.env.PORT || 5000;
|
const port = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
app.use('/api', routes);
|
app.use('/api', routes);
|
||||||
|
|
||||||
app.use(basicAuth({
|
app.use(basicAuth({
|
||||||
|
|||||||
230
services/doorLock/doorLock.js
Normal file
230
services/doorLock/doorLock.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const db = require('../../models');
|
||||||
|
const fs = require('fs');
|
||||||
|
const csv = require('csv-parser');
|
||||||
|
const moment = require('moment/moment');
|
||||||
|
const Op = require('sequelize').Op;
|
||||||
|
|
||||||
|
const {
|
||||||
|
USER_ENTRY_EVENT,
|
||||||
|
ENABLE_PASSAGE_MODE,
|
||||||
|
DISABLE_PASSAGE_MODE,
|
||||||
|
VALID_CSV_HEADERS,
|
||||||
|
doorLockEvents,
|
||||||
|
csvParserErrors,
|
||||||
|
} = require('../../constants/constants');
|
||||||
|
|
||||||
|
const { fetchAllMembers, findMember } = require('../officeRnD/members');
|
||||||
|
const { getMappingsFromDatabase } = require('../officeRnD/resources');
|
||||||
|
|
||||||
|
const extractMappingFromFileName = (fileName) => {
|
||||||
|
const contentBetweenBracketsRegex = /\[(.*?)\]/;
|
||||||
|
const rawContent = fileName.match(contentBetweenBracketsRegex)[1];
|
||||||
|
const mappingContent = rawContent.split('-').map(word => word.trim());
|
||||||
|
return {
|
||||||
|
officeSlug: mappingContent[0],
|
||||||
|
resourceSlug: mappingContent[1],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfMappingExsists = (mappingFromFileName, mappings) => {
|
||||||
|
const { officeSlug, resourceSlug } = mappingFromFileName;
|
||||||
|
|
||||||
|
return mappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug));
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseDoorLockDataFile = (file) => {
|
||||||
|
return new Promise ((resolve, reject) => {
|
||||||
|
const results = [];
|
||||||
|
const errors = [];
|
||||||
|
const unknownMembers = [];
|
||||||
|
let isValidFile = true;
|
||||||
|
|
||||||
|
const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()];
|
||||||
|
|
||||||
|
Promise.all(prefetchDataJobs)
|
||||||
|
.then(result => {
|
||||||
|
const mappings = result[0];
|
||||||
|
|
||||||
|
const mappingFromFileName = extractMappingFromFileName(file.name);
|
||||||
|
const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings);
|
||||||
|
if (!mappingObject){
|
||||||
|
reject('Error ! File contains unknown location');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.createReadStream(file.path)
|
||||||
|
.pipe(csv({
|
||||||
|
mapHeaders: ({ header, index }) => header.trim().toLowerCase(),
|
||||||
|
mapValues: ({ header, index, value }) => value.trim()
|
||||||
|
}))
|
||||||
|
.on('headers', (headers) => {
|
||||||
|
|
||||||
|
const sortedValidHeadersArray = VALID_CSV_HEADERS.concat().sort();
|
||||||
|
const sortedParsedHeadersArray = headers.map(header => header.trim()).sort();
|
||||||
|
|
||||||
|
let validHeaders = true;
|
||||||
|
if (sortedParsedHeadersArray.length !== sortedValidHeadersArray.length) {
|
||||||
|
validHeaders = false;
|
||||||
|
}else {
|
||||||
|
for (let i = 0; i < sortedValidHeadersArray.length; i++){
|
||||||
|
validHeaders = validHeaders
|
||||||
|
&& (sortedValidHeadersArray[i] === sortedParsedHeadersArray[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validHeaders){
|
||||||
|
isValidFile = false;
|
||||||
|
errors.push({
|
||||||
|
error: csvParserErrors.INVALID_HEADERS,
|
||||||
|
details: `Expected headers : ${JSON.stringify(VALID_CSV_HEADERS)}`,
|
||||||
|
file: file.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('data', (data) => {
|
||||||
|
if (!isValidFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = data.event.trim();
|
||||||
|
if ([USER_ENTRY_EVENT, ENABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE].includes(eventType)){
|
||||||
|
results.push(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
const parsedData = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < results.length){
|
||||||
|
//Verify pair
|
||||||
|
//First entry type should be user entry and second should be enable / disable passage
|
||||||
|
const firstEntry = results[i];
|
||||||
|
const secondEntry = results[i+1];
|
||||||
|
|
||||||
|
if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){
|
||||||
|
const memberObject = findMember(firstEntry.name);
|
||||||
|
|
||||||
|
if (!memberObject){
|
||||||
|
//Check if member is already labeled as unknown
|
||||||
|
const unknownMember = unknownMembers.find((member) => member.details === firstEntry.name);
|
||||||
|
if (!unknownMember){
|
||||||
|
unknownMembers.push({
|
||||||
|
error: csvParserErrors.UNKNOWN_MEMBER,
|
||||||
|
details: firstEntry.name,
|
||||||
|
file: file.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondEntry && (secondEntry.event === ENABLE_PASSAGE_MODE || secondEntry.event === DISABLE_PASSAGE_MODE)){
|
||||||
|
const event = (secondEntry.event === ENABLE_PASSAGE_MODE) ?
|
||||||
|
doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED;
|
||||||
|
|
||||||
|
const dateTimeString = `${firstEntry.date} ${firstEntry.time}`;
|
||||||
|
const timestamp = moment.utc(dateTimeString, 'MM/DD/YY HH:mm:ss A').toISOString();
|
||||||
|
|
||||||
|
//Verify that member is registered in OfficeRnD system
|
||||||
|
if (memberObject){
|
||||||
|
const entryData = {
|
||||||
|
memberName: firstEntry.name,
|
||||||
|
memberNumber: firstEntry['user no'],
|
||||||
|
memberId: memberObject.memberId,
|
||||||
|
timestamp,
|
||||||
|
event,
|
||||||
|
resourceId: mappingObject.resourceId,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
parsedData.push(entryData);
|
||||||
|
}
|
||||||
|
i+=2;
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE,
|
||||||
|
details: firstEntry,
|
||||||
|
file: file.name,
|
||||||
|
});
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
error: csvParserErrors.INVALID_ENTRY_EXPECTED_USER,
|
||||||
|
details: firstEntry,
|
||||||
|
file: file.name,
|
||||||
|
});
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
parsedData,
|
||||||
|
unknownMembers,
|
||||||
|
errors
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeDoorLockEvent = (entry) => {
|
||||||
|
return db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUnlockEntryForReservation = (reservation) => {
|
||||||
|
const { start, end, memberId, resourceId } = reservation;
|
||||||
|
|
||||||
|
const attributes = ['memberName', 'event', 'timestamp'];
|
||||||
|
const earliestUnlock = parseInt(process.env.EARLIEST_UNLOCK) || 0;
|
||||||
|
const fromTimestamp = moment(start).subtract(earliestUnlock).toISOString();
|
||||||
|
const toTimestamp = end;
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
memberId,
|
||||||
|
timestamp: {
|
||||||
|
[Op.and]: [
|
||||||
|
{[Op.gt]: fromTimestamp},
|
||||||
|
{[Op.lte]: toTimestamp}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
event: doorLockEvents.USER_UNLOCKED,
|
||||||
|
resourceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return db.doorLockEvent.findOne({
|
||||||
|
attributes,
|
||||||
|
where: filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelatedDoorLockEntries = (fromTimestamp, toTimestamp, memberId, resourceId) => {
|
||||||
|
const attributes = ['memberName', 'event', 'timestamp'];
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
memberId,
|
||||||
|
timestamp: {
|
||||||
|
[Op.and]: [
|
||||||
|
{[Op.gt]: fromTimestamp},
|
||||||
|
{[Op.lte]: toTimestamp}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
event: doorLockEvents.USER_LOCKED,
|
||||||
|
resourceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return db.doorLockEvent.findOne({
|
||||||
|
attributes,
|
||||||
|
where: filters
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parseDoorLockDataFile,
|
||||||
|
writeDoorLockEvent,
|
||||||
|
getRelatedDoorLockEntries,
|
||||||
|
getUnlockEntryForReservation,
|
||||||
|
};
|
||||||
502
services/integration/doorLockCharges.js
Normal file
502
services/integration/doorLockCharges.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const db = require('../../models/index');
|
const db = require('../../models/index');
|
||||||
|
const moment = require('moment-timezone');
|
||||||
|
const Op = require('sequelize').Op;
|
||||||
|
|
||||||
const { API } = require('../../helpers/api');
|
const { API } = require('../../helpers/api');
|
||||||
|
const { officeRnDAPIErrors } = require('../../constants/constants');
|
||||||
|
|
||||||
const fetchAllBookings = () => {
|
const fetchAllBookings = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -15,26 +18,118 @@ const fetchAllBookings = () => {
|
|||||||
cleanedBookingReservations.push({
|
cleanedBookingReservations.push({
|
||||||
reservationId: fullBookingEntry['_id'],
|
reservationId: fullBookingEntry['_id'],
|
||||||
memberId: fullBookingEntry.member,
|
memberId: fullBookingEntry.member,
|
||||||
resource: fullBookingEntry.resourceId,
|
officeId: fullBookingEntry.office,
|
||||||
|
resourceId: fullBookingEntry.resourceId,
|
||||||
start: fullBookingEntry.start.dateTime,
|
start: fullBookingEntry.start.dateTime,
|
||||||
end: fullBookingEntry.end.dateTime,
|
end: fullBookingEntry.end.dateTime,
|
||||||
|
timezone: fullBookingEntry.timezone,
|
||||||
|
canceled: fullBookingEntry.canceled || false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
resolve(cleanedBookingReservations);
|
resolve(cleanedBookingReservations);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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) => {
|
const writeBookingReservation = (bookingReservation) => {
|
||||||
db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}})
|
return db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}});
|
||||||
.then()
|
|
||||||
.catch();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchAllBookings,
|
fetchAllBookings,
|
||||||
writeBookingReservation,
|
writeBookingReservation,
|
||||||
|
getAllFinishedBookings,
|
||||||
|
getFirstNextBooking,
|
||||||
|
getFirstPreviousBooking,
|
||||||
};
|
};
|
||||||
|
|||||||
61
services/officeRnD/resources.js
Normal file
61
services/officeRnD/resources.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user