From 2db47e95e1d6e0665452cb4e4bb4fbd546db593e Mon Sep 17 00:00:00 2001 From: Senad Uka Date: Thu, 25 Jul 2019 06:53:32 +0200 Subject: [PATCH] Fix for loading --- .../GenerateFeesInORDButton/index.js | 53 ++++ .../components/MemberIncidentsTable/index.js | 111 -------- client/src/scenes/IncidentsReport/index.js | 38 ++- .../src/scenes/PracticeSummaryReport/index.js | 24 +- .../src/store/actions/integrationActions.js | 17 ++ client/src/store/constants.js | 4 + .../src/store/reducers/addFeesToOrdReducer.js | 38 +++ client/src/store/reducers/index.js | 2 + config/config.js | 15 +- config/config.json | 16 -- constants/constants.js | 35 ++- controllers/doorLock.js | 34 ++- controllers/integration.js | 51 +++- cronServices/checkBookingChanges.js | 39 +-- environment.env | 5 + routes/index.js | 10 +- services/doorLock.js | 150 ---------- services/integration/bookingChangeCharges.js | 2 +- services/integration/bookings.js | 53 ++++ services/integration/checkBookingChange.js | 44 +++ services/integration/doorLockCharges.js | 92 ++++--- services/integration/invoiceIntegration.js | 257 ++++++++++++++++++ services/integration/reports.js | 52 ++-- services/officeRnD/fees.js | 70 +++++ services/officeRnD/members.js | 1 + services/officeRnD/resources.js | 25 ++ 26 files changed, 838 insertions(+), 400 deletions(-) create mode 100644 client/src/components/GenerateFeesInORDButton/index.js delete mode 100644 client/src/components/MemberIncidentsTable/index.js create mode 100644 client/src/store/reducers/addFeesToOrdReducer.js delete mode 100644 config/config.json delete mode 100644 services/doorLock.js create mode 100644 services/integration/bookings.js create mode 100644 services/integration/checkBookingChange.js create mode 100644 services/integration/invoiceIntegration.js create mode 100644 services/officeRnD/fees.js 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 ( +
+ + + + Add fees to the ORD + +

{modalContent}

+
+ + +
+ ); + } +} + +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
{cellValue}
- } - }); - } - }); - } - - 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, };