diff --git a/client/src/components/DateRangePicker/index.js b/client/src/components/DateRangePicker/index.js index cb2d2c4..304ed83 100644 --- a/client/src/components/DateRangePicker/index.js +++ b/client/src/components/DateRangePicker/index.js @@ -81,7 +81,7 @@ class DateRangePicker extends Component { const endDateValue = endDate.format(defaultDateFormat); const buttonRender = ( - + { inlineButton && } {buttonLabel} diff --git a/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js b/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js index 98e778a..fbdfb3e 100644 --- a/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js +++ b/client/src/components/MemberIncidentsTables/components/SingleIncidentsTable.js @@ -70,6 +70,19 @@ const SingleIncidentsTable = props => { urlValue = `/practice-summary-report/${memberId}`; cellValue = props.value; break; + case 'resourceName': + if (props.row['_original'].resourceName){ + cellValue = props.row['_original'].resourceName || '---'; + }else{ + const oldResourceName = props.row['_original'].oldResourceName || '---'; + const newResourceName = props.row['_original'].newResourceName || '---'; + if (oldResourceName !== newResourceName){ + cellValue = `${oldResourceName}\n${newResourceName}`; + }else{ + cellValue = oldResourceName; + } + } + break; case 'reservation': const bookingStart = props.row['_original'].bookingStart; const bookingEnd = props.row['_original'].bookingEnd; @@ -116,8 +129,8 @@ const SingleIncidentsTable = props => { cellValue = `${oldBookingStart}\n${oldBookingEnd}`; break; case 'newReservation': - const newBookingStart = props.row['_original'].newBookingStart; - const newBookingEnd = props.row['_original'].newBookingEnd; + const newBookingStart = props.row['_original'].newBookingStart || '---'; + const newBookingEnd = props.row['_original'].newBookingEnd || '---'; cellValue = `${newBookingStart}\n${newBookingEnd}`; break; default: diff --git a/client/src/components/MemberIncidentsTables/index.js b/client/src/components/MemberIncidentsTables/index.js index ad6706a..45ec9d1 100644 --- a/client/src/components/MemberIncidentsTables/index.js +++ b/client/src/components/MemberIncidentsTables/index.js @@ -3,7 +3,7 @@ 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, BOOKING_MOVED_TO_ANOTHER_DAY, BOOKING_SHORTENED, + UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE, BOOKING_MOVED_TO_ANOTHER_DAY, BOOKING_SHORTENED, BOOKING_CANCELED_LATE, incidentTableTypes } from '../../constants/enums'; @@ -29,6 +29,7 @@ export default function MemberIncidentsTables (props) { break; case BOOKING_MOVED_TO_ANOTHER_DAY: case BOOKING_SHORTENED: + case BOOKING_CANCELED_LATE: bookingChangeIncidents.push(incident); break; default: diff --git a/client/src/constants/enums.js b/client/src/constants/enums.js index 6a5c48b..cfcd675 100644 --- a/client/src/constants/enums.js +++ b/client/src/constants/enums.js @@ -6,6 +6,7 @@ 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 BOOKING_CANCELED_LATE = 9; export const incidentDescriptions = {}; incidentDescriptions[UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION] = 'User left door unlocked'; @@ -15,6 +16,7 @@ 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'; +incidentDescriptions[BOOKING_CANCELED_LATE] = 'Reservation canceled late'; export const incidentLevelDescriptions = { UNLOCKED_0: 'First month', diff --git a/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js b/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js index c6e6af0..d19e4c3 100644 --- a/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js +++ b/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js @@ -5,7 +5,7 @@ import { UNSCHEDULED_INCIDENT_BEFORE_RESERVATION, UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION, UNSCHEDULED_INCIDENT_AFTER_RESERVATION, UNSCHEDULED_INCIDENT_STANDALONE, UNLOCKED_INCIDENT_STANDALONE, - BOOKING_MOVED_TO_ANOTHER_DAY, BOOKING_SHORTENED, + BOOKING_MOVED_TO_ANOTHER_DAY, BOOKING_SHORTENED, BOOKING_CANCELED_LATE, } from '../../../constants/enums'; const MemberSummary = props => { @@ -29,6 +29,7 @@ const MemberSummary = props => { break; case BOOKING_MOVED_TO_ANOTHER_DAY: case BOOKING_SHORTENED: + case BOOKING_CANCELED_LATE: totalBookingChangeFees += parseFloat(incident.totalChargeFee); break; default: diff --git a/constants/constants.js b/constants/constants.js index 9ccad10..03443e5 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -68,6 +68,7 @@ const incidentType = { UNSCHEDULED_INCIDENT_STANDALONE: 6, BOOKING_MOVED_TO_ANOTHER_DAY: 7, BOOKING_SHORTENED: 8, + BOOKING_CANCELED_LATE: 9, }; const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles'; diff --git a/cronServices/checkBookingChanges.js b/cronServices/checkBookingChanges.js index 00451a3..aeae234 100644 --- a/cronServices/checkBookingChanges.js +++ b/cronServices/checkBookingChanges.js @@ -4,18 +4,26 @@ require('dotenv').config(); const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../services/officeRnD/bookings'); const { chargeBookingChanges } = require('../services/integration/bookingChangeCharges'); +const { bulkWriteChanges } = require('../services/integration/bookingChangeLog'); const checkBookingChanges = () => { fetchAllBookings() .then((reservations) => { bulkWriteReservationsWithChangesTracking(reservations) .then((changes) => { - chargeBookingChanges(changes) + bulkWriteChanges(changes) .then(() => { - process.exit(); + chargeBookingChanges(changes) + .then(() => { + process.exit(); + }) + .catch((error) => { + console.log('Error creating charges ', error); + process.exit(); + }); }) .catch((error) => { - console.log('Error creating charges ', error); + console.log('Error bulk write booking reservation change log :', error); process.exit(); }); }) diff --git a/migrations/20190709120748-rename-resourceId-column-in-booking-change-incidents-table.js b/migrations/20190709120748-rename-resourceId-column-in-booking-change-incidents-table.js new file mode 100644 index 0000000..dc892da --- /dev/null +++ b/migrations/20190709120748-rename-resourceId-column-in-booking-change-incidents-table.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.renameColumn('bookingChangeIncidents', 'resourceId', 'oldResourceId'); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.renameColumn('bookingChangeIncidents', 'oldResourceId', 'resourceId'); + } +}; diff --git a/migrations/20190709121128-add-new-resourceId-column-in-booking-change-incidents-table.js b/migrations/20190709121128-add-new-resourceId-column-in-booking-change-incidents-table.js new file mode 100644 index 0000000..1b568b0 --- /dev/null +++ b/migrations/20190709121128-add-new-resourceId-column-in-booking-change-incidents-table.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.addColumn('bookingChangeIncidents', 'newResourceId', { + type: Sequelize.TEXT, + after: 'oldResourceId', + }); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.removeColumn('bookingChangeIncidents', 'newResourceId'); + } +}; diff --git a/migrations/20190711071404-add-booking-reservation-change-logs-table.js b/migrations/20190711071404-add-booking-reservation-change-logs-table.js new file mode 100644 index 0000000..b64e9a3 --- /dev/null +++ b/migrations/20190711071404-add-booking-reservation-change-logs-table.js @@ -0,0 +1,35 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('bookingReservationChangeLogs', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + reservationId: Sequelize.TEXT, + memberId: Sequelize.TEXT, + officeId: Sequelize.TEXT, + oldResourceId: Sequelize.TEXT, + newResourceId: Sequelize.TEXT, + oldStart: Sequelize.DATE, + oldEnd: Sequelize.DATE, + newStart: Sequelize.DATE, + newEnd: Sequelize.DATE, + canceled: Sequelize.BOOLEAN, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('bookingReservationChangeLogs'); + } +}; diff --git a/models/bookingChangeIncident.js b/models/bookingChangeIncident.js index 1a91f1e..a5e9a75 100644 --- a/models/bookingChangeIncident.js +++ b/models/bookingChangeIncident.js @@ -4,7 +4,8 @@ module.exports = (sequelize, DataTypes) => { const bookingChangeIncident = sequelize.define('bookingChangeIncident', { reservationId: DataTypes.TEXT, memberId: DataTypes.TEXT, - resourceId: DataTypes.TEXT, + oldResourceId: DataTypes.TEXT, + newResourceId: DataTypes.TEXT, oldBookingStart: DataTypes.DATE, oldBookingEnd: DataTypes.DATE, newBookingStart: DataTypes.DATE, diff --git a/models/bookingReservationChangeLog.js b/models/bookingReservationChangeLog.js new file mode 100644 index 0000000..b26866e --- /dev/null +++ b/models/bookingReservationChangeLog.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const bookingReservationChangeLog = sequelize.define('bookingReservationChangeLog', { + reservationId: DataTypes.TEXT, + memberId: DataTypes.TEXT, + officeId: DataTypes.TEXT, + oldResourceId: DataTypes.TEXT, + newResourceId: DataTypes.TEXT, + oldStart: DataTypes.DATE, + oldEnd: DataTypes.DATE, + newStart: DataTypes.DATE, + newEnd: DataTypes.DATE, + canceled: DataTypes.BOOLEAN, + }, {}); + bookingReservationChangeLog.associate = function(models) { + // associations can be defined here + }; + return bookingReservationChangeLog; +}; diff --git a/services/integration/bookingChangeCharges.js b/services/integration/bookingChangeCharges.js index 13d47b2..5163d74 100644 --- a/services/integration/bookingChangeCharges.js +++ b/services/integration/bookingChangeCharges.js @@ -24,10 +24,10 @@ 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 oldResourceId = oldReservation.resourceId; const oldStart = oldReservation.start ? moment.utc(oldReservation.start) : null; const oldEnd = oldReservation.end ? moment.utc(oldReservation.end) : null; @@ -35,7 +35,8 @@ const chargeBookingChanges = (changes) => { const newEnd = newReservation.end ? moment.utc(newReservation.end) : null; const reservationTimezone = newReservation.timezone ? newReservation.timezone : UI_TIMEZONE; - const reservationHourlyRate = newReservation.hourlyRate ? newReservation.hourlyRate : undefined; + const reservationHourlyRate = oldReservation.hourlyRate ? oldReservation.hourlyRate : undefined; + const canceled = newReservation.canceled; if (oldStart && oldEnd && newStart && newEnd && reservationHourlyRate){ const oldReservationLength = oldEnd.diff(oldStart, 'hours', true); @@ -43,66 +44,78 @@ const chargeBookingChanges = (changes) => { const differenceFromNow = oldStart.diff(moment.utc(), 'hours'); - if (differenceFromNow && (differenceFromNow < 24)){ + if (differenceFromNow < 24){ // Changed reservation that was within 24hrs from now + const { reservationId, memberId, resourceId } = newReservation; - // Check if new reservation is on same day - const sameDay = oldStart.tz(reservationTimezone).isSame(newStart.tz(reservationTimezone), 'day'); + if (!canceled) { + // 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 (sameDay){ - // Reservation moved in same day - // Check if member shortened the reservation + if (newReservationLength < oldReservationLength) { + const differenceInLength = oldReservationLength - newReservationLength; + const chargeFee = differenceInLength * reservationHourlyRate * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100; - if (newReservationLength < oldReservationLength){ + const incident = { + reservationId, + memberId, + oldResourceId: oldResourceId || resourceId, + newResourceId: resourceId, + oldBookingStart: oldReservation.start, + oldBookingEnd: oldReservation.end, + newBookingStart: newReservation.start, + newBookingEnd: newReservation.end, + incidentType: incidentType.BOOKING_SHORTENED, + chargeFee, + }; - const differenceInLength = oldReservationLength - newReservationLength; - const chargeFee = differenceInLength*hourlyRate*BOOKING_CHANGE_PERCENTAGE_CHARGE/100; + incidents.push(incident); + } + } else { + // Reservation moved to another day + // Add cancellation charge + const chargeFee = oldReservationLength * reservationHourlyRate * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100; const incident = { reservationId, memberId, - resourceId, + oldResourceId: oldResourceId || resourceId, + newResourceId: resourceId, oldBookingStart: oldReservation.start, oldBookingEnd: oldReservation.end, newBookingStart: newReservation.start, newBookingEnd: newReservation.end, - incidentType: incidentType.BOOKING_SHORTENED, + incidentType: incidentType.BOOKING_MOVED_TO_ANOTHER_DAY, chargeFee, }; incidents.push(incident); } }else{ - // Reservation moved to another day - // Add cancellation charge - const chargeFee = oldReservationLength*hourlyRate*BOOKING_CHANGE_PERCENTAGE_CHARGE/100; - + const chargeFee = 2 * reservationHourlyRate * oldReservationLength * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100; const incident = { reservationId, memberId, - resourceId, + oldResourceId: oldResourceId || resourceId, + newResourceId: null, oldBookingStart: oldReservation.start, oldBookingEnd: oldReservation.end, - newBookingStart: newReservation.start, - newBookingEnd: newReservation.end, - incidentType: incidentType.BOOKING_MOVED_TO_ANOTHER_DAY, + newBookingStart: null, + newBookingEnd: null, + incidentType: incidentType.BOOKING_CANCELED_LATE, 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 !'); diff --git a/services/integration/bookingChangeLog.js b/services/integration/bookingChangeLog.js new file mode 100644 index 0000000..72dfeeb --- /dev/null +++ b/services/integration/bookingChangeLog.js @@ -0,0 +1,48 @@ +'use strict'; + +const db = require('../../models/index'); + +const bulkWriteChanges = ((changes) => { + const changeLogsForDB = []; + + changes.forEach((change) => { + const { oldReservation, newReservation } = change; + const { reservationId, memberId, officeId, resourceId, start, end, canceled } = newReservation; + + const logEntry = { + reservationId: oldReservation.reservationId || reservationId, + memberId: oldReservation.memberId || memberId, + officeId: oldReservation.officeId || officeId, + oldResourceId: oldReservation.resourceId || resourceId, + newResourceId: resourceId, + oldStart: oldReservation.start || start, + newStart: start, + oldEnd: oldReservation.end || end, + newEnd: end, + canceled, + }; + + if (!oldReservation.start && !oldReservation.end && !oldReservation.resourceId){ + // new reservation + logEntry.oldResourceId = null; + logEntry.oldStart = null; + logEntry.oldEnd = null; + } + + if (newReservation.canceled){ + logEntry.newResourceId = null; + logEntry.newStart = null; + logEntry.newEnd = null; + } + + changeLogsForDB.push(logEntry); + }); + + return db.bookingReservationChangeLog.bulkCreate(changeLogsForDB); + // console.log(changeLogsForDB); + // return new Promise((resolve) => resolve()); +}); + +module.exports = { + bulkWriteChanges, +}; diff --git a/services/integration/reports.js b/services/integration/reports.js index 07c6f02..4bd54ca 100644 --- a/services/integration/reports.js +++ b/services/integration/reports.js @@ -116,7 +116,8 @@ const getBookingChangeIncidents = (startDate, endDate, memberId) => { 'id', 'reservationId', 'memberId', - 'resourceId', + 'oldResourceId', + 'newResourceId', 'oldBookingStart', 'oldBookingEnd', 'newBookingStart', @@ -251,7 +252,8 @@ const getAllIncidents = (dateRange, memberId) => { const { id, memberId, - resourceId, + oldResourceId, + newResourceId, oldBookingStart, oldBookingEnd, newBookingStart, @@ -261,14 +263,17 @@ const getAllIncidents = (dateRange, memberId) => { createdAt, } = bookingChangeIncident; const memberName = membersMap[memberId].name; - const resource = resourcesMap[resourceId]; - const resourceName = resource.resourceName; - const officeName = officesMap[resource.officeId].officeName; + const oldResource = resourcesMap[oldResourceId]; + const newResource = newResourceId ? resourcesMap[newResourceId] : null; + const oldResourceName = oldResource.resourceName; + const newResourceName = newResource ? newResource.resourceName : null; + const officeName = officesMap[oldResource.officeId].officeName; allIncidents.push({ incidentId: id, memberId, memberName, - resourceName, + oldResourceName, + newResourceName, officeName, oldBookingStart: formatTime(oldBookingStart), oldBookingEnd: formatTime(oldBookingEnd), diff --git a/services/officeRnD/bookings.js b/services/officeRnD/bookings.js index 3a63a6c..d63d991 100644 --- a/services/officeRnD/bookings.js +++ b/services/officeRnD/bookings.js @@ -174,7 +174,7 @@ const bulkWriteReservationsWithChangesTracking = (reservations) => { const changedKeys = instance.changed(); const previous = instance.previous(); - const lookupKeys = ['start', 'end']; + const lookupKeys = ['start', 'end', 'resourceId', 'canceled']; let realChange = false; lookupKeys.forEach((key) => { @@ -184,6 +184,8 @@ const bulkWriteReservationsWithChangesTracking = (reservations) => { } }); + instance.setDataValue('hourlyRate', previous.hourlyRate); + if (realChange){ changes.push({ oldReservation: previous, @@ -193,25 +195,41 @@ const bulkWriteReservationsWithChangesTracking = (reservations) => { }); 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); - } + const asyncReservationUpdate = () => { + return new Promise((resolve, reject) => { + db.bookingReservation.update(reservation, { + where: { + reservationId: reservation.reservationId, + }, + returning: true, + individualHooks: true, }) - .catch((error) => { - console.log('Error updating'); - console.log(error); - reject(error); - }) - ); + .then(([updateCount, updatedInstances]) => { + if (updateCount === 0){ + const oldReservation = { + start: null, + end: null, + resourceId: null, + }; + + changes.push({ + oldReservation, + newReservation: reservation, + }); + resolve(db.bookingReservation.upsert(reservation)); + }else{ + resolve(); + } + }) + .catch((error) => { + console.log('Error updating'); + console.log(error); + reject(error); + }) + }); + }; + + asyncJobs.push(asyncReservationUpdate()); }); Promise.all(asyncJobs)