From 876297b6988ed28f2956cb9c88a6091983b4ebff Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 13 Aug 2019 15:36:14 +0200 Subject: [PATCH] add negative fee for discount --- constants/constants.js | 12 ++ environment.env | 5 + services/integration/bookingChangeCharges.js | 2 +- services/integration/invoiceIntegration.js | 132 +++++++++++++++++-- services/officeRnD/members.js | 1 + services/officeRnD/memberships.js | 27 ++++ 6 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 services/officeRnD/memberships.js diff --git a/constants/constants.js b/constants/constants.js index 2ec13a6..9eb47ab 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -107,6 +107,17 @@ const BOOKING_CHANGE_PERCENTAGE_CHARGE = parseInt(process.env.BOOKING_CHANGE_PER const CHARGE_BOOKING_CHANGE_UNDER_TIME = parseInt(process.env.CHARGE_BOOKING_CHANGE_UNDER_TIME) || 1430; const ALLOWED_BOOKING_CANCELLATION_TIME = parseInt(process.env.ALLOWED_BOOKING_CANCELLATION_TIME) || 30; +const discounts = { + LEVEL_1:{ + hoursRequired: parseInt(process.env.DISCOUNT_LEVEL_1_HOURS) || 10, + percentage: parseInt(process.env.DISCOUNT_LEVEL_1_PERCENTAGE) || 5, + }, + LEVEL_2:{ + hoursRequired: parseInt(process.env.DISCOUNT_LEVEL_2_HOURS) || 40, + percentage: parseInt(process.env.DISCOUNT_LEVEL_2_PERCENTAGE) || 10, + } +}; + module.exports = { VALID_CSV_HEADERS, USER_ENTRY_EVENT, @@ -127,4 +138,5 @@ module.exports = { BOOKING_CHANGE_PERCENTAGE_CHARGE, CHARGE_BOOKING_CHANGE_UNDER_TIME, ALLOWED_BOOKING_CANCELLATION_TIME, + discounts, }; diff --git a/environment.env b/environment.env index 2db303e..cd1c2a7 100644 --- a/environment.env +++ b/environment.env @@ -26,6 +26,11 @@ ALLOWED_BOOKING_CANCELLATION_TIME=Time from creation (in minutes) in which cance SEQUELIZE_LOGGING=0 - false, 1 - true (console logging) +DISCOUNT_LEVEL_1_HOURS=Hours requred to apply DISCOUNT_LEVEL_1_PERCENTAGE discount +DISCOUNT_LEVEL_1_PERCENTAGE=Discount to apply in percentage, if DISCOUNT_LEVEL_1_HOURS of billable hours is booked +DISCOUNT_LEVEL_2_HOURS=Hours requred to apply DISCOUNT_LEVEL_2_PERCENTAGE discount +DISCOUNT_LEVEL_2_PERCENTAGE=Discount to apply in percentage, if DISCOUNT_LEVEL_2_HOURS of billable hours is booked + #More about pool option : http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html DB_POOL_MAX_CONNECTIONS=Maximum number of connection in pool (ex. 18) DB_POOL_ACQUIRE=The maximum time, in milliseconds, that pool will try to get connection before throwing error (ex. 120000) diff --git a/services/integration/bookingChangeCharges.js b/services/integration/bookingChangeCharges.js index 9a29c28..a880225 100644 --- a/services/integration/bookingChangeCharges.js +++ b/services/integration/bookingChangeCharges.js @@ -142,7 +142,7 @@ const getChargedCanceledReservations = (reservationIds) => { incidentType: incidentType.BOOKING_CANCELED_LATE, }; - const attributes = ['memberId', 'oldBookingStart', 'oldBookingEnd']; + const attributes = ['memberId', 'oldBookingStart', 'oldBookingEnd', 'chargeFee']; return db.bookingChangeIncident.findAll({attributes, where: filters}); }; diff --git a/services/integration/invoiceIntegration.js b/services/integration/invoiceIntegration.js index a19d548..5247a67 100644 --- a/services/integration/invoiceIntegration.js +++ b/services/integration/invoiceIntegration.js @@ -3,11 +3,14 @@ const moment = require('moment-timezone'); const { getAllIncidents } = require('./reports'); -const { getActiveBookingsForMembersInDateRange } = require('./bookings'); +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 { fetchAllMembershipsAsMap } = require('../officeRnD/memberships'); +const { getChargedCanceledReservations } = require('../integration/bookingChangeCharges'); +const { discounts } = require('../../constants/constants'); const createFeeFromIncident = (incident) => { const { @@ -222,33 +225,138 @@ const createFeeFromBooking = (booking, resourceMappings) => { } }; +const createNegativeFeeForDiscount = (memberData) => { + const { bookingData, member, membershipFee } = memberData; + const { totalBookedHours, totalChargedHours, totalBookingChargedFee } = bookingData; + const { memberId, officeId } = member; + + const totalChargeFee = membershipFee + 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; + console.log(discountRate); + discount = totalChargeFee * discountRate; + }else{ + discountPercentage = 0; + discount = 0; + } + + 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: moment.tz(UI_TIMEZONE).format(), + member: memberId, + team: null, + office: officeId, + isPersonal: false, + } + +}; + const getMembersFeesForDateRange = (dateRange, memberIds) => { return new Promise((resolve, reject) => { - const collectData = [getAllIncidents(dateRange, memberIds), getActiveBookingsForMembersInDateRange(dateRange, memberIds), getResourceMappings(), fetchAllMembers()]; - + const collectData = [getAllIncidents(dateRange, memberIds), getAllBookingsForMembersInDateRange(dateRange, memberIds), getResourceMappings(), fetchAllMembers(), fetchAllMembershipsAsMap()]; Promise.all(collectData) .then((result) => { const allIncidents = result[0]; - const allActiveBookings = result[1]; + const allBookings = result[1]; const resourceMappings = result[2]; const membersList = result[3]; + const membershipsMap = result[4]; + + const membersMap = {}; + const oneMemberObject = { + totalBookedHours: 0, + totalChargedHours: 0, + totalBookingChargedFee: 0, + }; - const memberIdTeamMappings = {}; membersList.forEach((member) => { - memberIdTeamMappings[member.memberId] = member.teamId; + membersMap[member.memberId] = { + member, + bookingData: Object.assign({}, oneMemberObject), + membershipFee: membershipsMap[member.memberId] || 0, + }; }); - const allFees = []; + const reservationIdsForAdditionalData = []; - allIncidents.forEach((incident) => allFees.push(createFeeFromIncident(incident))); - allActiveBookings.forEach((booking) => allFees.push(createFeeFromBooking(booking, resourceMappings))); + const allActiveBookings = []; + allBookings.forEach((booking) => { + const {reservationId, memberId, start, end, timezone, canceled, hourlyRate} = booking.get(); + const startMoment = moment.tz(start, timezone); + const endMoment = moment.tz(end, timezone); - allFees.forEach((fee) => { - fee.team = memberIdTeamMappings[fee.member] || null; + if (startMoment.isValid() && endMoment.isValid()) { + const bookingLength = endMoment.diff(startMoment, 'hours', true); + + if (!membersMap[memberId] || !membersMap[memberId].bookingData) { + membersMap[memberId].bookingData = Object.assign({}, oneMemberObject); + } + + membersMap[memberId].bookingData.totalBookedHours += bookingLength; + + if (canceled) { + reservationIdsForAdditionalData.push(reservationId); + } else { + membersMap[memberId].bookingData.totalChargedHours += bookingLength; + + const bookingFee = bookingLength * hourlyRate; + membersMap[memberId].bookingData.totalBookingChargedFee += bookingFee; + } + } }); - resolve(allFees); + getChargedCanceledReservations(reservationIdsForAdditionalData) + .then((incidents) => { + incidents.forEach((incident) => { + const {memberId, oldBookingStart, oldBookingEnd, chargeFee} = incident.get(); + + const startMoment = moment.tz(oldBookingStart, UI_TIMEZONE); + const endMoment = moment.tz(oldBookingEnd, UI_TIMEZONE); + + if (startMoment.isValid() && endMoment.isValid()) { + const bookingLength = endMoment.diff(startMoment, 'hours', true); + + membersMap[memberId].bookingData.totalChargedHours += bookingLength; + membersMap[memberId].bookingData.totalBookingChargedFee += chargeFee; + } + }); + + const memberIdTeamMappings = {}; + membersList.forEach((member) => { + memberIdTeamMappings[member.memberId] = member.teamId; + }); + + const allFees = []; + + allIncidents.forEach((incident) => allFees.push(createFeeFromIncident(incident))); + allActiveBookings.forEach((booking) => allFees.push(createFeeFromBooking(booking, resourceMappings))); + + //add discount + memberIds.forEach((memberId) => { + allFees.push(createNegativeFeeForDiscount(membersMap[memberId])); + }); + + allFees.forEach((fee) => { + fee.team = memberIdTeamMappings[fee.member] || null; + }); + + resolve(allFees); + }) + .catch((error) => reject(error)); }) .catch((error) => { console.log(error); diff --git a/services/officeRnD/members.js b/services/officeRnD/members.js index d1b9232..75945b6 100644 --- a/services/officeRnD/members.js +++ b/services/officeRnD/members.js @@ -14,6 +14,7 @@ const fetchAllMembers = () => { memberId: member['_id'], teamId: member.team, active: member.status === 'active', + officeId: member.office, }); }); cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 ); diff --git a/services/officeRnD/memberships.js b/services/officeRnD/memberships.js new file mode 100644 index 0000000..14c6d2b --- /dev/null +++ b/services/officeRnD/memberships.js @@ -0,0 +1,27 @@ +'use strict'; + +const { API } = require('../../helpers/api'); + +const fetchAllMembershipsAsMap = () => { + return new Promise((resolve, reject) => { + API.get('/memberships') + .then((result) => { + const membershipsMap = {}; + const memberships = result.data || []; + memberships.forEach((membership) => { + const { price, member } = membership; + if (!membershipsMap[member]) { + membershipsMap[member] = price; + } + }); + resolve(membershipsMap); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +module.exports = { + fetchAllMembershipsAsMap, +};