Files
old-crm-integration/services/integration/invoiceIntegration.js
2019-12-09 13:39:39 +01:00

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