diff --git a/client/src/components/GenerateFeesInORDButton/index.js b/client/src/components/GenerateFeesInORDButton/index.js
new file mode 100644
index 0000000..43e6d0f
--- /dev/null
+++ b/client/src/components/GenerateFeesInORDButton/index.js
@@ -0,0 +1,53 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { Button, Modal } from 'semantic-ui-react';
+
+import { addFeesToOrd } from '../../store/actions';
+
+class GenerateFeesInORDButton extends Component {
+ state = { open: false };
+
+ show = size => () => this.setState({ size, open: true });
+ close = () => this.setState({ open: false });
+ confirm = () => {
+ const { addFeesToOrd, dateRange, memberIds } = this.props;
+
+ if (dateRange){
+ addFeesToOrd(dateRange, memberIds);
+ }
+
+ this.close();
+ };
+
+ render() {
+ const { open, size } = this.state;
+ const { singleMember, disabled } = this.props;
+
+ const modalContent = singleMember ?
+ 'This will remove all existing fees in ORD for selected member in selected date range and generate new fees based on shown incident tables. Do you want to continue ?':
+ 'This will remove all existing fees in ORD for all members in selected date range and generate new fees based on shown incident tables. Do you want to continue ?';
+
+ return (
+
+
Generate fees in ORD
+
+
+ Add fees to the ORD
+
+ {modalContent}
+
+
+ No
+
+
+
+
+ );
+ }
+}
+
+const mapDispatchToProps = (dispatch) => ({
+ addFeesToOrd: (dateRange, memberIds) => addFeesToOrd(dispatch, dateRange, memberIds),
+});
+
+export default connect(null, mapDispatchToProps)(GenerateFeesInORDButton);
diff --git a/client/src/components/MemberIncidentsTable/index.js b/client/src/components/MemberIncidentsTable/index.js
deleted file mode 100644
index 6f79f59..0000000
--- a/client/src/components/MemberIncidentsTable/index.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import React from 'react';
-import { Loader } from 'semantic-ui-react';
-import ReactTable from 'react-table';
-import 'react-table/react-table.css';
-import { NavLink } from 'react-router-dom';
-
-import {incidentsReportHeaderTitles} from '../../constants/menuItems';
-import {
- incidentDescriptions,
- incidentLevelDescriptions,
- UNLOCKED_INCIDENT,
- UNSCHEDULED_INCIDENT
-} from '../../constants/enums';
-
-
-const MemberIncidentsTable = props => {
- const { loading, title, openMemberSummaryOnMemberClick } = props;
- const incidents = props.incidents ? props.incidents : [];
-
- const columns = [];
- if (incidents && incidents.length > 0){
- const incidentHeaders = Object.keys(incidentsReportHeaderTitles);
-
- incidentHeaders.forEach((header) => {
- const columnTitle = incidentsReportHeaderTitles[header];
-
- if (columnTitle){
- const columnAlignments = {
- left: 'left',
- right: 'right',
- };
- let columnContentsAlignment = columnAlignments.left;
-
- columns.push({
- Header: incidentsReportHeaderTitles[header],
- accessor: header,
- Cell: props => {
- let cellValue = '';
- let urlValue = undefined;
-
- switch (props.column.id) {
- case 'memberName':
- const memberId = props.row['_original'].memberId;
- urlValue = `/practice-summary-report/${memberId}`;
- cellValue = props.value;
- break;
- case 'incidentType':
- cellValue = incidentDescriptions[props.value];
- break;
- case 'incidentLevel':
- cellValue = incidentLevelDescriptions[props.value];
- break;
- case 'feeDescription':
- const { incidentType, incidentLevel, timeIntervalsToCharge } = props.row['_original'];
-
- switch (incidentType) {
- case UNLOCKED_INCIDENT:
- cellValue = `${incidentLevelDescriptions[incidentLevel]}`;
- break;
- case UNSCHEDULED_INCIDENT:
- cellValue = `${timeIntervalsToCharge} x 5 min`;
- break;
- default:
- cellValue = '';
- break;
- }
- break;
- case 'totalChargeFee':
- const totalFee = props.value ? props.value : props.row['_original'].incidentPrice;
- const totalFeeFormatted = parseFloat(totalFee).toFixed(2);
- cellValue = `$ ${totalFeeFormatted}`;
- columnContentsAlignment = columnAlignments.right;
- break;
- default:
- cellValue = props.value;
- }
-
- if (openMemberSummaryOnMemberClick && urlValue){
- return {cellValue}
- }else{
- return {cellValue}
- }
-
- // return
- // {cellValue}
- //
-
- // return
- }
- });
- }
- });
- }
-
- return (
-
-
{title}
-
- {
- !loading && incidents &&
-
- }
-
- );
-};
-
-export default MemberIncidentsTable;
diff --git a/client/src/scenes/IncidentsReport/index.js b/client/src/scenes/IncidentsReport/index.js
index 3c4f766..40dad4b 100644
--- a/client/src/scenes/IncidentsReport/index.js
+++ b/client/src/scenes/IncidentsReport/index.js
@@ -5,26 +5,50 @@ import { Container } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import DateRangePicker from '../../components/DateRangePicker';
import MemberIncidentsTables from '../../components/MemberIncidentsTables';
+import GenerateFeesInORDButton from '../../components/GenerateFeesInORDButton';
-import { fetchIncidents } from '../../store/actions';
+import { fetchIncidents, addFeesToOrd } from '../../store/actions';
class IncidentsReport extends Component {
- onDatesUpdate(dateRange) {
+ state = {dateRange: null};
+
+ onDatesUpdate = (dateRange) => {
const { fetchIncidents } = this.props;
+
+ this.setState({dateRange});
fetchIncidents(dateRange);
- }
+ };
render () {
- const { pendingIncidents, incidents } = this.props;
+ const { pendingIncidents, incidents, pendingAddFeesStatus } = this.props;
+ const { dateRange } = this.state;
+
+ const loading = pendingIncidents || pendingAddFeesStatus;
+
+ const membersMap = {};
+ if (incidents && Array.isArray(incidents)) {
+ incidents.forEach((incident) => {
+ membersMap[incident.memberId] = true;
+ });
+ }
+ const memberIds = Object.keys(membersMap) || [];
return (
Incidents Report
-
+
-
+
+
+
+
+
);
}
@@ -33,10 +57,12 @@ class IncidentsReport extends Component {
const mapStateToProps = (state) => ({
pendingIncidents: state.incidentsReport.pending,
incidents: state.incidentsReport.result,
+ pendingAddFeesStatus: state.addFeesStatus.pending,
});
const mapDispatchToProps = (dispatch) => ({
fetchIncidents: (dateRange) => fetchIncidents(dispatch, dateRange),
+ addFeesToOrd: (dateRange, memberIds) => addFeesToOrd(dispatch, dateRange, memberIds),
});
export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport);
diff --git a/client/src/scenes/PracticeSummaryReport/index.js b/client/src/scenes/PracticeSummaryReport/index.js
index e9a062b..b0df6b8 100644
--- a/client/src/scenes/PracticeSummaryReport/index.js
+++ b/client/src/scenes/PracticeSummaryReport/index.js
@@ -1,12 +1,13 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
-import {Container, Grid} from 'semantic-ui-react';
+import { Container, Grid } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import DateRangePicker from '../../components/DateRangePicker';
import MemberSelector from './components/MemberSelector';
import MemberSummary from './components/MemberSummary';
import MemberIncidentsTables from '../../components/MemberIncidentsTables';
+import GenerateFeesInORDButton from '../../components/GenerateFeesInORDButton';
import { fetchMemberIncidents } from '../../store/actions';
@@ -39,8 +40,12 @@ class PracticeSummaryReport extends Component {
}
render () {
- const { memberIncidents, loading } = this.props;
- const { memberId } = this.state;
+ const { memberIncidents, loadingMemberIncidents, loadingAddFeesStatus } = this.props;
+ const { memberId, dateRange } = this.state;
+
+ const loading = loadingAddFeesStatus || loadingMemberIncidents;
+
+ const addFeesButtonDisabled = !memberId || !dateRange || loading;
return (
@@ -64,6 +69,16 @@ class PracticeSummaryReport extends Component {
/>
+
+
+
+
+
@@ -78,7 +93,8 @@ class PracticeSummaryReport extends Component {
const mapStateToProps = (state) => ({
memberIncidents: state.memberIncidents.result,
- loading: state.memberIncidents.pending,
+ loadingMemberIncidents: state.memberIncidents.pending,
+ loadingAddFeesStatus: state.addFeesStatus.pending,
});
const mapDispatchToProps = (dispatch) => ({
diff --git a/client/src/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js
index 5e9a127..79a9068 100644
--- a/client/src/store/actions/integrationActions.js
+++ b/client/src/store/actions/integrationActions.js
@@ -14,6 +14,9 @@ import {
FETCH_MEMBER_INCIDENTS_PENDING,
FETCH_MEMBER_INCIDENTS_SUCCESS,
FETCH_MEMBER_INCIDENTS_FAILED,
+ ADD_FEES_TO_ORD_PENDING,
+ ADD_FEES_TO_ORD_SUCCESS,
+ ADD_FEES_TO_ORD_FAILED,
} from '../constants';
import API from '../../utilities/api';
@@ -78,3 +81,17 @@ export const fetchMemberIncidents = (dispatch, memberId, dateRange) => {
dispatch({type: FETCH_MEMBER_INCIDENTS_FAILED, payload: error.response});
});
};
+
+export const addFeesToOrd = (dispatch, dateRange, memberIds) => {
+ dispatch({type: ADD_FEES_TO_ORD_PENDING});
+ API.post(`integration/addFees`, {
+ dateRange,
+ memberIds: memberIds || [],
+ })
+ .then(response => {
+ dispatch({type: ADD_FEES_TO_ORD_SUCCESS, payload: response.data});
+ })
+ .catch(error => {
+ dispatch({type: ADD_FEES_TO_ORD_FAILED, payload: error.response});
+ });
+};
diff --git a/client/src/store/constants.js b/client/src/store/constants.js
index 6dd4fd8..6d67aef 100644
--- a/client/src/store/constants.js
+++ b/client/src/store/constants.js
@@ -21,3 +21,7 @@ export const FETCH_MEMBERS_FAILED = 'FETCH_MEMBERS_FAILED';
export const FETCH_MEMBER_INCIDENTS_PENDING = 'FETCH_MEMBER_INCIDENTS_PENDING';
export const FETCH_MEMBER_INCIDENTS_SUCCESS = 'FETCH_MEMBER_INCIDENTS_SUCCESS';
export const FETCH_MEMBER_INCIDENTS_FAILED = 'FETCH_MEMBER_INCIDENTS_FAILED';
+
+export const ADD_FEES_TO_ORD_PENDING = 'ADD_FEES_TO_ORD_PENDING';
+export const ADD_FEES_TO_ORD_SUCCESS = 'ADD_FEES_TO_ORD_SUCCESS';
+export const ADD_FEES_TO_ORD_FAILED = 'ADD_FEES_TO_ORD_FAILED';
diff --git a/client/src/store/reducers/addFeesToOrdReducer.js b/client/src/store/reducers/addFeesToOrdReducer.js
new file mode 100644
index 0000000..402cdca
--- /dev/null
+++ b/client/src/store/reducers/addFeesToOrdReducer.js
@@ -0,0 +1,38 @@
+import {
+ ADD_FEES_TO_ORD_PENDING,
+ ADD_FEES_TO_ORD_SUCCESS,
+ ADD_FEES_TO_ORD_FAILED,
+} from '../constants';
+
+const initialState = {
+ pending: false,
+ result: null,
+ error: null,
+};
+
+export const addFeesStatus = (state, action) => {
+ state = state || initialState;
+ action = action || {};
+
+ switch(action.type){
+ case ADD_FEES_TO_ORD_PENDING:
+ return Object.assign({}, state, {
+ pending: true,
+ error: null,
+ });
+ case ADD_FEES_TO_ORD_SUCCESS:
+ return Object.assign({}, state, {
+ pending: false,
+ result: action.payload,
+ error: null,
+ });
+ case ADD_FEES_TO_ORD_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 869300c..3eb9e9f 100644
--- a/client/src/store/reducers/index.js
+++ b/client/src/store/reducers/index.js
@@ -6,6 +6,7 @@ import { addMapping } from './addMappingReducer';
import { incidentsReport } from './incidentsReportReducer';
import { membersList } from './membersListReducer';
import { memberIncidents} from './memberIncidentsReducer';
+import { addFeesStatus } from './addFeesToOrdReducer';
export const rootReducer = combineReducers({
doorLockData,
@@ -14,5 +15,6 @@ export const rootReducer = combineReducers({
incidentsReport,
membersList,
memberIncidents,
+ addFeesStatus,
});
diff --git a/config/config.js b/config/config.js
index 3bb20d6..ea66105 100644
--- a/config/config.js
+++ b/config/config.js
@@ -1,3 +1,9 @@
+const pool = {
+ max: parseInt(process.env.DB_POOL_MAX_CONNECTIONS) || 5,
+ acquire: parseInt(process.env.DB_POOL_ACQUIRE) || 60000,
+ evict: parseInt(process.env.DB_POOL_EVICT) || 1000,
+};
+
module.exports = {
development: {
username: 'docker',
@@ -5,14 +11,17 @@ module.exports = {
database: 'CrmIntegration',
port: '5431',
dialect: 'postgres',
- logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false
+ logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false,
+ pool
},
test: {
"use_env_variable": 'DATABASE_URL',
- logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false
+ logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false,
+ pool
},
production: {
"use_env_variable": 'DATABASE_URL',
- logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false
+ logging: parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false,
+ pool
}
};
diff --git a/config/config.json b/config/config.json
deleted file mode 100644
index bb7a042..0000000
--- a/config/config.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "development": {
- "username": "docker",
- "password": "docker",
- "database": "CrmIntegration",
- "port": "5431",
- "dialect": "postgres",
- "logging": false
- },
- "test": {
- "use_env_variable": "DATABASE_URL"
- },
- "production": {
- "use_env_variable": "DATABASE_URL"
- }
-}
diff --git a/constants/constants.js b/constants/constants.js
index d339663..1d06ee2 100644
--- a/constants/constants.js
+++ b/constants/constants.js
@@ -13,32 +13,38 @@ const unlockedIncidentLevelsPrices = {
UNLOCKED_0: {
id: 0,
title: 'UNLOCKED_0',
- price: parseInt(process.env.UNLOCK_0) || 0
+ price: parseInt(process.env.UNLOCK_0) || 0,
+ description: 'First month - warning',
},
UNLOCKED_1: {
id: 1,
title: 'UNLOCKED_1',
- price: parseInt(process.env.UNLOCK_1) || 10
+ price: parseInt(process.env.UNLOCK_1) || 10,
+ description: 'Second month',
},
UNLOCKED_2: {
id: 2,
title: 'UNLOCKED_2',
- price: parseInt(process.env.UNLOCK_2) || 20
+ price: parseInt(process.env.UNLOCK_2) || 20,
+ description: 'Third month',
},
UNLOCKED_3: {
id: 3,
title: 'UNLOCKED_3',
- price: parseInt(process.env.UNLOCK_3) || 30
+ price: parseInt(process.env.UNLOCK_3) || 30,
+ description: 'Fourth month',
},
UNLOCKED_4: {
id: 4,
title: 'UNLOCKED_4',
- price: parseInt(process.env.UNLOCK_4) || 40
+ price: parseInt(process.env.UNLOCK_4) || 40,
+ description: 'Fifth month',
},
UNLOCKED_5: {
id: 5,
title: 'UNLOCKED_5',
- price: parseInt(process.env.UNLOCK_5) || 50
+ price: parseInt(process.env.UNLOCK_5) || 50,
+ description: 'Sixth month and onward',
}
};
const csvParserErrors = {
@@ -50,9 +56,13 @@ const csvParserErrors = {
};
const officeRnDAPIErrors = {
FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members',
- FAILED_TO_FETCH_BOOKINGS: 'Failed to fetch booking reservations'
+ FAILED_TO_FETCH_BOOKINGS: 'Failed to fetch booking reservations',
+ FAILED_TO_FETCH_FEES: 'Failed to fetch existing fees',
+ FAILED_TO_DELETE_FEES: 'Failed to delete fees in ORD',
+ FAILED_TO_ADD_FEES: 'Failed to add fees in ORD',
};
const integrationServiceErrors = {
+ IMPORT_SUCCESSFUL_CALCULATION_FAILED: 'Import succeeded but fetching reservations and calculating charges failed',
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',
@@ -71,6 +81,16 @@ const incidentType = {
BOOKING_CANCELED_LATE: 9,
};
+const incidentTypeExplanations = {};
+incidentTypeExplanations[incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION] = 'Door left unlocked';
+incidentTypeExplanations[incidentType.UNLOCKED_INCIDENT_STANDALONE] = 'Door left unlocked';
+incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION] = 'Room used before reservation started';
+incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION] = 'Room used after reservation started';
+incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_STANDALONE] = 'Room used without reservation';
+incidentTypeExplanations[incidentType.BOOKING_MOVED_TO_ANOTHER_DAY] = 'Reservation moved to another day';
+incidentTypeExplanations[incidentType.BOOKING_SHORTENED] = 'Reservation shortened';
+incidentTypeExplanations[incidentType.BOOKING_CANCELED_LATE] = 'A reservation canceled after the grace period';
+
const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles';
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
@@ -94,6 +114,7 @@ module.exports = {
unlockedIncidentLevelsPrices,
integrationServiceErrors,
incidentType,
+ incidentTypeExplanations,
UI_TIMEZONE,
DEFAULT_DATE_FORMAT,
MAX_BACK_TO_BACK_DIFFERENCE,
diff --git a/controllers/doorLock.js b/controllers/doorLock.js
index c34248a..a75cf2c 100644
--- a/controllers/doorLock.js
+++ b/controllers/doorLock.js
@@ -4,6 +4,7 @@ const { parseDoorLockDataFile, writeDoorLockEvent } = require('../services/doorL
const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
const { integrationServiceErrors } = require('../constants/constants');
+const { checkBookingChanges } = require('../services/integration/checkBookingChange');
const IncomingForm = require('formidable').IncomingForm;
@@ -42,11 +43,25 @@ const uploadDoorLockData = (req, res) => {
Promise.all(asyncWriteJobs)
.then(() => {
- res.json({
- parsedData,
- parserErrors,
- unknownMembers
- });
+ checkBookingChanges()
+ .then(() => {
+ calculateDoorLockCharges()
+ .then(() => {
+ res.json({
+ parsedData,
+ parserErrors,
+ unknownMembers
+ });
+ })
+ .catch((error) => {
+ console.log('Error : ', error);
+ res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED);
+ });
+ })
+ .catch((error) => {
+ console.log('Error : ', error);
+ res.status(500).send(integrationServiceErrors.IMPORT_SUCCESSFUL_CALCULATION_FAILED);
+ });
})
.catch((error) => {
console.log(integrationServiceErrors.FAILED_TO_SAVE_DOOR_LOCK_ENTRIES);
@@ -54,7 +69,7 @@ const uploadDoorLockData = (req, res) => {
res.status(500).send(integrationServiceErrors.FAILED_TO_SAVE_DATA_GENERIC);
});
- fetchAllBookings()
+ /*fetchAllBookings()
.then((bookingEntries) => {
const asyncJobs = [];
bookingEntries.forEach((bookingEntry) => asyncJobs.push(writeBookingReservation(bookingEntry)));
@@ -62,13 +77,18 @@ const uploadDoorLockData = (req, res) => {
Promise.all(asyncJobs)
.then(() => {
calculateDoorLockCharges();
- });
+ })
+ .catch((error) => {
+ console.log('Error updating booking reservations : ');
+ console.log(error);
+ })
})
.catch((error) => {
console.log(integrationServiceErrors.FAILED_TO_SAVE_BOOKINGS);
console.log(error);
return;
});
+ */
})
.catch((error) => {
res.status(500).send(error);
diff --git a/controllers/integration.js b/controllers/integration.js
index 235a774..ffe2ef0 100644
--- a/controllers/integration.js
+++ b/controllers/integration.js
@@ -2,6 +2,9 @@
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
const { getAllIncidents } = require('../services/integration/reports');
+const { getMembersFeesForDateRange } = require('../services/integration/invoiceIntegration');
+const { deleteFeesFromORD, addFeesToORD } = require('../services/officeRnD/fees');
+const { checkBookingChanges } = require('../services/integration/checkBookingChange');
const getKnownOfficeResourceMappings = (req, res) => {
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
@@ -38,7 +41,7 @@ const getAllIncidentsController = (req, res) => {
endDate: req.params.endDate,
};
- getAllIncidents(dateRange)
+ getAllIncidents(dateRange, [])
.then((incidents) => {
res.send(incidents);
})
@@ -55,7 +58,7 @@ const getMemberIncidents = (req, res) => {
endDate: req.params.endDate,
};
- getAllIncidents(dateRange, memberId)
+ getAllIncidents(dateRange, [memberId])
.then((incidents) => {
res.send(incidents);
})
@@ -65,9 +68,53 @@ const getMemberIncidents = (req, res) => {
});
};
+const addFees = (req, res) => {
+ const memberIds = req.body && req.body.memberIds ? req.body.memberIds : [];
+ const dateRange = req.body && req.body.dateRange ? req.body.dateRange : {startDate: null, endDate: null};
+ const { startDate, endDate } = dateRange;
+
+ if (startDate && endDate && Array.isArray(memberIds)){
+ checkBookingChanges()
+ .then(() => {
+ deleteFeesFromORD(dateRange, memberIds)
+ .then(() => {
+ // TODO: Change this to parallel execution
+ getMembersFeesForDateRange(dateRange, memberIds)
+ .then((allFees) => {
+ addFeesToORD(allFees)
+ .then((numberOfInsertedFees) => {
+ res.send({
+ status: 'ok',
+ numberOfInsertedFees
+ });
+ })
+ .catch((error) => {
+ console.log('Error adding fees');
+ res.status(500).send(error);
+ })
+ })
+ .catch((error) => {
+ console.log(error);
+ res.status(500).send(error);
+ })
+ })
+ .catch((error) => {
+ res.status(500).send(error);
+ });
+ })
+ .catch((error) => {
+ console.log('Error with updating booking reservations');
+ res.status(500).send(error);
+ })
+ }else{
+ res.status(400).send('Date range is missing');
+ }
+};
+
module.exports = {
getKnownOfficeResourceMappings,
addNewMapping,
getAllIncidentsController,
getMemberIncidents,
+ addFees,
};
diff --git a/cronServices/checkBookingChanges.js b/cronServices/checkBookingChanges.js
index aeae234..a4a2e37 100644
--- a/cronServices/checkBookingChanges.js
+++ b/cronServices/checkBookingChanges.js
@@ -1,41 +1,6 @@
'use strict';
require('dotenv').config();
-const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../services/officeRnD/bookings');
+const { checkBookingChanges } = require('../services/integration/checkBookingChange');
-const { chargeBookingChanges } = require('../services/integration/bookingChangeCharges');
-const { bulkWriteChanges } = require('../services/integration/bookingChangeLog');
-
-const checkBookingChanges = () => {
- fetchAllBookings()
- .then((reservations) => {
- bulkWriteReservationsWithChangesTracking(reservations)
- .then((changes) => {
- bulkWriteChanges(changes)
- .then(() => {
- chargeBookingChanges(changes)
- .then(() => {
- process.exit();
- })
- .catch((error) => {
- console.log('Error creating charges ', error);
- process.exit();
- });
- })
- .catch((error) => {
- console.log('Error bulk write booking reservation change log :', error);
- process.exit();
- });
- })
- .catch((error) => {
- console.log('Error bulk write booking reservations :', error);
- process.exit();
- });
- })
- .catch((error) => {
- console.log('Error fetching bookings from ORD ', error);
- process.exit();
- });
-};
-
-checkBookingChanges();
+checkBookingChanges().then(() => process.exit()).catch(() => process.exit());
diff --git a/environment.env b/environment.env
index b77df6a..74ad45d 100644
--- a/environment.env
+++ b/environment.env
@@ -24,3 +24,8 @@ BOOKING_CHANGE_PERCENTAGE_CHARGE=Percentage of hourly reate to apply for cancell
ALLOWED_BOOKING_CANCELLATION_TIME=Time from creation (in minutes) in which cancellation is not charged
SEQUELIZE_LOGGING=0 - false, 1 - true (console logging)
+
+#More about pool option : http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html
+DB_POOL_MAX_CONNECTIONS=Maximum number of connection in pool (ex. 18)
+DB_POOL_ACQUIRE=The maximum time, in milliseconds, that pool will try to get connection before throwing error (ex. 120000)
+DB_POOL_EVICT=The time interval, in milliseconds, after which sequelize-pool will remove idle connections. (ex. 10000)
diff --git a/routes/index.js b/routes/index.js
index 0a6f01a..11efed8 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -2,8 +2,14 @@
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
const { uploadDoorLockData } = require('../controllers/doorLock');
-const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidentsController, getMemberIncidents } = require('../controllers/integration');
const { fetchMembersList } = require('../controllers/officeRnD');
+const {
+ getKnownOfficeResourceMappings,
+ addNewMapping,
+ getAllIncidentsController,
+ getMemberIncidents,
+ addFees
+} = require('../controllers/integration');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
@@ -21,6 +27,8 @@ router.get('/integration/report/allIncidents/:startDate/:endDate', getAllInciden
router.get('/officeRnD/membersList', fetchMembersList);
+router.post('/integration/addFees', addFees);
+
// temporary route, manually trigger door lock charge calculations
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});
diff --git a/services/doorLock.js b/services/doorLock.js
deleted file mode 100644
index 497ae66..0000000
--- a/services/doorLock.js
+++ /dev/null
@@ -1,150 +0,0 @@
-'use strict';
-
-const db = require('../models/index');
-const fs = require('fs');
-const csv = require('csv-parser');
-const moment = require('moment');
-
-const {
- USER_ENTRY_EVENT,
- ENABLE_PASSAGE_MODE,
- DISABLE_PASSAGE_MODE,
- USER_UNLOCKED_DOOR,
- USER_LOCKED_DOOR,
- VALID_CSV_HEADERS,
- csvParserErrors,
-} = require('../constants/constants');
-
-const { fetchAllMembers, findMember } = require('../services/officeRnD/members');
-
-
-const parseDoorLockDataFile = (file) => {
- return new Promise ((resolve, reject) => {
- const results = [];
- const errors = [];
- const unknownMembers = [];
- let isValidFile = true;
-
- fetchAllMembers()
- .then(() => {
- 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) ? USER_UNLOCKED_DOOR : USER_LOCKED_DOOR;
- 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,
- };
-
- 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) => {
- db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}})
- .then()
- .catch();
-};
-
-module.exports = {
- parseDoorLockDataFile,
- writeDoorLockEvent,
-};
diff --git a/services/integration/bookingChangeCharges.js b/services/integration/bookingChangeCharges.js
index cbe26c4..1906707 100644
--- a/services/integration/bookingChangeCharges.js
+++ b/services/integration/bookingChangeCharges.js
@@ -97,7 +97,7 @@ const chargeBookingChanges = (changes) => {
incidents.push(incident);
}
}else{
- const differenceFromCreation = reservationCreationTimestamp.diff(moment.utc(), 'minutes');
+ const differenceFromCreation = moment.utc().diff(reservationCreationTimestamp, 'minutes');
if (differenceFromCreation > ALLOWED_BOOKING_CANCELLATION_TIME){
const chargeFee = 2 * reservationHourlyRate * oldReservationLength * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100;
diff --git a/services/integration/bookings.js b/services/integration/bookings.js
new file mode 100644
index 0000000..9e69131
--- /dev/null
+++ b/services/integration/bookings.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const Op = require('sequelize').Op;
+const db = require('../../models/index');
+const moment = require('moment-timezone');
+
+const { DEFAULT_DATE_FORMAT, UI_TIMEZONE } = require('../../constants/constants');
+
+const getActiveBookingsForMembersInDateRange = (dateRange, memberIds) => {
+ const startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).startOf('day');
+ const endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).endOf('day');
+
+ const attributes = [
+ 'id',
+ 'reservationId',
+ 'memberId',
+ 'officeId',
+ 'resourceId',
+ 'start',
+ 'end',
+ 'timezone',
+ 'canceled',
+ 'hourlyRate'
+ ];
+
+ const filters = {
+ canceled: false,
+ };
+
+ if (startDate && endDate) {
+ filters.start = {
+ [Op.gte]: startDate.toISOString()
+ };
+ filters.end = {
+ [Op.lte]: endDate.toISOString(),
+ };
+ }
+
+ if (memberIds.length > 0){
+ filters.memberId = {
+ [Op.in]: memberIds
+ };
+ }
+
+ return db.bookingReservation.findAll({
+ attributes,
+ where: filters,
+ });
+};
+
+module.exports = {
+ getActiveBookingsForMembersInDateRange,
+};
diff --git a/services/integration/checkBookingChange.js b/services/integration/checkBookingChange.js
new file mode 100644
index 0000000..3606c3e
--- /dev/null
+++ b/services/integration/checkBookingChange.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../officeRnD/bookings');
+
+const { chargeBookingChanges } = require('./bookingChangeCharges');
+const { bulkWriteChanges } = require('./bookingChangeLog');
+
+const checkBookingChanges = () => {
+ return new Promise((resolve, reject) => {
+ fetchAllBookings()
+ .then((reservations) => {
+ bulkWriteReservationsWithChangesTracking(reservations)
+ .then((changes) => {
+ bulkWriteChanges(changes)
+ .then(() => {
+ chargeBookingChanges(changes)
+ .then(() => {
+ resolve(true);
+ })
+ .catch((error) => {
+ console.log('Error creating charges ', error);
+ reject(error);
+ });
+ })
+ .catch((error) => {
+ console.log('Error bulk write booking reservation change log :', error);
+ reject(error);
+ });
+ })
+ .catch((error) => {
+ console.log('Error bulk write booking reservations :', error);
+ reject(error);
+ });
+ })
+ .catch((error) => {
+ console.log('Error fetching bookings from ORD ', error);
+ reject(error);
+ });
+ });
+};
+
+module.exports = {
+ checkBookingChanges
+};
diff --git a/services/integration/doorLockCharges.js b/services/integration/doorLockCharges.js
index 0aeb222..582ba28 100644
--- a/services/integration/doorLockCharges.js
+++ b/services/integration/doorLockCharges.js
@@ -510,52 +510,66 @@ const getIncidentData = (reservation) => {
};
const calculateDoorLockCharges = () => {
- getAllFinishedBookings()
- .then((reservations) => {
- const unlockedIncidents = [];
- const unscheduledIncidents = [];
+ return new Promise((resolve, reject) => {
+ getAllFinishedBookings()
+ .then((reservations) => {
+ const unlockedIncidents = [];
+ const unscheduledIncidents = [];
- const asyncCheckForIncidents = [];
+ const asyncCheckForIncidents = [];
- reservations.forEach((reservation) => {
- asyncCheckForIncidents.push(getIncidentData(reservation));
- });
+ reservations.forEach((reservation) => {
+ asyncCheckForIncidents.push(getIncidentData(reservation));
+ });
- Promise.all(asyncCheckForIncidents)
- .then((allReservationsIncidents) => {
- allReservationsIncidents.forEach((reservationIncidents) => {
- reservationIncidents.forEach((incident) => {
- if (incident.error) {
- console.log('Error checking incident : ', incident.error);
- } else if (incident.incidentType) {
- switch (incident.incidentType) {
- case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
- case incidentType.UNLOCKED_INCIDENT_STANDALONE:
- unlockedIncidents.push(incident);
- break;
- case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
- case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
- case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
- unscheduledIncidents.push(incident);
- break;
+ Promise.all(asyncCheckForIncidents)
+ .then((allReservationsIncidents) => {
+ allReservationsIncidents.forEach((reservationIncidents) => {
+ reservationIncidents.forEach((incident) => {
+ if (incident.error) {
+ console.log('Error checking incident : ', incident.error);
+ } else if (incident.incidentType) {
+ switch (incident.incidentType) {
+ case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
+ case incidentType.UNLOCKED_INCIDENT_STANDALONE:
+ unlockedIncidents.push(incident);
+ break;
+ case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
+ case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
+ case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
+ unscheduledIncidents.push(incident);
+ break;
+ }
}
- }
+ });
});
- });
- insertUnscheduledIncidents(unscheduledIncidents)
- .catch((error) => console.log(error));
+ setUnlockedIncidentsLevel(unlockedIncidents)
+ .then((completedUnlockedIncidents) => {
+ const insertIncidentsJobs = [insertUnscheduledIncidents(unscheduledIncidents), insertUnlockedIncidents(completedUnlockedIncidents)];
- 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));
+ Promise.all(insertIncidentsJobs)
+ .then(() => {
+ resolve(true);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+
+ /*
+ insertUnscheduledIncidents(unscheduledIncidents)
+ .catch((error) => console.log(error));
+
+ insertUnlockedIncidents(completedUnlockedIncidents)
+ .catch((error) => console.log(error));
+ */
+ })
+ .catch((error) => reject(error));
+ })
+ .catch((error) => reject(error));
+ })
+ .catch((error) => reject(error));
+ });
};
module.exports = {
diff --git a/services/integration/invoiceIntegration.js b/services/integration/invoiceIntegration.js
new file mode 100644
index 0000000..b54e875
--- /dev/null
+++ b/services/integration/invoiceIntegration.js
@@ -0,0 +1,257 @@
+'use strict';
+
+const moment = require('moment-timezone');
+
+const { getAllIncidents } = require('./reports');
+const { getActiveBookingsForMembersInDateRange } = require('./bookings');
+
+const { DEFAULT_DATE_FORMAT, UI_TIMEZONE, incidentTypeExplanations, incidentType, unlockedIncidentLevelsPrices } = require('../../constants/constants');
+const { getResourceMappings } = require('../officeRnD/resources');
+const { fetchAllMembers } = require('../officeRnD/members');
+
+const createFeeFromIncident = (incident) => {
+ const {
+ memberId,
+ officeId,
+ officeName,
+ resourceName,
+ oldResourceName,
+ newResourceName,
+ bookingStartRaw,
+ bookingEndRaw,
+ oldBookingStartRaw,
+ oldBookingEndRaw,
+ newBookingStartRaw,
+ newBookingEndRaw,
+ unlockTimestampRaw,
+ lockTimestampRaw,
+ incidentLevel,
+ timeIntervalsToCharge,
+ incidentPrice,
+ chargePrice,
+ totalChargeFee,
+ incidentTimestampRaw
+ } = incident;
+ const incidentTypeNumber = incident.incidentType;
+
+ const incidentExplanation = incidentTypeExplanations[incidentTypeNumber];
+
+ let date = '';
+ let price = 0;
+ let quantity = 0;
+ let priceExplanation = '';
+ let bookingTimeExplanation = '';
+ let incidentTimeExplanation = '';
+ let additionalIncidentExplanation = '';
+
+ let roomExplanation = '';
+ let dateExplanation = '';
+
+ const bookingStartMoment = moment.tz(bookingStartRaw, UI_TIMEZONE);
+ const bookingEndMoment = moment.tz(bookingEndRaw, UI_TIMEZONE);
+ const unlockMoment = moment.tz(unlockTimestampRaw, UI_TIMEZONE);
+ const lockMoment = moment.tz(lockTimestampRaw, UI_TIMEZONE);
+
+ const oldBookingStartMoment = moment.tz(oldBookingStartRaw, UI_TIMEZONE);
+ const oldBookingEndMoment = moment.tz(oldBookingEndRaw, UI_TIMEZONE);
+ const newBookingStartMoment = moment.tz(newBookingStartRaw, UI_TIMEZONE);
+ const newBookingEndMoment = moment.tz(newBookingEndRaw, UI_TIMEZONE);
+
+ const incidentTimestampMoment = moment.tz(incidentTimestampRaw, UI_TIMEZONE);
+
+ switch (incidentTypeNumber){
+ case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
+ roomExplanation = resourceName;
+ dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD, YYYY');
+ bookingTimeExplanation = `${bookingStartMoment.clone().format('HH:mm a')} - ${bookingEndMoment.clone().format('HH:mm a')}`;
+ incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')}`;
+ unlockedIncidentLevelsPrices[incidentLevel].description
+ additionalIncidentExplanation = unlockedIncidentLevelsPrices[incidentLevel].description;
+
+ date = bookingStartMoment.clone().startOf('day').format();
+
+ price = incidentPrice;
+ quantity = 1;
+ priceExplanation = `$${price.toFixed(2)}, 1 x $${price.toFixed(2)}`;
+ break;
+ case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
+ roomExplanation = resourceName;
+ dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD, YYYY');
+ bookingTimeExplanation = `${bookingStartMoment.clone().format('HH:mm a')} - ${bookingEndMoment.clone().format('HH:mm a')}`;
+ incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')}`;
+
+ date = bookingStartMoment.clone().startOf('day').format();
+
+ price = chargePrice;
+ quantity = timeIntervalsToCharge;
+ priceExplanation = `$${totalChargeFee.toFixed(2)}, ${quantity} x $${price.toFixed(2)}`;
+ break;
+ case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
+ roomExplanation = resourceName;
+ dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD, YYYY');
+ bookingTimeExplanation = `${bookingStartMoment.clone().format('HH:mm a')} - ${bookingEndMoment.clone().format('HH:mm a')}`;
+ incidentTimeExplanation = `LOCK : ${lockMoment.clone().format('HH:mm a')}`;
+
+ date = bookingStartMoment.clone().startOf('day').format();
+
+ price = chargePrice;
+ quantity = timeIntervalsToCharge;
+ priceExplanation = `$${totalChargeFee.toFixed(2)}, ${quantity} x $${price.toFixed(2)}`;
+ break;
+ case incidentType.UNLOCKED_INCIDENT_STANDALONE:
+ roomExplanation = resourceName;
+ dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD, YYYY');
+ bookingTimeExplanation = `NO RESERVATION`;
+ incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')}`;
+ additionalIncidentExplanation = unlockedIncidentLevelsPrices[incidentLevel].description;
+
+ date = unlockMoment.clone().startOf('day').format();
+
+ price = incidentPrice;
+ quantity = 1;
+ priceExplanation = `$${price.toFixed(2)}, 1 x $${price.toFixed(2)}`;
+ break;
+ case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
+ roomExplanation = resourceName;
+ dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD, YYYY');
+ bookingTimeExplanation = `NO RESERVATION`;
+ incidentTimeExplanation = `UNLOCK : ${unlockMoment.clone().format('HH:mm a')} LOCK : ${lockMoment.clone().format('HH:mm a')}`;
+ additionalIncidentExplanation = '';
+
+ date = unlockMoment.clone().startOf('day').format();
+
+ price = chargePrice;
+ quantity = timeIntervalsToCharge;
+ priceExplanation = `$${totalChargeFee.toFixed(2)}, ${quantity} x $${price.toFixed(2)}`;
+ break;
+ case incidentType.BOOKING_MOVED_TO_ANOTHER_DAY:
+ if (oldResourceName !== newResourceName){
+ roomExplanation = `${oldResourceName} -> ${newResourceName}`;
+ }else{
+ roomExplanation = oldResourceName;
+ }
+
+ dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD, YYYY')} -> ${newBookingStartMoment.clone().format('MMM DD, YYYY')}`;
+ bookingTimeExplanation = `(${oldBookingStartMoment.clone().format('HH:mm a')} - ${oldBookingEndMoment.clone().format('HH:mm a')}) -> (${newBookingStartMoment.clone().format('HH:mm a')} - ${newBookingEndMoment.clone().format('HH:mm a')})`;
+ incidentTimeExplanation = `MOVED ON : ${incidentTimestampMoment.clone().format('MMM DD, YYYY')}`;
+
+ date = incidentTimestampMoment.clone().startOf('day').format();
+
+ price = totalChargeFee;
+ quantity = 1;
+ priceExplanation = `$${totalChargeFee.toFixed(2)}, 1 x $${price.toFixed(2)}`;
+ break;
+ case incidentType.BOOKING_SHORTENED:
+ if (oldResourceName !== newResourceName){
+ roomExplanation = `${oldResourceName} -> ${newResourceName}`;
+ }else{
+ roomExplanation = oldResourceName;
+ }
+
+ dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD, YYYY')}`;
+ bookingTimeExplanation = `(${oldBookingStartMoment.clone().format('HH:mm a')} - ${oldBookingEndMoment.clone().format('HH:mm a')}) -> (${newBookingStartMoment.clone().format('HH:mm a')} - ${newBookingEndMoment.clone().format('HH:mm a')})`;
+ incidentTimeExplanation = `SHORTENED ON : ${incidentTimestampMoment.clone().format('MMM DD, YYYY')}`;
+
+ date = incidentTimestampMoment.clone().startOf('day').format();
+
+ price = totalChargeFee;
+ quantity = 1;
+ priceExplanation = `$${totalChargeFee.toFixed(2)}, 1 x $${price.toFixed(2)}`;
+ break;
+ case incidentType.BOOKING_CANCELED_LATE:
+ roomExplanation = oldResourceName;
+ dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD, YYYY')}`;
+ bookingTimeExplanation = `${oldBookingStartMoment.clone().format('HH:mm a')} - ${oldBookingEndMoment.clone().format('HH:mm a')}`;
+ incidentTimeExplanation = `CANCELED ON : ${incidentTimestampMoment.clone().format('MMM DD, YYYY')}`;
+
+ date = incidentTimestampMoment.clone().startOf('day').format();
+
+ price = totalChargeFee;
+ quantity = 1;
+ priceExplanation = `$${totalChargeFee.toFixed(2)}, 1 x $${price.toFixed(2)}`;
+ break;
+ }
+
+ const formattedName = `[INCIDENT FEES] ${officeName}, ${roomExplanation}, ${dateExplanation}, ${bookingTimeExplanation}, ${incidentTimeExplanation}, ${incidentExplanation}, ${additionalIncidentExplanation} ${additionalIncidentExplanation !== '' ? ',' : ''} ${priceExplanation}`;
+
+ return {
+ name: formattedName,
+ price,
+ quantity,
+ date,
+ member: memberId,
+ team: null,
+ office: officeId,
+ isPersonal: false,
+ }
+};
+
+const createFeeFromBooking = (booking, resourceMappings) => {
+ const { officeId, resourceId, memberId, start, end, timezone, hourlyRate } = booking;
+ const { officesMap, resourcesMap } = resourceMappings;
+
+ const startMoment = moment.tz(start, DEFAULT_DATE_FORMAT, timezone);
+ const endMoment = moment.tz(end, DEFAULT_DATE_FORMAT, timezone);
+ const reservationLength = endMoment.diff(startMoment, 'hours', true);
+
+ const officeName = officesMap[officeId].officeName || 'Unknown';
+ const resourceName = resourcesMap[resourceId].resourceName || 'Unknown';
+
+ const totalCost = (hourlyRate*reservationLength).toFixed(2);
+
+ const formattedDate = startMoment.clone().startOf('day').format('MMM DD, YYYY');
+ const formattedStartTime = startMoment.format('HH:mm a');
+ const formattedEndTime = endMoment.format('HH:mm a');
+
+ const formattedName = `[BOOKING FEES] ${officeName}, ${resourceName}, $${totalCost}, ${reservationLength.toFixed(2)} x $${hourlyRate.toFixed(2)}, ${formattedDate} [${formattedStartTime} - ${formattedEndTime}]`;
+
+ return {
+ name: formattedName,
+ price: hourlyRate,
+ quantity: reservationLength,
+ date: startMoment.startOf('day').toISOString(),
+ member: memberId,
+ team: null,
+ office: officeId,
+ isPersonal: false,
+ }
+};
+
+const getMembersFeesForDateRange = (dateRange, memberIds) => {
+ return new Promise((resolve, reject) => {
+ const collectData = [getAllIncidents(dateRange, memberIds), getActiveBookingsForMembersInDateRange(dateRange, memberIds), getResourceMappings(), fetchAllMembers()];
+
+
+ Promise.all(collectData)
+ .then((result) => {
+ const allIncidents = result[0];
+ const allActiveBookings = result[1];
+ const resourceMappings = result[2];
+ const membersList = result[3];
+
+ const memberIdTeamMappings = {};
+ membersList.forEach((member) => {
+ memberIdTeamMappings[member.memberId] = member.teamId;
+ });
+
+ const allFees = [];
+
+ allIncidents.forEach((incident) => allFees.push(createFeeFromIncident(incident)));
+ allActiveBookings.forEach((booking) => allFees.push(createFeeFromBooking(booking, resourceMappings)));
+
+ allFees.forEach((fee) => {
+ fee.team = memberIdTeamMappings[fee.member] || null;
+ });
+
+ resolve(allFees);
+ })
+ .catch((error) => {
+ console.log(error);
+ reject(error);
+ });
+ });
+};
+
+module.exports = {
+ getMembersFeesForDateRange,
+};
diff --git a/services/integration/reports.js b/services/integration/reports.js
index 4bd54ca..ccab8d1 100644
--- a/services/integration/reports.js
+++ b/services/integration/reports.js
@@ -10,7 +10,7 @@ const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors
const { fetchAllMembers } = require('../officeRnD/members');
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
-const getUnlockedIncidents = (startDate, endDate, memberId) => {
+const getUnlockedIncidents = (startDate, endDate, memberIds) => {
const attributes = ['id', 'reservationId', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'unlockTimestamp', 'incidentLevel', 'incidentLevelPrice'];
const filters = {};
@@ -41,8 +41,10 @@ const getUnlockedIncidents = (startDate, endDate, memberId) => {
Object.assign(filters, bookingStartOrUnlockTimestamp);
}
- if (memberId){
- filters.memberId = memberId;
+ if (memberIds.length > 0){
+ filters.memberId = {
+ [Op.in]: memberIds
+ };
}
return db.unlockedIncident.findAll({
@@ -54,7 +56,7 @@ const getUnlockedIncidents = (startDate, endDate, memberId) => {
});
};
-const getUnscheduledIncidents = (startDate, endDate, memberId) => {
+const getUnscheduledIncidents = (startDate, endDate, memberIds) => {
const attributes = [
'id',
'reservationId',
@@ -98,8 +100,10 @@ const getUnscheduledIncidents = (startDate, endDate, memberId) => {
}
- if (memberId){
- filters.memberId = memberId;
+ if (memberIds.length > 0){
+ filters.memberId = {
+ [Op.in]: memberIds
+ };
}
return db.unscheduledIncident.findAll({
@@ -111,7 +115,7 @@ const getUnscheduledIncidents = (startDate, endDate, memberId) => {
});
};
-const getBookingChangeIncidents = (startDate, endDate, memberId) => {
+const getBookingChangeIncidents = (startDate, endDate, memberIds) => {
const attributes = [
'id',
'reservationId',
@@ -138,8 +142,10 @@ const getBookingChangeIncidents = (startDate, endDate, memberId) => {
}
}
- if (memberId){
- filters.memberId = memberId;
+ if (memberIds.length > 0){
+ filters.memberId = {
+ [Op.in]: memberIds
+ };
}
return db.bookingChangeIncident.findAll({
@@ -160,7 +166,7 @@ const formatTime = (timestamp) => {
}
};
-const getAllIncidents = (dateRange, memberId) => {
+const getAllIncidents = (dateRange, memberIds) => {
return new Promise ((resolve, reject) => {
let startDate, endDate;
@@ -178,9 +184,9 @@ const getAllIncidents = (dateRange, memberId) => {
fetchAllMembers(),
fetchOffices(),
fetchResources(),
- getUnlockedIncidents(startDate, endDate, memberId),
- getUnscheduledIncidents(startDate, endDate, memberId),
- getBookingChangeIncidents(startDate, endDate, memberId)
+ getUnlockedIncidents(startDate, endDate, memberIds),
+ getUnscheduledIncidents(startDate, endDate, memberIds),
+ getBookingChangeIncidents(startDate, endDate, memberIds)
];
Promise.all(dataFetchJobs)
@@ -210,10 +216,14 @@ const getAllIncidents = (dateRange, memberId) => {
memberId: unlockedIncident.memberId,
memberName: membersMap[unlockedIncident.memberId].name,
resourceName: resourcesMap[unlockedIncident.resourceId].resourceName,
+ officeId: resourcesMap[unlockedIncident.resourceId].officeId,
officeName: officesMap[resourcesMap[unlockedIncident.resourceId].officeId].officeName,
bookingStart: formatTime(unlockedIncident.bookingStart),
bookingEnd: formatTime(unlockedIncident.bookingEnd),
+ bookingStartRaw: unlockedIncident.bookingStart,
+ bookingEndRaw: unlockedIncident.bookingEnd,
unlockTimestamp: formatTime(unlockedIncident.unlockTimestamp),
+ unlockTimestampRaw: unlockedIncident.unlockTimestamp,
incidentType: incidentTypeNumber,
incidentLevel: unlockedIncident.incidentLevel,
incidentPrice: unlockedIncident.incidentLevelPrice,
@@ -236,11 +246,16 @@ const getAllIncidents = (dateRange, memberId) => {
memberId: unscheduledIncident.memberId,
memberName: membersMap[unscheduledIncident.memberId].name,
resourceName: resourcesMap[unscheduledIncident.resourceId].resourceName,
+ officeId: resourcesMap[unscheduledIncident.resourceId].officeId,
officeName: officesMap[resourcesMap[unscheduledIncident.resourceId].officeId].officeName,
bookingStart: formatTime(unscheduledIncident.bookingStart),
bookingEnd: formatTime(unscheduledIncident.bookingEnd),
+ bookingStartRaw: unscheduledIncident.bookingStart,
+ bookingEndRaw: unscheduledIncident.bookingEnd,
unlockTimestamp: formatTime(unscheduledIncident.unlockTimestamp),
lockTimestamp: formatTime(unscheduledIncident.lockTimestamp),
+ unlockTimestampRaw: unscheduledIncident.unlockTimestamp,
+ lockTimestampRaw: unscheduledIncident.lockTimestamp,
incidentType: incidentTypeNumber,
timeIntervalsToCharge: unscheduledIncident.timeIntervalsToCharge,
chargePrice: unscheduledIncident.chargePrice,
@@ -267,21 +282,28 @@ const getAllIncidents = (dateRange, memberId) => {
const newResource = newResourceId ? resourcesMap[newResourceId] : null;
const oldResourceName = oldResource.resourceName;
const newResourceName = newResource ? newResource.resourceName : null;
- const officeName = officesMap[oldResource.officeId].officeName;
+ const officeId = oldResource.officeId;
+ const officeName = officesMap[officeId].officeName;
allIncidents.push({
incidentId: id,
memberId,
memberName,
oldResourceName,
newResourceName,
+ officeId,
officeName,
oldBookingStart: formatTime(oldBookingStart),
oldBookingEnd: formatTime(oldBookingEnd),
newBookingStart: formatTime(newBookingStart),
newBookingEnd: formatTime(newBookingEnd),
+ oldBookingStartRaw: oldBookingStart,
+ oldBookingEndRaw: oldBookingEnd,
+ newBookingStartRaw: newBookingStart,
+ newBookingEndRaw: newBookingEnd,
incidentType,
totalChargeFee: chargeFee,
incidentTimestamp: formatTime(createdAt),
+ incidentTimestampRaw: createdAt,
});
});
@@ -292,7 +314,5 @@ const getAllIncidents = (dateRange, memberId) => {
};
module.exports = {
- getUnlockedIncidents,
- getUnscheduledIncidents,
getAllIncidents,
};
diff --git a/services/officeRnD/fees.js b/services/officeRnD/fees.js
new file mode 100644
index 0000000..dd93131
--- /dev/null
+++ b/services/officeRnD/fees.js
@@ -0,0 +1,70 @@
+'use strict';
+
+const moment = require('moment-timezone');
+
+const { API } = require('../../helpers/api');
+const { officeRnDAPIErrors, DEFAULT_DATE_FORMAT } = require('../../constants/constants');
+
+const deleteFeesFromORD = (dateRange, memberIds) => {
+ return new Promise((resolve, reject) => {
+ const startDate = moment.utc(dateRange.startDate, DEFAULT_DATE_FORMAT).startOf('day');
+ const endDate = moment.utc(dateRange.endDate, DEFAULT_DATE_FORMAT).endOf('day');
+
+ API.get('fees')
+ .then((response) => {
+ const fetchedFees = response.data ? response.data : [];
+
+ const memberIdsMap = {};
+ memberIds.forEach((memberId) => {
+ memberIdsMap[memberId] = true;
+ });
+
+ const deleteRequests = [];
+ const sendDeleteRequestPromise = (feeId) => {
+ return new Promise((resolve, reject) => {
+ API.delete(`fees/${feeId}`)
+ .then(() => resolve(true))
+ .catch(() => resolve(false));
+ });
+ };
+
+ fetchedFees.forEach((fee) => {
+ const { member, date } = fee;
+ const feeId = fee['_id'];
+
+ const isDateInDateRange = startDate.isSameOrBefore(date) && endDate.isSameOrAfter(date);
+ if (memberIdsMap[member] && isDateInDateRange) {
+ deleteRequests.push(sendDeleteRequestPromise(feeId));
+ }
+ });
+
+ Promise.all(deleteRequests)
+ .then(() => {
+ resolve(true);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ })
+ .catch((error) => {
+ console.log(error);
+ reject(officeRnDAPIErrors.FAILED_TO_FETCH_FEES);
+ });
+ });
+};
+
+const addFeesToORD = (allFees) => {
+ return new Promise((resolve, reject) => {
+ API.post('/fees', allFees)
+ .catch((error) => {
+ console.log('==== ERROR ====');
+ console.log(error);
+ });
+ resolve(allFees.length);
+ });
+};
+
+module.exports = {
+ deleteFeesFromORD,
+ addFeesToORD
+};
diff --git a/services/officeRnD/members.js b/services/officeRnD/members.js
index 6c804d3..42bbe1d 100644
--- a/services/officeRnD/members.js
+++ b/services/officeRnD/members.js
@@ -12,6 +12,7 @@ const fetchAllMembers = () => {
cleanedResult.push({
name: member.name,
memberId: member['_id'],
+ teamId: member.team,
});
});
cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 );
diff --git a/services/officeRnD/resources.js b/services/officeRnD/resources.js
index 95b1d64..37d73b7 100644
--- a/services/officeRnD/resources.js
+++ b/services/officeRnD/resources.js
@@ -45,6 +45,30 @@ const fetchResources = () => {
});
};
+const getResourceMappings = () => {
+ return new Promise((resolve, reject) => {
+ const fetchJobs = [fetchOffices(), fetchResources()];
+
+ Promise.all(fetchJobs)
+ .then((mappings) => {
+ const offices = mappings[0];
+ const resources = mappings[1];
+
+ const officesMap = {};
+ const resourcesMap = {};
+
+ offices.forEach((office) => officesMap[office.officeId] = office);
+ resources.forEach((resource) => resourcesMap[resource.resourceId] = resource);
+
+ resolve({
+ officesMap,
+ resourcesMap,
+ });
+ })
+ .catch((error) => reject(error));
+ });
+};
+
const getMappingsFromDatabase = () => {
return db.officeResourceMapping.findAll();
};
@@ -57,5 +81,6 @@ module.exports = {
getMappingsFromDatabase,
fetchOffices,
fetchResources,
+ getResourceMappings,
saveNewMappingToDatabase,
};