Add cancelation charges

This commit is contained in:
Senad Uka
2019-07-08 20:37:14 +02:00
parent 1e1c61882f
commit 9d96ac4772
21 changed files with 575 additions and 64 deletions

View File

@@ -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 <NavLink to={urlValue}>{cellValue}</NavLink>
}else{
return <div style={{ textAlign: columnContentsAlignment }}>{cellValue}</div>
return <div style={{ textAlign: columnContentsAlignment, whiteSpace: 'pre' }}>{cellValue}</div>
}
}
});

View File

@@ -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 = (
<SingleIncidentsTable
loading={pendingIncidents}
incidents={bookingChangeIncidents}
openMemberSummaryOnMemberClick
hideMemberName={hideMemberName}
tableType={incidentTableTypes.BOOKING_CHANGE_INCIDENTS}
/>
);
@@ -63,7 +81,8 @@ export default function MemberIncidentsTables (props) {
},
{
key: 'reservation-modification-incidents',
title: {content: <Label color='blue' content={'Cancellation charges'}/>},
title: {content: <Label color='blue' content={'Booking change charges'}/>},
content: { content: bookingChangeIncidentsTable },
}
];

View File

@@ -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',
};

View File

@@ -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,
};

View File

@@ -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',
};

View File

@@ -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 &&
<Grid stackable>
<Grid.Row>
<Grid.Column width={3}>
<Grid.Column width={4}>
<p>Unscheduled incidents total :</p>
</Grid.Column>
<Grid.Column width={9}>
@@ -52,7 +59,7 @@ const MemberSummary = props => {
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={3}>
<Grid.Column width={4}>
<p>Unlocked incidents total :</p>
</Grid.Column>
<Grid.Column width={9}>
@@ -60,7 +67,15 @@ const MemberSummary = props => {
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={3}>
<Grid.Column width={4}>
<p>Booking change charges total :</p>
</Grid.Column>
<Grid.Column width={9}>
<p>{formattedBookingChangeFees}</p>
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={4}>
<p><b>Grand Total :</b></p>
</Grid.Column>
<Grid.Column width={9}>

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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();

View File

@@ -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

View File

@@ -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',
});
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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"

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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,
};