diff --git a/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js b/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js
index 5c1d91e..98e778a 100644
--- a/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js
+++ b/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js
@@ -4,33 +4,48 @@ import ReactTable from 'react-table';
import 'react-table/react-table.css';
import { NavLink } from 'react-router-dom';
-import {incidentsReportHeaderTitles} from '../../../constants/menuItems';
+import {incidentsReportHeaderTitles} from '../../../constants/constants';
import {
+ incidentTableTypes,
incidentDescriptions,
incidentLevelDescriptions,
UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION, UNLOCKED_INCIDENT_STANDALONE, UNSCHEDULED_INCIDENT_AFTER_RESERVATION,
UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE
} from '../../../constants/enums';
+import { doorLockRelatedWithReservationIncidentHeaders, standaloneDoorLockIncidentHeaders, bookingChangeIncidentHeaders } from '../../../constants/constants';
const SingleIncidentsTable = props => {
- const { loading, title, openMemberSummaryOnMemberClick, showBookingTimes, showDoorLockEntryTimes, hideMemberName } = props;
+ const {
+ loading,
+ title,
+ openMemberSummaryOnMemberClick,
+ hideMemberName,
+ tableType
+ } = props;
const incidents = props.incidents ? props.incidents : [];
-
const columns = [];
- if (incidents && incidents.length > 0){
- const incidentHeaders = Object.keys(incidentsReportHeaderTitles);
- incidentHeaders.forEach((header) => {
+ if (incidents && incidents.length > 0){
+ let tableHeaders;
+ switch (tableType) {
+ case incidentTableTypes.INCIDENTS_RELATED_TO_RESERVATIONS:
+ tableHeaders = doorLockRelatedWithReservationIncidentHeaders;
+ break;
+ case incidentTableTypes.STANDALONE_INCIDENTS:
+ tableHeaders = standaloneDoorLockIncidentHeaders;
+ break;
+ case incidentTableTypes.BOOKING_CHANGE_INCIDENTS:
+ tableHeaders = bookingChangeIncidentHeaders;
+ break;
+ default:
+ break;
+ }
+
+ tableHeaders.forEach((header) => {
const columnTitle = incidentsReportHeaderTitles[header];
let showColumn = true;
- if ((header === 'bookingStart' || header === 'bookingEnd') && !showBookingTimes){
- showColumn = false;
- }
- if ((header === 'unlockTimestamp' || header === 'lockTimestamp') && !showDoorLockEntryTimes){
- showColumn = false;
- }
if (header === 'memberName' && hideMemberName){
showColumn = false;
}
@@ -55,6 +70,16 @@ const SingleIncidentsTable = props => {
urlValue = `/practice-summary-report/${memberId}`;
cellValue = props.value;
break;
+ case 'reservation':
+ const bookingStart = props.row['_original'].bookingStart;
+ const bookingEnd = props.row['_original'].bookingEnd;
+ cellValue = `${bookingStart}\n${bookingEnd}`;
+ break;
+ case 'doorLockTimestamps':
+ const unlockTimestamp = props.row['_original'].unlockTimestamp;
+ const lockTimestamp = props.row['_original'].lockTimestamp;
+ cellValue = `${unlockTimestamp ? unlockTimestamp : '---'}\n${lockTimestamp ? lockTimestamp : '---'}`;
+ break;
case 'incidentType':
cellValue = incidentDescriptions[props.value];
break;
@@ -85,6 +110,16 @@ const SingleIncidentsTable = props => {
cellValue = `$ ${totalFeeFormatted}`;
columnContentsAlignment = columnAlignments.right;
break;
+ case 'oldReservation':
+ const oldBookingStart = props.row['_original'].oldBookingStart;
+ const oldBookingEnd = props.row['_original'].oldBookingEnd;
+ cellValue = `${oldBookingStart}\n${oldBookingEnd}`;
+ break;
+ case 'newReservation':
+ const newBookingStart = props.row['_original'].newBookingStart;
+ const newBookingEnd = props.row['_original'].newBookingEnd;
+ cellValue = `${newBookingStart}\n${newBookingEnd}`;
+ break;
default:
cellValue = props.value;
}
@@ -92,7 +127,7 @@ const SingleIncidentsTable = props => {
if (openMemberSummaryOnMemberClick && urlValue){
return {cellValue}
}else{
- return
{cellValue}
+ return {cellValue}
}
}
});
diff --git a/client/src/components/MemberIncidentsTables/index.js b/client/src/components/MemberIncidentsTables/index.js
index ed05541..ad6706a 100644
--- a/client/src/components/MemberIncidentsTables/index.js
+++ b/client/src/components/MemberIncidentsTables/index.js
@@ -3,7 +3,8 @@ import {Accordion, Label} from 'semantic-ui-react';
import SingleIncidentsTable from './components/SingleIncidentsTable';
import {
UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION, UNLOCKED_INCIDENT_STANDALONE, UNSCHEDULED_INCIDENT_AFTER_RESERVATION,
- UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE
+ UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE, BOOKING_MOVED_TO_ANOTHER_DAY, BOOKING_SHORTENED,
+ incidentTableTypes
} from '../../constants/enums';
export default function MemberIncidentsTables (props) {
@@ -11,6 +12,7 @@ export default function MemberIncidentsTables (props) {
const incidentsRelatedToReservations = [];
const standaloneIncidents = [];
+ const bookingChangeIncidents = [];
if (Array.isArray(incidents)){
incidents.forEach((incident) => {
@@ -25,6 +27,12 @@ export default function MemberIncidentsTables (props) {
case UNSCHEDULED_INCIDENT_STANDALONE:
standaloneIncidents.push(incident);
break;
+ case BOOKING_MOVED_TO_ANOTHER_DAY:
+ case BOOKING_SHORTENED:
+ bookingChangeIncidents.push(incident);
+ break;
+ default:
+ break;
}
}
});
@@ -35,8 +43,8 @@ export default function MemberIncidentsTables (props) {
loading={pendingIncidents}
incidents={incidentsRelatedToReservations}
openMemberSummaryOnMemberClick
- showBookingTimes
hideMemberName={hideMemberName}
+ tableType={incidentTableTypes.INCIDENTS_RELATED_TO_RESERVATIONS}
/>
);
@@ -45,8 +53,18 @@ export default function MemberIncidentsTables (props) {
loading={pendingIncidents}
incidents={standaloneIncidents}
openMemberSummaryOnMemberClick
- showDoorLockEntryTimes
hideMemberName={hideMemberName}
+ tableType={incidentTableTypes.STANDALONE_INCIDENTS}
+ />
+ );
+
+ const bookingChangeIncidentsTable = (
+
);
@@ -63,7 +81,8 @@ export default function MemberIncidentsTables (props) {
},
{
key: 'reservation-modification-incidents',
- title: {content: },
+ title: {content: },
+ content: { content: bookingChangeIncidentsTable },
}
];
diff --git a/client/src/constants/constants.js b/client/src/constants/constants.js
index ed5633b..22e9725 100644
--- a/client/src/constants/constants.js
+++ b/client/src/constants/constants.js
@@ -1 +1,48 @@
export const defaultDateFormat = 'YYYY-MM-DD';
+
+export const doorLockRelatedWithReservationIncidentHeaders = [
+ 'officeName',
+ 'resourceName',
+ 'memberName',
+ 'reservation',
+ 'incidentType',
+ 'feeDescription',
+ 'totalChargeFee'
+];
+export const standaloneDoorLockIncidentHeaders = [
+ 'officeName',
+ 'resourceName',
+ 'memberName',
+ 'doorLockTimestamps',
+ 'incidentType',
+ 'feeDescription',
+ 'totalChargeFee'
+];
+export const bookingChangeIncidentHeaders = [
+ 'officeName',
+ 'resourceName',
+ 'memberName',
+ 'incidentTimestamp',
+ 'oldReservation',
+ 'newReservation',
+ 'incidentType',
+ 'totalChargeFee',
+];
+
+export const incidentsReportHeaderTitles = {
+ officeName: 'Office',
+ resourceName: 'Room',
+ bookingStart: 'Reservation Start',
+ bookingEnd: 'Reservation End',
+ reservation: 'Reservation start/end',
+ doorLockTimestamps: 'Door unlock/lock',
+ unlockTimestamp: 'Unlock Time',
+ lockTimestamp: 'Lock Time',
+ oldReservation: 'Old Reservation',
+ newReservation: 'New Reservation',
+ incidentTimestamp: 'Incident Time',
+ memberName: 'Member Name',
+ incidentType: 'Incident Type',
+ feeDescription: 'Fee description',
+ totalChargeFee: 'Total Fee',
+};
diff --git a/client/src/constants/enums.js b/client/src/constants/enums.js
index 2a3f642..6a5c48b 100644
--- a/client/src/constants/enums.js
+++ b/client/src/constants/enums.js
@@ -4,6 +4,8 @@ export const UNSCHEDULED_INCIDENT_BEFORE_RESERVATION = 3;
export const UNSCHEDULED_INCIDENT_AFTER_RESERVATION = 4;
export const UNLOCKED_INCIDENT_STANDALONE = 5;
export const UNSCHEDULED_INCIDENT_STANDALONE = 6;
+export const BOOKING_MOVED_TO_ANOTHER_DAY = 7;
+export const BOOKING_SHORTENED = 8;
export const incidentDescriptions = {};
incidentDescriptions[UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION] = 'User left door unlocked';
@@ -11,6 +13,8 @@ incidentDescriptions[UNSCHEDULED_INCIDENT_BEFORE_RESERVATION] = 'Unscheduled use
incidentDescriptions[UNSCHEDULED_INCIDENT_AFTER_RESERVATION] = 'Unscheduled use - after';
incidentDescriptions[UNLOCKED_INCIDENT_STANDALONE] = 'User left door unlocked';
incidentDescriptions[UNSCHEDULED_INCIDENT_STANDALONE] = 'Unscheduled use';
+incidentDescriptions[BOOKING_MOVED_TO_ANOTHER_DAY] = 'Reservation moved to another day';
+incidentDescriptions[BOOKING_SHORTENED] = 'Reservation shortened';
export const incidentLevelDescriptions = {
UNLOCKED_0: 'First month',
@@ -20,3 +24,9 @@ export const incidentLevelDescriptions = {
UNLOCKED_4: 'Fifth month',
UNLOCKED_5: 'Sixth month',
};
+
+export const incidentTableTypes = {
+ INCIDENTS_RELATED_TO_RESERVATIONS: 1,
+ STANDALONE_INCIDENTS: 2,
+ BOOKING_CHANGE_INCIDENTS: 3,
+};
diff --git a/client/src/constants/menuItems.js b/client/src/constants/menuItems.js
index 8a83cfc..63cac6d 100644
--- a/client/src/constants/menuItems.js
+++ b/client/src/constants/menuItems.js
@@ -39,16 +39,3 @@ export const mainMenuItems = [
component: UploadDLockData,
},
];
-
-export const incidentsReportHeaderTitles = {
- officeName: 'Office',
- resourceName: 'Room',
- bookingStart: 'Reservation Start',
- bookingEnd: 'Reservation End',
- unlockTimestamp: 'Unlock Time',
- lockTimestamp: 'Lock Time',
- memberName: 'Member Name',
- incidentType: 'Incident Type',
- feeDescription: 'Fee description',
- totalChargeFee: 'Total Fee',
-};
diff --git a/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js b/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js
index 212d173..c6e6af0 100644
--- a/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js
+++ b/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js
@@ -4,7 +4,8 @@ import { Loader, Grid } from 'semantic-ui-react';
import {
UNSCHEDULED_INCIDENT_BEFORE_RESERVATION,
UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION,
- UNSCHEDULED_INCIDENT_AFTER_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE, UNLOCKED_INCIDENT_STANDALONE
+ UNSCHEDULED_INCIDENT_AFTER_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE, UNLOCKED_INCIDENT_STANDALONE,
+ BOOKING_MOVED_TO_ANOTHER_DAY, BOOKING_SHORTENED,
} from '../../../constants/enums';
const MemberSummary = props => {
@@ -13,6 +14,7 @@ const MemberSummary = props => {
let totalUnscheduledFees = 0;
let totalUnlockedFees = 0;
+ let totalBookingChangeFees = 0;
incidents.forEach((incident) => {
switch (incident.incidentType) {
@@ -25,15 +27,20 @@ const MemberSummary = props => {
case UNLOCKED_INCIDENT_STANDALONE:
totalUnlockedFees += parseFloat(incident.incidentPrice);
break;
+ case BOOKING_MOVED_TO_ANOTHER_DAY:
+ case BOOKING_SHORTENED:
+ totalBookingChangeFees += parseFloat(incident.totalChargeFee);
+ break;
default:
break;
}
});
- const grandTotal = totalUnlockedFees + totalUnscheduledFees;
+ const grandTotal = totalUnlockedFees + totalUnscheduledFees + totalBookingChangeFees;
const formattedUnscheduledFees = `$ ${totalUnscheduledFees.toFixed(2)}`;
const formattedUnlockedFees = `$ ${totalUnlockedFees.toFixed(2)}`;
+ const formattedBookingChangeFees = `$ ${totalBookingChangeFees.toFixed(2)}`;
const formattedGrandTotalFee = `$ ${grandTotal.toFixed(2)}`;
return (
@@ -44,7 +51,7 @@ const MemberSummary = props => {
!loading &&
-
+
Unscheduled incidents total :
@@ -52,7 +59,7 @@ const MemberSummary = props => {
-
+
Unlocked incidents total :
@@ -60,7 +67,15 @@ const MemberSummary = props => {
-
+
+ Booking change charges total :
+
+
+ {formattedBookingChangeFees}
+
+
+
+
Grand Total :
diff --git a/constants/constants.js b/constants/constants.js
index 8f6f966..9ccad10 100644
--- a/constants/constants.js
+++ b/constants/constants.js
@@ -66,6 +66,8 @@ const incidentType = {
UNSCHEDULED_INCIDENT_AFTER_RESERVATION: 4,
UNLOCKED_INCIDENT_STANDALONE: 5,
UNSCHEDULED_INCIDENT_STANDALONE: 6,
+ BOOKING_MOVED_TO_ANOTHER_DAY: 7,
+ BOOKING_SHORTENED: 8,
};
const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles';
@@ -76,6 +78,8 @@ const MAX_BACK_TO_BACK_DIFFERENCE = parseInt(process.env.MAX_BACK_TO_BACK_DIFFER
const UNSCHEDULED_TIME_RESOLUTION = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5;
const UNSCHEDULED_CHARGE_PRICE = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_PRICE) || 5;
+const BOOKING_CHANGE_PERCENTAGE_CHARGE = parseInt(process.env.BOOKING_CHANGE_PERCENTAGE_CHARGE) || 100;
+
module.exports = {
VALID_CSV_HEADERS,
USER_ENTRY_EVENT,
@@ -92,4 +96,5 @@ module.exports = {
MAX_BACK_TO_BACK_DIFFERENCE,
UNSCHEDULED_TIME_RESOLUTION,
UNSCHEDULED_CHARGE_PRICE,
+ BOOKING_CHANGE_PERCENTAGE_CHARGE,
};
diff --git a/controllers/integration.js b/controllers/integration.js
index fd2cde0..235a774 100644
--- a/controllers/integration.js
+++ b/controllers/integration.js
@@ -1,7 +1,7 @@
'use strict';
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
-const { getAllDoorLockIncidents } = require('../services/integration/reports');
+const { getAllIncidents } = require('../services/integration/reports');
const getKnownOfficeResourceMappings = (req, res) => {
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
@@ -32,13 +32,13 @@ const addNewMapping = (req, res) => {
}
};
-const getAllIncidents = (req, res) => {
+const getAllIncidentsController = (req, res) => {
const dateRange = {
startDate: req.params.startDate,
endDate: req.params.endDate,
};
- getAllDoorLockIncidents(dateRange)
+ getAllIncidents(dateRange)
.then((incidents) => {
res.send(incidents);
})
@@ -55,7 +55,7 @@ const getMemberIncidents = (req, res) => {
endDate: req.params.endDate,
};
- getAllDoorLockIncidents(dateRange, memberId)
+ getAllIncidents(dateRange, memberId)
.then((incidents) => {
res.send(incidents);
})
@@ -65,19 +65,9 @@ const getMemberIncidents = (req, res) => {
});
};
-const getUnlockedIncidents = (req, res) => {
-
-};
-
-const getUnscheduledIncidents = (req, res) => {
-
-};
-
module.exports = {
getKnownOfficeResourceMappings,
addNewMapping,
- getAllIncidents,
- getUnscheduledIncidents,
- getUnlockedIncidents,
+ getAllIncidentsController,
getMemberIncidents,
};
diff --git a/cronServices/checkBookingChanges.js b/cronServices/checkBookingChanges.js
new file mode 100644
index 0000000..00451a3
--- /dev/null
+++ b/cronServices/checkBookingChanges.js
@@ -0,0 +1,33 @@
+'use strict';
+require('dotenv').config();
+
+const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../services/officeRnD/bookings');
+
+const { chargeBookingChanges } = require('../services/integration/bookingChangeCharges');
+
+const checkBookingChanges = () => {
+ fetchAllBookings()
+ .then((reservations) => {
+ bulkWriteReservationsWithChangesTracking(reservations)
+ .then((changes) => {
+ chargeBookingChanges(changes)
+ .then(() => {
+ process.exit();
+ })
+ .catch((error) => {
+ console.log('Error creating charges ', 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();
diff --git a/environment.env b/environment.env
index f9172c0..1ea3b01 100644
--- a/environment.env
+++ b/environment.env
@@ -18,3 +18,5 @@ UNLOCK_4=Price for unlocked door, fifth month
UNLOCK_5=Price for unlocked door, sixth month
UNLOCK_STREAK_REPAIR_AFTER=Number of months without incidents to reset user incident level
+
+BOOKING_CHANGE_PERCENTAGE_CHARGE=Percentage of hourly reate to apply for cancellation-like charges
diff --git a/migrations/20190626135045-alter-booking-reservations-table-remove-old-primary-key.js b/migrations/20190626135045-alter-booking-reservations-table-remove-old-primary-key.js
new file mode 100644
index 0000000..bdf5ea5
--- /dev/null
+++ b/migrations/20190626135045-alter-booking-reservations-table-remove-old-primary-key.js
@@ -0,0 +1,14 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.removeConstraint('bookingReservations', 'bookingReservations_pkey');
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.addConstraint('bookingReservations', ['id'], {
+ name: 'bookingReservations_pkey',
+ type: 'PRIMARY KEY',
+ });
+ }
+};
diff --git a/migrations/20190626173324-alter-booking-reservations-table-add-new-primary-key.js b/migrations/20190626173324-alter-booking-reservations-table-add-new-primary-key.js
new file mode 100644
index 0000000..9cb25fb
--- /dev/null
+++ b/migrations/20190626173324-alter-booking-reservations-table-add-new-primary-key.js
@@ -0,0 +1,14 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addConstraint('bookingReservations', ['reservationId'], {
+ name: 'bookingReservations_pkey',
+ type: 'PRIMARY KEY',
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeConstraint('bookingReservations', 'bookingReservations_pkey');
+ }
+};
diff --git a/migrations/20190708114639-create-booking-change-incidents-table.js b/migrations/20190708114639-create-booking-change-incidents-table.js
new file mode 100644
index 0000000..8c6e382
--- /dev/null
+++ b/migrations/20190708114639-create-booking-change-incidents-table.js
@@ -0,0 +1,34 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable('bookingChangeIncidents', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ reservationId: Sequelize.TEXT,
+ memberId: Sequelize.TEXT,
+ resourceId: Sequelize.TEXT,
+ oldBookingStart: Sequelize.DATE,
+ oldBookingEnd: Sequelize.DATE,
+ newBookingStart: Sequelize.DATE,
+ newBookingEnd: Sequelize.DATE,
+ incidentType: Sequelize.INTEGER,
+ chargeFee: Sequelize.FLOAT,
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable('bookingChangeIncidents');
+ }
+};
diff --git a/migrations/20190708120732-add-reservation-hourly-price-column-to-the-booking-reservations-table.js b/migrations/20190708120732-add-reservation-hourly-price-column-to-the-booking-reservations-table.js
new file mode 100644
index 0000000..1cd1aca
--- /dev/null
+++ b/migrations/20190708120732-add-reservation-hourly-price-column-to-the-booking-reservations-table.js
@@ -0,0 +1,14 @@
+'use strict';
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn('bookingReservations', 'hourlyRate', {
+ type: Sequelize.FLOAT,
+ after: 'end',
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn('bookingReservations', 'hourlyRate');
+ }
+};
diff --git a/models/bookingChangeIncident.js b/models/bookingChangeIncident.js
new file mode 100644
index 0000000..1a91f1e
--- /dev/null
+++ b/models/bookingChangeIncident.js
@@ -0,0 +1,19 @@
+'use strict';
+
+module.exports = (sequelize, DataTypes) => {
+ const bookingChangeIncident = sequelize.define('bookingChangeIncident', {
+ reservationId: DataTypes.TEXT,
+ memberId: DataTypes.TEXT,
+ resourceId: DataTypes.TEXT,
+ oldBookingStart: DataTypes.DATE,
+ oldBookingEnd: DataTypes.DATE,
+ newBookingStart: DataTypes.DATE,
+ newBookingEnd: DataTypes.DATE,
+ incidentType: DataTypes.INTEGER,
+ chargeFee: DataTypes.FLOAT,
+ }, {});
+ bookingChangeIncident.associate = function(models) {
+ // associations can be defined here
+ };
+ return bookingChangeIncident;
+};
diff --git a/models/bookingReservation.js b/models/bookingReservation.js
index d977de3..d6ba4c7 100644
--- a/models/bookingReservation.js
+++ b/models/bookingReservation.js
@@ -2,15 +2,18 @@
module.exports = (sequelize, DataTypes) => {
const bookingReservation = sequelize.define('bookingReservation', {
- reservationId: DataTypes.TEXT,
+ reservationId: {
+ type: DataTypes.TEXT,
+ primaryKey: true,
+ },
memberId: DataTypes.TEXT,
officeId: DataTypes.TEXT,
resourceId: DataTypes.TEXT,
start: DataTypes.DATE,
end: DataTypes.DATE,
+ hourlyRate: DataTypes.FLOAT,
timezone: DataTypes.TEXT,
canceled: DataTypes.BOOLEAN,
-
}, {});
bookingReservation.associate = function(models) {
// associations can be defined here
diff --git a/package.json b/package.json
index c51ec9a..952c593 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
"start-server": "nodemon server.js",
"start-client": "cd client && yarn start",
"start": "node server.js",
- "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
+ "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client",
+ "check-booking-changes": "node ./cronServices/checkBookingChanges.js"
},
"engines": {
"node": "11.12.x"
diff --git a/routes/index.js b/routes/index.js
index abe717c..0a6f01a 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -2,7 +2,7 @@
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
const { uploadDoorLockData } = require('../controllers/doorLock');
-const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidents, getMemberIncidents,getUnscheduledIncidents, getUnlockedIncidents } = require('../controllers/integration');
+const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidentsController, getMemberIncidents } = require('../controllers/integration');
const { fetchMembersList } = require('../controllers/officeRnD');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
@@ -17,9 +17,7 @@ router.get('/integration/mappings', getKnownOfficeResourceMappings);
router.post('/integration/mappings', addNewMapping);
router.get('/integration/report/member/:memberId/:startDate/:endDate', getMemberIncidents);
-router.get('/integration/report/allIncidents/:startDate/:endDate', getAllIncidents);
-router.get('/integration/report/unlockedIncidents', getUnlockedIncidents);
-router.get('/integration/report/unscheduledIncidents', getUnscheduledIncidents);
+router.get('/integration/report/allIncidents/:startDate/:endDate', getAllIncidentsController);
router.get('/officeRnD/membersList', fetchMembersList);
diff --git a/services/integration/bookingChangeCharges.js b/services/integration/bookingChangeCharges.js
new file mode 100644
index 0000000..13d47b2
--- /dev/null
+++ b/services/integration/bookingChangeCharges.js
@@ -0,0 +1,115 @@
+'use strict';
+
+const moment = require('moment-timezone');
+const db = require('../../models/index');
+
+const { UI_TIMEZONE, BOOKING_CHANGE_PERCENTAGE_CHARGE, incidentType } = require('../../constants/constants');
+
+const bulkWriteBookingChangeIncidents = (incidents) => {
+ return new Promise((resolve, reject) => {
+ const asyncJobs = [];
+ incidents.forEach((incident) => {
+ asyncJobs.push(db.bookingChangeIncident.findOrCreate({where: incident, defaults: incident}));
+ });
+
+ Promise.all(asyncJobs)
+ .then(() => {
+ resolve();
+ })
+ .catch((error) => reject(error));
+ });
+};
+
+const chargeBookingChanges = (changes) => {
+ return new Promise((resolve, reject) => {
+ if (Array.isArray(changes)){
+ const incidents = [];
+ const errors = [];
+ changes.forEach((change) => {
+ const { oldReservation, newReservation } = change;
+ if (oldReservation && newReservation){
+ const oldStart = oldReservation.start ? moment.utc(oldReservation.start) : null;
+ const oldEnd = oldReservation.end ? moment.utc(oldReservation.end) : null;
+
+ const newStart = newReservation.start ? moment.utc(newReservation.start) : null;
+ const newEnd = newReservation.end ? moment.utc(newReservation.end) : null;
+
+ const reservationTimezone = newReservation.timezone ? newReservation.timezone : UI_TIMEZONE;
+ const reservationHourlyRate = newReservation.hourlyRate ? newReservation.hourlyRate : undefined;
+
+ if (oldStart && oldEnd && newStart && newEnd && reservationHourlyRate){
+ const oldReservationLength = oldEnd.diff(oldStart, 'hours', true);
+ const newReservationLength = newEnd.diff(newStart, 'hours', true);
+
+ const differenceFromNow = oldStart.diff(moment.utc(), 'hours');
+
+ if (differenceFromNow && (differenceFromNow < 24)){
+ // Changed reservation that was within 24hrs from now
+
+ // Check if new reservation is on same day
+ const sameDay = oldStart.tz(reservationTimezone).isSame(newStart.tz(reservationTimezone), 'day');
+
+ const { reservationId, memberId, resourceId, hourlyRate } = newReservation;
+
+ if (sameDay){
+ // Reservation moved in same day
+ // Check if member shortened the reservation
+
+ if (newReservationLength < oldReservationLength){
+
+ const differenceInLength = oldReservationLength - newReservationLength;
+ const chargeFee = differenceInLength*hourlyRate*BOOKING_CHANGE_PERCENTAGE_CHARGE/100;
+
+ const incident = {
+ reservationId,
+ memberId,
+ resourceId,
+ oldBookingStart: oldReservation.start,
+ oldBookingEnd: oldReservation.end,
+ newBookingStart: newReservation.start,
+ newBookingEnd: newReservation.end,
+ incidentType: incidentType.BOOKING_SHORTENED,
+ chargeFee,
+ };
+
+ incidents.push(incident);
+ }
+ }else{
+ // Reservation moved to another day
+ // Add cancellation charge
+ const chargeFee = oldReservationLength*hourlyRate*BOOKING_CHANGE_PERCENTAGE_CHARGE/100;
+
+ const incident = {
+ reservationId,
+ memberId,
+ resourceId,
+ oldBookingStart: oldReservation.start,
+ oldBookingEnd: oldReservation.end,
+ newBookingStart: newReservation.start,
+ newBookingEnd: newReservation.end,
+ incidentType: incidentType.BOOKING_MOVED_TO_ANOTHER_DAY,
+ chargeFee,
+ };
+
+ incidents.push(incident);
+ }
+ }
+ }else{
+ errors.push(change);
+ }
+ }
+ });
+ if (errors.length > 0){
+ console.log('There were some errors with incomplete bookings : ');
+ console.log(errors);
+ }
+ resolve(bulkWriteBookingChangeIncidents(incidents));
+ }else{
+ reject('Input argument is not an array !');
+ }
+ });
+};
+
+module.exports = {
+ chargeBookingChanges,
+};
diff --git a/services/integration/reports.js b/services/integration/reports.js
index ed05c6f..07c6f02 100644
--- a/services/integration/reports.js
+++ b/services/integration/reports.js
@@ -111,6 +111,45 @@ const getUnscheduledIncidents = (startDate, endDate, memberId) => {
});
};
+const getBookingChangeIncidents = (startDate, endDate, memberId) => {
+ const attributes = [
+ 'id',
+ 'reservationId',
+ 'memberId',
+ 'resourceId',
+ 'oldBookingStart',
+ 'oldBookingEnd',
+ 'newBookingStart',
+ 'newBookingEnd',
+ 'incidentType',
+ 'chargeFee',
+ 'createdAt'
+ ];
+
+ const filters = {};
+
+ if (startDate && endDate) {
+ filters.createdAt = {
+ [Op.and]: {
+ [Op.gte]: startDate.toISOString(),
+ [Op.lte]: endDate.toISOString(),
+ }
+ }
+ }
+
+ if (memberId){
+ filters.memberId = memberId;
+ }
+
+ return db.bookingChangeIncident.findAll({
+ attributes,
+ where: filters,
+ sort: [
+ ['createdAt', 'ASC']
+ ]
+ });
+};
+
const formatTime = (timestamp) => {
const momentObject = moment.tz(timestamp, UI_TIMEZONE);
if (momentObject.isValid()){
@@ -120,13 +159,13 @@ const formatTime = (timestamp) => {
}
};
-const getAllDoorLockIncidents = (dateRange, memberId) => {
+const getAllIncidents = (dateRange, memberId) => {
return new Promise ((resolve, reject) => {
let startDate, endDate;
if (dateRange.startDate && dateRange.endDate){
- startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE);
- endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE);
+ startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).startOf('day');
+ endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).endOf('day');
if (!startDate.isValid() || !endDate.isValid() || endDate.isBefore(startDate)){
reject(integrationServiceErrors.INVALID_DATE_RANGE);
@@ -134,7 +173,14 @@ const getAllDoorLockIncidents = (dateRange, memberId) => {
}
}
- const dataFetchJobs = [fetchAllMembers(), fetchOffices(), fetchResources(), getUnlockedIncidents(startDate, endDate, memberId), getUnscheduledIncidents(startDate, endDate, memberId)];
+ const dataFetchJobs = [
+ fetchAllMembers(),
+ fetchOffices(),
+ fetchResources(),
+ getUnlockedIncidents(startDate, endDate, memberId),
+ getUnscheduledIncidents(startDate, endDate, memberId),
+ getBookingChangeIncidents(startDate, endDate, memberId)
+ ];
Promise.all(dataFetchJobs)
.then((data) => {
@@ -143,6 +189,7 @@ const getAllDoorLockIncidents = (dateRange, memberId) => {
const resources = data[2];
const unlockedIncidents = data[3];
const unscheduledIncidents = data[4];
+ const bookingChangeIncidents = data[5];
const membersMap = {};
const officesMap = {};
@@ -200,6 +247,39 @@ const getAllDoorLockIncidents = (dateRange, memberId) => {
});
});
+ bookingChangeIncidents.forEach((bookingChangeIncident) => {
+ const {
+ id,
+ memberId,
+ resourceId,
+ oldBookingStart,
+ oldBookingEnd,
+ newBookingStart,
+ newBookingEnd,
+ incidentType,
+ chargeFee,
+ createdAt,
+ } = bookingChangeIncident;
+ const memberName = membersMap[memberId].name;
+ const resource = resourcesMap[resourceId];
+ const resourceName = resource.resourceName;
+ const officeName = officesMap[resource.officeId].officeName;
+ allIncidents.push({
+ incidentId: id,
+ memberId,
+ memberName,
+ resourceName,
+ officeName,
+ oldBookingStart: formatTime(oldBookingStart),
+ oldBookingEnd: formatTime(oldBookingEnd),
+ newBookingStart: formatTime(newBookingStart),
+ newBookingEnd: formatTime(newBookingEnd),
+ incidentType,
+ totalChargeFee: chargeFee,
+ incidentTimestamp: formatTime(createdAt),
+ });
+ });
+
resolve(allIncidents);
})
.catch((error) => reject(error));
@@ -209,5 +289,5 @@ const getAllDoorLockIncidents = (dateRange, memberId) => {
module.exports = {
getUnlockedIncidents,
getUnscheduledIncidents,
- getAllDoorLockIncidents,
+ getAllIncidents,
};
diff --git a/services/officeRnD/bookings.js b/services/officeRnD/bookings.js
index c31da33..3a63a6c 100644
--- a/services/officeRnD/bookings.js
+++ b/services/officeRnD/bookings.js
@@ -15,6 +15,10 @@ const fetchAllBookings = () => {
const bookingData = result && result.data ? result.data : [];
bookingData.forEach((fullBookingEntry) => {
+ const fees = fullBookingEntry && fullBookingEntry.fees ? fullBookingEntry.fees : [];
+ const firstFee = fees.length > 0 && fees[0].fee ? fees[0].fee : undefined;
+ const hourlyRate = firstFee && firstFee.price ? firstFee.price : undefined;
+
cleanedBookingReservations.push({
reservationId: fullBookingEntry['_id'],
memberId: fullBookingEntry.member,
@@ -24,6 +28,7 @@ const fetchAllBookings = () => {
end: fullBookingEntry.end.dateTime,
timezone: fullBookingEntry.timezone,
canceled: fullBookingEntry.canceled || false,
+ hourlyRate,
});
});
resolve(cleanedBookingReservations);
@@ -145,7 +150,77 @@ const getFirstReservationInBlock = (reservation) => {
};
const writeBookingReservation = (bookingReservation) => {
- return db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}});
+ const { reservationId, memberId, officeId, resourceId, start, end, timezone, canceled, hourlyRate } = bookingReservation;
+ const bookingReservationForDB = {
+ reservationId,
+ memberId,
+ officeId,
+ resourceId,
+ start,
+ end,
+ timezone,
+ canceled,
+ hourlyRate,
+ };
+ return db.bookingReservation.findOrCreate({where: {...bookingReservationForDB}, defaults: {...bookingReservationForDB}});
+};
+
+const bulkWriteReservationsWithChangesTracking = (reservations) => {
+ return new Promise ((resolve, reject) => {
+ const changes = [];
+ const asyncJobs = [];
+
+ db.bookingReservation.addHook('beforeUpdate', 'updateHook', (instance) => {
+ const changedKeys = instance.changed();
+ const previous = instance.previous();
+
+ const lookupKeys = ['start', 'end'];
+
+ let realChange = false;
+ lookupKeys.forEach((key) => {
+ if ((changedKeys.indexOf(key) !== -1) &&
+ (JSON.stringify(previous[key]) !== JSON.stringify(instance[key]))){
+ realChange = true;
+ }
+ });
+
+ if (realChange){
+ changes.push({
+ oldReservation: previous,
+ newReservation: instance.get(),
+ });
+ }
+ });
+
+ reservations.forEach((reservation) => {
+ asyncJobs.push(
+ db.bookingReservation.update(reservation, {
+ where: {
+ reservationId: reservation.reservationId,
+ },
+ returning: true,
+ individualHooks: true,
+ })
+ .then(([updateCount, updatedInstances]) => {
+ if (updateCount === 0){
+ db.bookingReservation.upsert(reservation);
+ }
+ })
+ .catch((error) => {
+ console.log('Error updating');
+ console.log(error);
+ reject(error);
+ })
+ );
+ });
+
+ Promise.all(asyncJobs)
+ .then(() => {
+ db.bookingReservation.removeHook('updateHook');
+ resolve(changes);
+ })
+ .catch((error) => reject(error));
+ });
};
module.exports = {
@@ -154,5 +229,6 @@ module.exports = {
getAllFinishedBookings,
getFirstNextBooking,
getFirstPreviousBooking,
- getFirstReservationInBlock
+ getFirstReservationInBlock,
+ bulkWriteReservationsWithChangesTracking,
};