498 lines
23 KiB
JavaScript
498 lines
23 KiB
JavaScript
'use strict';
|
|
|
|
const moment = require('moment-timezone');
|
|
|
|
const { getAllIncidents } = require('./reports');
|
|
const { getAllBookingsForMembersInDateRange } = 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 { fetchAllMembershipsForMemberIds } = require('../officeRnD/memberships');
|
|
const { discounts, DISCOUNT_PLANS, officeRnDAPIErrors } = require('../../constants/constants');
|
|
|
|
const createFeeFromIncident = (incident) => {
|
|
try {
|
|
const {
|
|
memberId,
|
|
officeId,
|
|
officeName,
|
|
officeSlug,
|
|
resourceName,
|
|
oldResourceName,
|
|
newResourceName,
|
|
bookingStartRaw,
|
|
bookingEndRaw,
|
|
oldBookingStartRaw,
|
|
oldBookingEndRaw,
|
|
newBookingStartRaw,
|
|
newBookingEndRaw,
|
|
unlockTimestampRaw,
|
|
lockTimestampRaw,
|
|
incidentLevel,
|
|
timeIntervalsToCharge,
|
|
incidentPrice,
|
|
chargePrice,
|
|
totalChargeFee,
|
|
incidentTimestampRaw
|
|
} = incident;
|
|
const incidentTypeNumber = incident.incidentType;
|
|
|
|
let incidentExplanation = incidentTypeExplanations[incidentTypeNumber];
|
|
|
|
let date = '';
|
|
let price = 0;
|
|
let quantity = 0;
|
|
let bookingTimeExplanation = '';
|
|
let incidentTimeExplanation = '';
|
|
|
|
let spacing = '';
|
|
|
|
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:
|
|
spacing = ' ';
|
|
roomExplanation = resourceName || 'Unknown';
|
|
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD');
|
|
bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}`;
|
|
incidentExplanation += `, ${unlockedIncidentLevelsPrices[incidentLevel].description}, ${incidentTimeExplanation}`;
|
|
|
|
date = bookingStartMoment.clone().startOf('day').format();
|
|
|
|
price = +incidentPrice.toFixed(2);
|
|
quantity = 1.00;
|
|
break;
|
|
case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
|
|
spacing = ' ';
|
|
roomExplanation = resourceName || 'Unknown';
|
|
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD');
|
|
bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}`;
|
|
incidentExplanation += `, ${incidentTimeExplanation}`;
|
|
|
|
date = bookingStartMoment.clone().startOf('day').format();
|
|
|
|
price = +chargePrice.toFixed(2);
|
|
quantity = +timeIntervalsToCharge.toFixed(2);
|
|
break;
|
|
case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
|
|
spacing = ' ';
|
|
roomExplanation = resourceName || 'Unknown';
|
|
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD');
|
|
bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `lock : ${lockMoment.clone().format('HH:mm')}`;
|
|
incidentExplanation += `, ${incidentTimeExplanation}`;
|
|
|
|
date = bookingStartMoment.clone().startOf('day').format();
|
|
|
|
price = +chargePrice.toFixed(2);
|
|
quantity = +timeIntervalsToCharge.toFixed(2);
|
|
break;
|
|
case incidentType.UNLOCKED_INCIDENT_STANDALONE:
|
|
spacing = ' ';
|
|
roomExplanation = resourceName || 'Unknown';
|
|
dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD');
|
|
bookingTimeExplanation = `[${unlockMoment.clone().format('HH:mm')} to ${lockMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}`;
|
|
incidentExplanation += `, ${unlockedIncidentLevelsPrices[incidentLevel].description}, ${incidentTimeExplanation}`;
|
|
|
|
date = unlockMoment.clone().startOf('day').format();
|
|
|
|
price = +incidentPrice.toFixed(2);
|
|
quantity = 1.00;
|
|
break;
|
|
case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
|
|
spacing = ' ';
|
|
roomExplanation = resourceName || 'Unknown';
|
|
dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD');
|
|
bookingTimeExplanation = `[${unlockMoment.clone().format('HH:mm')} to ${lockMoment.clone().format('HH:mm')}]`;
|
|
//incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}, lock : ${lockMoment.clone().format('HH:mm')}`;
|
|
|
|
date = unlockMoment.clone().startOf('day').format();
|
|
|
|
price = +chargePrice.toFixed(2);
|
|
quantity = +timeIntervalsToCharge.toFixed(2);
|
|
break;
|
|
case incidentType.BOOKING_MOVED_TO_ANOTHER_DAY:
|
|
spacing = ' ';
|
|
// if (oldResourceName !== newResourceName){
|
|
// roomExplanation = `${oldResourceName} -> ${newResourceName}`;
|
|
// }else{
|
|
// roomExplanation = oldResourceName;
|
|
// }
|
|
roomExplanation = newResourceName || 'Unknown';
|
|
|
|
// dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')} -> ${newBookingStartMoment.clone().format('ddd, MMM DD')}`;
|
|
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD')}`;
|
|
bookingTimeExplanation = `[${oldBookingStartMoment.clone().format('HH:mm')} to ${oldBookingEndMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `moved on : ${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}`;
|
|
incidentExplanation += `, ${incidentTimeExplanation}`;
|
|
|
|
date = incidentTimestampMoment.clone().startOf('day').format();
|
|
|
|
price = +totalChargeFee.toFixed(2);
|
|
quantity = 1.00;
|
|
break;
|
|
case incidentType.BOOKING_SHORTENED:
|
|
spacing = ' ';
|
|
// if (oldResourceName !== newResourceName){
|
|
// roomExplanation = `${oldResourceName} -> ${newResourceName}`;
|
|
// }else{
|
|
// roomExplanation = oldResourceName;
|
|
// }
|
|
roomExplanation = newResourceName || 'Unknown';
|
|
|
|
// dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')}`;
|
|
|
|
const originalBookingExplanation = `${oldBookingStartMoment.clone().format('HH:mm')} to ${oldBookingEndMoment.clone().format('HH:mm')}`;
|
|
dateExplanation = `${newBookingStartMoment.clone().format('MMM DD')}`;
|
|
bookingTimeExplanation = `[${newBookingStartMoment.clone().format('HH:mm')} to ${newBookingEndMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `reservation shortened from [${originalBookingExplanation}] on [${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}]`;
|
|
incidentExplanation = `${incidentTimeExplanation}`;
|
|
|
|
date = newBookingStartMoment.clone().utc(UI_TIMEZONE).startOf('day').format();
|
|
|
|
price = +totalChargeFee.toFixed(2);
|
|
quantity = 1.00;
|
|
break;
|
|
case incidentType.BOOKING_CANCELED_LATE:
|
|
spacing = ' ';
|
|
roomExplanation = oldResourceName || 'Unknown';
|
|
// dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')}`;
|
|
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD')}`;
|
|
bookingTimeExplanation = `[${oldBookingStartMoment.clone().format('HH:mm')} to ${oldBookingEndMoment.clone().format('HH:mm')}]`;
|
|
incidentTimeExplanation = `canceled on : ${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}`;
|
|
incidentExplanation += `, ${incidentTimeExplanation}`;
|
|
|
|
date = incidentTimestampMoment.clone().startOf('day').format();
|
|
|
|
price = +totalChargeFee.toFixed(2);
|
|
quantity = 1.00;
|
|
break;
|
|
}
|
|
|
|
const formattedName = `${officeSlug}, ${dateExplanation} ${bookingTimeExplanation}${spacing}${roomExplanation}, ${incidentExplanation}`;
|
|
|
|
return {
|
|
name: formattedName,
|
|
price,
|
|
quantity,
|
|
date,
|
|
member: memberId,
|
|
team: null,
|
|
office: officeId,
|
|
isPersonal: false,
|
|
}
|
|
}
|
|
catch (e) {
|
|
console.log("[Create Fee From Incident] Incident is incomplete, it will not be added");
|
|
console.log(e);
|
|
console.log(">> INCIDENT : ", incident);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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 officeSlug = officesMap[officeId].officeSlug || 'Unknown';
|
|
const resourceName = resourcesMap[resourceId].resourceName || 'Unknown';
|
|
|
|
const formattedDate = startMoment.clone().startOf('day').format('MMM DD');
|
|
const formattedStartTime = startMoment.format('HH:mm');
|
|
const formattedEndTime = endMoment.format('HH:mm');
|
|
|
|
const formattedName = `${officeSlug}, ${formattedDate} [${formattedStartTime} to ${formattedEndTime}] ${resourceName}`;
|
|
|
|
return {
|
|
name: formattedName,
|
|
price: +hourlyRate.toFixed(2),
|
|
quantity: +reservationLength.toFixed(2),
|
|
date: startMoment.startOf('day').toISOString(),
|
|
member: memberId,
|
|
team: null,
|
|
office: officeId,
|
|
isPersonal: false,
|
|
}
|
|
};
|
|
|
|
const createNegativeFeeForDiscount = (memberData, dateRange) => {
|
|
const { bookingData, member, membershipFees } = memberData;
|
|
const { totalBookedHours, totalChargedHours, totalBookingChargedFee } = bookingData;
|
|
const { memberId, officeId } = member;
|
|
|
|
let dateForDiscount = moment.utc().subtract(1, 'month').startOf('month').toISOString();
|
|
if (dateRange.startDate){
|
|
dateForDiscount = moment.utc(dateRange.startDate, DEFAULT_DATE_FORMAT).startOf('month').toISOString();
|
|
}
|
|
|
|
let membershipFeeForDiscount = 0;
|
|
membershipFees.forEach((membershipFee) => {
|
|
const {name, price} = membershipFee;
|
|
const cleanName = name.replace('[', '').replace(']', '').trim();
|
|
if (DISCOUNT_PLANS.indexOf(cleanName) !== -1){
|
|
membershipFeeForDiscount = price;
|
|
}
|
|
});
|
|
|
|
if (membershipFeeForDiscount === 0){
|
|
return null; //This member's plan is not eligible for a discount
|
|
}
|
|
|
|
const totalChargeFee = membershipFeeForDiscount + totalBookingChargedFee;
|
|
|
|
let discount = 0;
|
|
let discountPercentage = 0;
|
|
|
|
if (totalChargedHours >= discounts.LEVEL_2.hoursRequired){
|
|
discountPercentage = discounts.LEVEL_2.percentage;
|
|
const discountRate = discountPercentage / 100;
|
|
discount = totalChargeFee * discountRate;
|
|
}else if (totalChargedHours >= discounts.LEVEL_1.hoursRequired){
|
|
discountPercentage = discounts.LEVEL_1.percentage;
|
|
const discountRate = discountPercentage / 100;
|
|
discount = totalChargeFee * discountRate;
|
|
}else{
|
|
return null; //Not enough hours to earn a discount
|
|
}
|
|
|
|
const formattedName = `[Discount] Total booked : ${totalBookedHours.toFixed(2)} hrs, Total charged : ${totalChargedHours.toFixed(2)} hrs, Discount : ${discountPercentage} %`;
|
|
|
|
return {
|
|
name: formattedName,
|
|
price: -discount.toFixed(2),
|
|
quantity: 1,
|
|
date: dateForDiscount,
|
|
member: memberId,
|
|
team: null,
|
|
office: officeId,
|
|
isPersonal: false,
|
|
}
|
|
};
|
|
|
|
const getMembersFeesForDateRange = (dateRange, memberIds) => {
|
|
return new Promise((resolve, reject) => {
|
|
const collectData = [getAllIncidents(dateRange, memberIds), getAllBookingsForMembersInDateRange(dateRange, memberIds), getResourceMappings(), fetchAllMembers(), fetchAllMembershipsForMemberIds(memberIds)];
|
|
|
|
Promise.all(collectData)
|
|
.then((result) => {
|
|
const allIncidents = result[0];
|
|
const allBookings = result[1];
|
|
const resourceMappings = result[2];
|
|
const membersList = result[3];
|
|
const memberships = result[4];
|
|
|
|
const membershipsMap = {};
|
|
memberships.forEach((membership) => {
|
|
const { price, name, member } = membership;
|
|
if (!membershipsMap[member]) {
|
|
membershipsMap[member] = [{
|
|
price,
|
|
name,
|
|
}];
|
|
}else{
|
|
membershipsMap[member].push({
|
|
price,
|
|
name,
|
|
});
|
|
}
|
|
});
|
|
|
|
const memberIdTeamMappings = {};
|
|
const membersMap = {};
|
|
const oneMemberObject = {
|
|
totalBookedHours: 0,
|
|
totalChargedHours: 0,
|
|
totalBookingChargedFee: 0,
|
|
};
|
|
|
|
membersList.forEach((member) => {
|
|
membersMap[member.memberId] = {
|
|
member,
|
|
bookingData: Object.assign({}, oneMemberObject),
|
|
membershipFees: membershipsMap[member.memberId] || [],
|
|
};
|
|
|
|
memberIdTeamMappings[member.memberId] = member.teamId;
|
|
});
|
|
|
|
const memberIdsToUse = memberIds.length > 0 ? memberIds : Object.keys(membersMap);
|
|
|
|
const allFees = [];
|
|
|
|
allIncidents.forEach((incident) => {
|
|
const feeFromIncident = createFeeFromIncident(incident);
|
|
if (feeFromIncident){
|
|
allFees.push(feeFromIncident);
|
|
}
|
|
|
|
const incidentsValuableForDiscountCalculation = [
|
|
incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION,
|
|
incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION,
|
|
incidentType.UNSCHEDULED_INCIDENT_STANDALONE,
|
|
incidentType.BOOKING_SHORTENED,
|
|
incidentType.BOOKING_CANCELED_LATE
|
|
];
|
|
|
|
const incidentTypeNumber = incident.incidentType;
|
|
if (incidentsValuableForDiscountCalculation.indexOf(incidentTypeNumber) === -1){
|
|
return;
|
|
}
|
|
|
|
const {
|
|
memberId,
|
|
oldBookingStartRaw,
|
|
oldBookingEndRaw,
|
|
newBookingStartRaw,
|
|
newBookingEndRaw,
|
|
unlockTimestampRaw,
|
|
lockTimestampRaw,
|
|
bookingStartRaw,
|
|
bookingEndRaw,
|
|
totalChargeFee
|
|
} = incident;
|
|
|
|
let chargedBookingLength = 0;
|
|
|
|
switch (incidentTypeNumber){
|
|
case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
|
|
const unlockMoment = moment.utc(unlockTimestampRaw);
|
|
const bookingStartMoment =moment.utc(bookingStartRaw);
|
|
if (unlockMoment.isValid() && bookingStartMoment.isValid()){
|
|
chargedBookingLength = bookingStartMoment.diff(unlockMoment, 'hours', true);
|
|
}
|
|
break;
|
|
case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
|
|
const lockMoment = moment.utc(lockTimestampRaw);
|
|
const bookingEndMoment =moment.utc(bookingEndRaw);
|
|
if (lockMoment.isValid() && bookingEndMoment.isValid()){
|
|
chargedBookingLength = lockMoment.diff(bookingEndMoment, 'hours', true);
|
|
}
|
|
break;
|
|
case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
|
|
const unlockMomentStandalone = moment.utc(unlockTimestampRaw);
|
|
const lockMomentStandalone = moment.utc(lockTimestampRaw);
|
|
if (unlockMomentStandalone.isValid() && lockMomentStandalone.isValid()){
|
|
chargedBookingLength = lockMomentStandalone.diff(unlockMomentStandalone, 'hours', true);
|
|
}
|
|
break;
|
|
case incidentType.BOOKING_SHORTENED:
|
|
const oldBookingStartMoment = moment.utc(oldBookingStartRaw);
|
|
const oldBookingEndMoment = moment.utc(oldBookingEndRaw);
|
|
const newBookingStartMoment = moment.utc(newBookingStartRaw);
|
|
const newBookingEndMoment = moment.utc(newBookingEndRaw);
|
|
|
|
if (oldBookingStartMoment.isValid() && oldBookingEndMoment.isValid() && newBookingStartMoment.isValid() && newBookingEndMoment.isValid()){
|
|
const oldBookingLength = oldBookingEndMoment.diff(oldBookingStartMoment, 'hours', true);
|
|
const newBookingLength = newBookingEndMoment.diff(newBookingStartMoment, 'hours', true);
|
|
|
|
chargedBookingLength = Math.abs(oldBookingLength - newBookingLength);
|
|
}
|
|
break;
|
|
case incidentType.BOOKING_CANCELED_LATE:
|
|
const startMoment = moment.utc(oldBookingStartRaw);
|
|
const endMoment = moment.utc(oldBookingEndRaw);
|
|
|
|
if (startMoment.isValid() && endMoment.isValid()) {
|
|
chargedBookingLength = endMoment.diff(startMoment, 'hours', true);
|
|
|
|
// membersMap[memberId].bookingData.totalBookedHours += bookingLength;
|
|
// "booked hours" is counted in canceled booking section
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (membersMap[memberId] && membersMap[memberId].bookingData){
|
|
membersMap[memberId].bookingData.totalChargedHours += chargedBookingLength;
|
|
membersMap[memberId].bookingData.totalBookingChargedFee += totalChargeFee;
|
|
}
|
|
});
|
|
allBookings.forEach((booking) => {
|
|
const {memberId, start, end, timezone, hourlyRate, canceled } = booking.get();
|
|
const startMoment = moment.tz(start, timezone);
|
|
const endMoment = moment.tz(end, timezone);
|
|
|
|
const memberMappingExists = !!membersMap[memberId];
|
|
|
|
if (startMoment.isValid() && endMoment.isValid() && memberMappingExists) {
|
|
const bookingLength = endMoment.diff(startMoment, 'hours', true);
|
|
|
|
if (!membersMap[memberId].bookingData) {
|
|
membersMap[memberId].bookingData = Object.assign({}, oneMemberObject);
|
|
}
|
|
|
|
membersMap[memberId].bookingData.totalBookedHours += bookingLength;
|
|
|
|
if (!canceled){
|
|
membersMap[memberId].bookingData.totalChargedHours += bookingLength;
|
|
const bookingFee = bookingLength * hourlyRate;
|
|
membersMap[memberId].bookingData.totalBookingChargedFee += bookingFee;
|
|
|
|
allFees.push(createFeeFromBooking(booking, resourceMappings));
|
|
}
|
|
}else{
|
|
console.log('[Get Members Fees For Date Range] Start Moment / End Moment / Member Mapping is invalid for member : ', memberId);
|
|
}
|
|
});
|
|
|
|
//add discount
|
|
memberIdsToUse.forEach((memberId) => {
|
|
if (membersMap[memberId]){
|
|
const discountFee = createNegativeFeeForDiscount(membersMap[memberId], dateRange);
|
|
if (discountFee){
|
|
allFees.push(discountFee);
|
|
}
|
|
}else{
|
|
console.log('[Get Members Fees For Date Range] Member mapping unknown for member : ', memberId);
|
|
}
|
|
});
|
|
|
|
allFees.forEach((fee) => {
|
|
const { member } = fee;
|
|
const memberFromMapping = membersMap[member];
|
|
if (memberFromMapping && memberFromMapping.member){
|
|
const { teamId, name: memberName} = memberFromMapping.member;
|
|
|
|
fee.team = memberIdTeamMappings[member] || null;
|
|
if (teamId){
|
|
//if member is part of the company, add name to the fee description/name
|
|
fee.name = `${memberName}, ${fee.name}`;
|
|
}
|
|
}
|
|
});
|
|
|
|
resolve({allFees, memberships});
|
|
|
|
})
|
|
.catch((error) => {
|
|
console.log("[Get Members Fees For Date Range] Error fetching data : ",error);
|
|
reject(officeRnDAPIErrors.FAILED_TO_FETCH_DATA);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
getMembersFeesForDateRange,
|
|
};
|