Discounts support / make rates configurable
This commit is contained in:
@@ -1,22 +1,92 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Container, Button, Loader } from 'semantic-ui-react';
|
import { Container, Button, Loader, Input, Message, Grid } from 'semantic-ui-react';
|
||||||
|
|
||||||
import MainMenu from '../../components/MainMenu';
|
import MainMenu from '../../components/MainMenu';
|
||||||
|
|
||||||
import { fetchMemberPracticeSummaryReport } from '../../store/actions';
|
import { fetchMemberPracticeSummaryReport } from '../../store/actions';
|
||||||
|
|
||||||
class MemberPracticeSummaryReport extends Component {
|
class MemberPracticeSummaryReport extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
stateError: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onGenerateReportClick = () => {
|
||||||
|
const {fetchMemberPracticeSummaryReport} = this.props;
|
||||||
|
const { year } = this.state;
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const parsedYear = parseInt(year);
|
||||||
|
|
||||||
|
if (!parsedYear || isNaN(parsedYear)){
|
||||||
|
this.setState({stateError: 'Year is not a number'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedYear > currentYear){
|
||||||
|
this.setState({stateError: 'Selected year cannot be greater than current year'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMemberPracticeSummaryReport(year);
|
||||||
|
};
|
||||||
|
|
||||||
|
onYearInputChange = (event, data) => {
|
||||||
|
let newYear = parseInt(data.value)
|
||||||
|
if (!newYear || isNaN(newYear)){
|
||||||
|
newYear = new Date().getFullYear();
|
||||||
|
}
|
||||||
|
this.setState({year: newYear, stateError: null})
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { fetchMemberPracticeSummaryReport, pendingReport } = this.props;
|
const { pendingReport, fetchReportError } = this.props;
|
||||||
|
const { year, stateError } = this.state;
|
||||||
|
|
||||||
|
let error;
|
||||||
|
error = stateError ? stateError : null;
|
||||||
|
error = fetchReportError ? fetchReportError : error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<MainMenu/>
|
<MainMenu/>
|
||||||
<h3>Member Practice Summary Report</h3>
|
<h3>Member Practice Summary Report</h3>
|
||||||
<hr/>
|
<hr/>
|
||||||
<br/>
|
<br/>
|
||||||
|
<Grid stackable>
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column width={5}>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
type="number"
|
||||||
|
label={'Report for : '}
|
||||||
|
value={year}
|
||||||
|
onChange={this.onYearInputChange}
|
||||||
|
/>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column width={5}>
|
||||||
|
<Button disabled={pendingReport} onClick={this.onGenerateReportClick}>Generate Report</Button>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
{error &&
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<Message negative>
|
||||||
|
<Message.Header>Error</Message.Header>
|
||||||
|
<br/>
|
||||||
|
<Message.Content><p>{error}</p></Message.Content>
|
||||||
|
</Message>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
<Loader active={pendingReport} />
|
<Loader active={pendingReport} />
|
||||||
<Button disabled={pendingReport} onClick={fetchMemberPracticeSummaryReport}>Generate Report</Button>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -24,10 +94,11 @@ class MemberPracticeSummaryReport extends Component {
|
|||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
pendingReport: state.memberPracticeSummaryReport.pending,
|
pendingReport: state.memberPracticeSummaryReport.pending,
|
||||||
|
fetchReportError: state.memberPracticeSummaryReport.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
fetchMemberPracticeSummaryReport: () => fetchMemberPracticeSummaryReport(dispatch),
|
fetchMemberPracticeSummaryReport: (year) => fetchMemberPracticeSummaryReport(dispatch, year),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(MemberPracticeSummaryReport);
|
export default connect(mapStateToProps, mapDispatchToProps)(MemberPracticeSummaryReport);
|
||||||
|
|||||||
@@ -113,15 +113,24 @@ export const checkProcessing = (dispatch) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchMemberPracticeSummaryReport = (dispatch) => {
|
export const fetchMemberPracticeSummaryReport = (dispatch, year) => {
|
||||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_PENDING});
|
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_PENDING});
|
||||||
API.get('integration/report/practiceSummary', {
|
API.get(`integration/report/practiceSummary/${year}`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_SUCCESS, payload: response});
|
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_SUCCESS, payload: response});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_FAILED, payload: error.response});
|
let errorMessage = 'Error generating Member Practice Summary Report';
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 400:
|
||||||
|
errorMessage = 'Year cannot be greater than current year and it has to be a number';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_FAILED, payload: errorMessage});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ 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 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 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,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const DISCOUNT_PLANS = process.env.DISCOUNT_PLANS.split(',').map(planName => planName.trim()) || [];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
VALID_CSV_HEADERS,
|
VALID_CSV_HEADERS,
|
||||||
USER_ENTRY_EVENT,
|
USER_ENTRY_EVENT,
|
||||||
@@ -127,4 +139,6 @@ module.exports = {
|
|||||||
BOOKING_CHANGE_PERCENTAGE_CHARGE,
|
BOOKING_CHANGE_PERCENTAGE_CHARGE,
|
||||||
CHARGE_BOOKING_CHANGE_UNDER_TIME,
|
CHARGE_BOOKING_CHANGE_UNDER_TIME,
|
||||||
ALLOWED_BOOKING_CANCELLATION_TIME,
|
ALLOWED_BOOKING_CANCELLATION_TIME,
|
||||||
|
discounts,
|
||||||
|
DISCOUNT_PLANS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const moment = require('moment-timezone');
|
||||||
|
|
||||||
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
|
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
|
||||||
const { getAllIncidents, getMemberPracticeSummaryReport } = require('../services/integration/reports');
|
const { getAllIncidents, getMemberPracticeSummaryReport } = require('../services/integration/reports');
|
||||||
const { getMembersFeesForDateRange } = require('../services/integration/invoiceIntegration');
|
const { getMembersFeesForDateRange } = require('../services/integration/invoiceIntegration');
|
||||||
@@ -7,6 +9,8 @@ const { deleteFeesFromORD, addFeesToORD } = require('../services/officeRnD/fees'
|
|||||||
const { checkBookingChanges } = require('../services/integration/checkBookingChange');
|
const { checkBookingChanges } = require('../services/integration/checkBookingChange');
|
||||||
const { checkIfProcessing } = require('../services/integration/processingStatus');
|
const { checkIfProcessing } = require('../services/integration/processingStatus');
|
||||||
|
|
||||||
|
const { UI_TIMEZONE } = require('../constants/constants');
|
||||||
|
|
||||||
const getKnownOfficeResourceMappings = (req, res) => {
|
const getKnownOfficeResourceMappings = (req, res) => {
|
||||||
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
|
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
|
||||||
|
|
||||||
@@ -127,7 +131,22 @@ const checkProcessingStatus = (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPracticeSummaryReport = (req, res) => {
|
const getPracticeSummaryReport = (req, res) => {
|
||||||
getMemberPracticeSummaryReport()
|
const year = req.params.year;
|
||||||
|
|
||||||
|
const currentYear = moment.tz(UI_TIMEZONE).year();
|
||||||
|
const parsedYear = parseInt(year);
|
||||||
|
|
||||||
|
if (!parsedYear || isNaN(parsedYear)){
|
||||||
|
res.status(400).send('Year is not a number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedYear > currentYear){
|
||||||
|
res.status(400).send('Selected year cannot be greater than current year');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMemberPracticeSummaryReport(parsedYear)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const pathToDownloadFile = `${__dirname}/../${result.reportPath}`;
|
const pathToDownloadFile = `${__dirname}/../${result.reportPath}`;
|
||||||
res.download(pathToDownloadFile);
|
res.download(pathToDownloadFile);
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ ALLOWED_BOOKING_CANCELLATION_TIME=Time from creation (in minutes) in which cance
|
|||||||
|
|
||||||
SEQUELIZE_LOGGING=0 - false, 1 - true (console logging)
|
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
|
||||||
|
DISCOUNT_PLANS=Plan names for which discount is available. Comma-separated
|
||||||
|
|
||||||
#More about pool option : http://docs.sequelizejs.com/class/lib/sequelize.js~Sequelize.html
|
#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_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)
|
DB_POOL_ACQUIRE=The maximum time, in milliseconds, that pool will try to get connection before throwing error (ex. 120000)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ router.post('/integration/addFees', addFees);
|
|||||||
|
|
||||||
router.get('/integration/processing', checkProcessingStatus);
|
router.get('/integration/processing', checkProcessingStatus);
|
||||||
|
|
||||||
router.get('/integration/report/practiceSummary', getPracticeSummaryReport);
|
router.get('/integration/report/practiceSummary/:year', getPracticeSummaryReport);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const getChargedCanceledReservations = (reservationIds) => {
|
|||||||
incidentType: incidentType.BOOKING_CANCELED_LATE,
|
incidentType: incidentType.BOOKING_CANCELED_LATE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const attributes = ['memberId', 'oldBookingStart', 'oldBookingEnd'];
|
const attributes = ['memberId', 'oldBookingStart', 'oldBookingEnd', 'chargeFee'];
|
||||||
|
|
||||||
return db.bookingChangeIncident.findAll({attributes, where: filters});
|
return db.bookingChangeIncident.findAll({attributes, where: filters});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ const getActiveBookingsForMembersInDateRange = (dateRange, memberIds) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllBookingsForYear = (year) => {
|
const getAllBookingsForMembersInDateRange = (dateRange, memberIds) => {
|
||||||
const startDate = moment.tz(year, 'YYYY', UI_TIMEZONE).startOf('year');
|
const startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).startOf('day');
|
||||||
const endDate = moment.tz(year, 'YYYY', UI_TIMEZONE).endOf('year');
|
const endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).endOf('day');
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
'id',
|
'id',
|
||||||
@@ -76,6 +76,12 @@ const getAllBookingsForYear = (year) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (memberIds && Array.isArray(memberIds) && memberIds.length > 0){
|
||||||
|
filters.memberId = {
|
||||||
|
[Op.in]: memberIds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return db.bookingReservation.findAll({
|
return db.bookingReservation.findAll({
|
||||||
attributes,
|
attributes,
|
||||||
where: filters,
|
where: filters,
|
||||||
@@ -84,5 +90,5 @@ const getAllBookingsForYear = (year) => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getActiveBookingsForMembersInDateRange,
|
getActiveBookingsForMembersInDateRange,
|
||||||
getAllBookingsForYear,
|
getAllBookingsForMembersInDateRange,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
const moment = require('moment-timezone');
|
const moment = require('moment-timezone');
|
||||||
|
|
||||||
const { getAllIncidents } = require('./reports');
|
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 { DEFAULT_DATE_FORMAT, UI_TIMEZONE, incidentTypeExplanations, incidentType, unlockedIncidentLevelsPrices } = require('../../constants/constants');
|
||||||
const { getResourceMappings } = require('../officeRnD/resources');
|
const { getResourceMappings } = require('../officeRnD/resources');
|
||||||
const { fetchAllMembers } = require('../officeRnD/members');
|
const { fetchAllMembers } = require('../officeRnD/members');
|
||||||
|
const { fetchAllMembershipsAsMap } = require('../officeRnD/memberships');
|
||||||
|
const { discounts, DISCOUNT_PLANS } = require('../../constants/constants');
|
||||||
|
|
||||||
const createFeeFromIncident = (incident) => {
|
const createFeeFromIncident = (incident) => {
|
||||||
const {
|
const {
|
||||||
@@ -222,17 +224,81 @@ const createFeeFromBooking = (booking, resourceMappings) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createNegativeFeeForDiscount = (memberData, dateRange) => {
|
||||||
|
const { bookingData, member, membershipFees } = memberData;
|
||||||
|
const { totalBookedHours, totalChargedHours, totalBookingChargedFee } = bookingData;
|
||||||
|
const { memberId, officeId } = member;
|
||||||
|
|
||||||
|
let endDate = moment.utc().endOf('day').toISOString();
|
||||||
|
if (dateRange.endDate){
|
||||||
|
endDate = moment.utc(dateRange.endDate, DEFAULT_DATE_FORMAT).endOf('day').toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let membershipFeeForDiscount = 0;
|
||||||
|
membershipFees.forEach((membershipFee) => {
|
||||||
|
const {name, price} = membershipFee;
|
||||||
|
if (DISCOUNT_PLANS.indexOf(name) !== -1){
|
||||||
|
membershipFeeForDiscount = price;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: endDate,
|
||||||
|
member: memberId,
|
||||||
|
team: null,
|
||||||
|
office: officeId,
|
||||||
|
isPersonal: false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getMembersFeesForDateRange = (dateRange, memberIds) => {
|
const getMembersFeesForDateRange = (dateRange, memberIds) => {
|
||||||
return new Promise((resolve, reject) => {
|
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)
|
Promise.all(collectData)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const allIncidents = result[0];
|
const allIncidents = result[0];
|
||||||
const allActiveBookings = result[1];
|
const allBookings = result[1];
|
||||||
const resourceMappings = result[2];
|
const resourceMappings = result[2];
|
||||||
const membersList = result[3];
|
const membersList = result[3];
|
||||||
|
const membershipsMap = result[4];
|
||||||
|
|
||||||
|
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],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const memberIdTeamMappings = {};
|
const memberIdTeamMappings = {};
|
||||||
membersList.forEach((member) => {
|
membersList.forEach((member) => {
|
||||||
@@ -241,14 +307,127 @@ const getMembersFeesForDateRange = (dateRange, memberIds) => {
|
|||||||
|
|
||||||
const allFees = [];
|
const allFees = [];
|
||||||
|
|
||||||
allIncidents.forEach((incident) => allFees.push(createFeeFromIncident(incident)));
|
allIncidents.forEach((incident) => {
|
||||||
allActiveBookings.forEach((booking) => allFees.push(createFeeFromBooking(booking, resourceMappings)));
|
allFees.push(createFeeFromIncident(incident));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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){
|
||||||
|
membersMap[memberId].bookingData.totalChargedHours += bookingLength;
|
||||||
|
const bookingFee = bookingLength * hourlyRate;
|
||||||
|
membersMap[memberId].bookingData.totalBookingChargedFee += bookingFee;
|
||||||
|
|
||||||
|
allFees.push(createFeeFromBooking(booking, resourceMappings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//add discount
|
||||||
|
memberIds.forEach((memberId) => {
|
||||||
|
const discountFee = createNegativeFeeForDiscount(membersMap[memberId], dateRange);
|
||||||
|
if (discountFee){
|
||||||
|
allFees.push(discountFee);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
allFees.forEach((fee) => {
|
allFees.forEach((fee) => {
|
||||||
fee.team = memberIdTeamMappings[fee.member] || null;
|
fee.team = memberIdTeamMappings[fee.member] || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve(allFees);
|
resolve(allFees);
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const workbookCreator = require('excel4node');
|
|||||||
const { checkBookingChanges } = require('./checkBookingChange');
|
const { checkBookingChanges } = require('./checkBookingChange');
|
||||||
const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors } = require('../../constants/constants');
|
const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors } = require('../../constants/constants');
|
||||||
|
|
||||||
const { getAllBookingsForYear } = require('./bookings');
|
const { getAllBookingsForMembersInDateRange } = require('./bookings');
|
||||||
const { fetchAllMembers } = require('../officeRnD/members');
|
const { fetchAllMembers } = require('../officeRnD/members');
|
||||||
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
|
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
|
||||||
const { getChargedCanceledReservations } = require('../integration/bookingChangeCharges');
|
const { getChargedCanceledReservations } = require('../integration/bookingChangeCharges');
|
||||||
@@ -318,11 +318,17 @@ const getAllIncidents = (dateRange, memberIds) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMemberPracticeSummaryReport = (res) => {
|
const getMemberPracticeSummaryReport = (year) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const year = moment.tz(UI_TIMEZONE).year();
|
|
||||||
|
|
||||||
const asyncJobs = [checkBookingChanges(), getAllBookingsForYear(year), fetchAllMembers()];
|
const startDate = moment.tz(year, 'YYYY', UI_TIMEZONE).startOf('year').format(DEFAULT_DATE_FORMAT);
|
||||||
|
const endDate = moment.tz(year, 'YYYY', UI_TIMEZONE).endOf('year').format(DEFAULT_DATE_FORMAT);
|
||||||
|
const dateRange = {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const asyncJobs = [checkBookingChanges(), getAllBookingsForMembersInDateRange(dateRange), fetchAllMembers()];
|
||||||
|
|
||||||
Promise.all(asyncJobs)
|
Promise.all(asyncJobs)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const fetchAllMembers = () => {
|
|||||||
memberId: member['_id'],
|
memberId: member['_id'],
|
||||||
teamId: member.team,
|
teamId: member.team,
|
||||||
active: member.status === 'active',
|
active: member.status === 'active',
|
||||||
|
officeId: member.office,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 );
|
cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 );
|
||||||
|
|||||||
35
services/officeRnD/memberships.js
Normal file
35
services/officeRnD/memberships.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
'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, name, member } = membership;
|
||||||
|
if (!membershipsMap[member]) {
|
||||||
|
membershipsMap[member] = [{
|
||||||
|
price,
|
||||||
|
name,
|
||||||
|
}];
|
||||||
|
}else{
|
||||||
|
membershipsMap[member].push({
|
||||||
|
price,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve(membershipsMap);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fetchAllMembershipsAsMap,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user