'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 = `${newBookingStartMoment.clone().format('MMM DD')}`; bookingTimeExplanation = `[${newBookingStartMoment.clone().format('HH:mm')} to ${newBookingEndMoment.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 oldBookingDuration = oldBookingEndMoment.diff(oldBookingStartMoment, "minutes", false); const durationInHours = Math.floor(oldBookingDuration / 60); const durationInMinutes = Math.floor(oldBookingDuration % 60); let durationAsText = ''; if (durationInHours !== 0){ durationAsText += durationInHours + ' hour'; if (durationInHours === 1){ durationAsText += ' '; }else{ durationAsText += 's '; } } durationAsText += durationInMinutes + ' minute'; if (durationInMinutes > 1){ durationAsText += 's'; } dateExplanation = `${newBookingStartMoment.clone().format('MMM DD')}`; bookingTimeExplanation = `[${newBookingStartMoment.clone().format('HH:mm')} to ${newBookingEndMoment.clone().format('HH:mm')}]`; incidentTimeExplanation = `reservation shortened from ${durationAsText} 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_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, };