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/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/constants/constants.js b/constants/constants.js index d339663..0388308 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,7 +56,10 @@ 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 = { FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations', @@ -71,6 +80,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 +113,7 @@ module.exports = { unlockedIncidentLevelsPrices, integrationServiceErrors, incidentType, + incidentTypeExplanations, UI_TIMEZONE, DEFAULT_DATE_FORMAT, MAX_BACK_TO_BACK_DIFFERENCE, 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/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/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/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, };