'use strict'; const db = require('../../models/index'); const moment = require('moment-timezone'); const Op = require('sequelize').Op; const { API } = require('../../helpers/api'); const { officeRnDAPIErrors, MAX_BACK_TO_BACK_DIFFERENCE, UI_TIMEZONE } = require('../../constants/constants'); const fetchAllBookings = () => { return new Promise((resolve, reject) => { API.get('/bookings') .then((result) => { const cleanedBookingReservations = []; const bookingData = result && result.data ? result.data : []; const bookingsToCreate = []; const bookingIdsToRemove = []; bookingData.forEach(fullBookingEntry => { if (!fullBookingEntry){ return; } const fees = fullBookingEntry.fees ? fullBookingEntry.fees : []; if (fees.length > 1){ // Recurring booking, let's create new booking const member = fullBookingEntry.member ? fullBookingEntry.member : null; const office = fullBookingEntry.office ? fullBookingEntry.office : null; const resourceId = fullBookingEntry.resourceId ? fullBookingEntry.resourceId : null; const team = fullBookingEntry.team ? fullBookingEntry.team : null; const organization = fullBookingEntry.organization ? fullBookingEntry.organization : null; const plan = fullBookingEntry.plan ? fullBookingEntry.plan : null; const timezone = fullBookingEntry.timezone ? fullBookingEntry.timezone : UI_TIMEZONE; const source = 'admin'; const startMoment = fullBookingEntry && fullBookingEntry.start && fullBookingEntry.start.dateTime ? moment.utc(fullBookingEntry.start.dateTime) : null; const endMoment = fullBookingEntry && fullBookingEntry.end && fullBookingEntry.end.dateTime ? moment.utc(fullBookingEntry.end.dateTime) : null; fees.forEach(fee => { const dateMoment = fee.date ? moment.utc(fee.date) : null; if (startMoment && endMoment && dateMoment){ const yearPart = dateMoment.year(); const monthPart = dateMoment.month(); const dayPart = dateMoment.date(); const newStartMoment = startMoment.clone().tz(fullBookingEntry.timezone).year(yearPart).month(monthPart).date(dayPart); const newEndMoment = endMoment.clone().tz(fullBookingEntry.timezone).year(yearPart).month(monthPart).date(dayPart); bookingsToCreate.push({ start: { dateTime: newStartMoment.toISOString() }, end: { dateTime: newEndMoment.toISOString() }, team, member, resourceId, office, source, timezone, organization, plan }) } }); bookingIdsToRemove.push(fullBookingEntry['_id']); } }); //Here we now have possible bookings to create and then load again "check Booking changes" if (bookingIdsToRemove.length > 0){ //First delete, wait until operation is done, than create bookings (to avoid conflicting date/time) API.delete('bookings/?silent', { data: bookingIdsToRemove }) .then(() => { //Now, insert new bookings API.post('bookings/?silent', bookingsToCreate) .then(() => { //And fetch again all bookings resolve(fetchAllBookings()); }) .catch((error) => { console.log(officeRnDAPIErrors.FAILED_TO_CREATE_BOOKINGS); console.log('Details : ', error); reject(officeRnDAPIErrors.FAILED_TO_CREATE_BOOKINGS); }); }) .catch(error => { console.log(officeRnDAPIErrors.FAILED_TO_DELETE_BOOKINGS); console.log('Details : ', error); reject(officeRnDAPIErrors.FAILED_TO_DELETE_BOOKINGS); }); }else{ bookingData.forEach((fullBookingEntry) => { const fees = fullBookingEntry && fullBookingEntry.fees ? fullBookingEntry.fees : []; const firstFee = fees.length > 0 && fees[0].fee ? fees[0].fee : undefined; const hourlyRate = firstFee && firstFee.price ? firstFee.price : 0; const startMoment = fullBookingEntry && fullBookingEntry.start && fullBookingEntry.start.dateTime ? moment.utc(fullBookingEntry.start.dateTime) : null; const endMoment = fullBookingEntry && fullBookingEntry.end && fullBookingEntry.end.dateTime ? moment.utc(fullBookingEntry.end.dateTime) : null; // console.log('\r\n\r\nStart : ', startMoment.clone().tz(fullBookingEntry.timezone).format('DD.MM. HH:mm'), '[', startMoment.toISOString(),']'); // console.log('End : ', endMoment.clone().tz(fullBookingEntry.timezone).format('DD.MM. HH:mm'), '[', endMoment.toISOString(), ']'); // console.log('Fees : '); if (startMoment && endMoment){ cleanedBookingReservations.push({ reservationId: fullBookingEntry['_id'], memberId: fullBookingEntry.member, officeId: fullBookingEntry.office, resourceId: fullBookingEntry.resourceId, start: startMoment.toISOString(), end: endMoment.toISOString(), timezone: fullBookingEntry.timezone, canceled: fullBookingEntry.canceled || false, hourlyRate, }); } }); resolve(cleanedBookingReservations); } }) .catch((error) => { console.log(officeRnDAPIErrors.FAILED_TO_FETCH_BOOKINGS); console.log('Details : ', error); reject(officeRnDAPIErrors.FAILED_TO_FETCH_BOOKINGS); }); }); }; const getAllFinishedBookings = () => { const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone']; const filters = { canceled: false, end: { [Op.lt]: moment.utc().toISOString() } }; return db.bookingReservation.findAll({ attributes, where: filters, order: [ ['start', 'ASC'], ] }) }; const getFirstNextBooking = (reservation) => { return new Promise ((resolve, reject) => { const { resourceId, start } = reservation; const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone']; const filters = { canceled: false, start: { [Op.gt]: start }, resourceId, }; const order = [['start', 'ASC']]; db.bookingReservation.findAll({ attributes, where: filters, order, }) .then((reservations) => { if (reservations && reservations[0]){ resolve(reservations[0]); }else{ resolve(undefined); } }) .catch((error) => reject(error)); }); }; const getFirstPreviousBooking = (reservation) => { return new Promise ((resolve, reject) => { const { resourceId, start } = reservation; const attributes = ['reservationId', 'memberId', 'resourceId', 'start', 'end', 'timezone']; const filters = { canceled: false, end: { [Op.lte]: start }, resourceId, }; const order = [['end', 'DESC']]; db.bookingReservation.findAll({ attributes, where: filters, order, }) .then((reservations) => { if (reservations && reservations[0]){ resolve(reservations[0]); }else{ resolve(undefined); } }) .catch((error) => reject(error)); }); }; const getFirstReservationInBlock = (reservation) => { return new Promise ((resolve, reject) => { const { resourceId, memberId, start } = reservation; const fromTimestamp = moment.utc(start).subtract(MAX_BACK_TO_BACK_DIFFERENCE).toISOString(); const toTimestamp = start; const filters = { resourceId, memberId, end: { [Op.and]: [ {[Op.gte]: fromTimestamp}, {[Op.lte]: toTimestamp} ] } }; db.bookingReservation.findOne({where: filters}) .then((previousReservation) => { if (!previousReservation) { resolve(reservation); } else { resolve(getFirstReservationInBlock(previousReservation)); } }) .catch((error) => reject(error)); }); }; const writeBookingReservation = (bookingReservation) => { const { reservationId, memberId, officeId, resourceId, start, end, timezone, canceled, hourlyRate } = bookingReservation; const bookingReservationForDB = { reservationId, memberId, officeId, resourceId, start, end, timezone, canceled, hourlyRate, }; return db.bookingReservation.findOrCreate({where: {...bookingReservationForDB}, defaults: {...bookingReservationForDB}}); }; const bulkWriteReservationsWithChangesTracking = (reservations, resourcesMap) => { return new Promise ((resolve, reject) => { const changes = []; const asyncJobs = []; db.bookingReservation.addHook('beforeUpdate', 'updateHook', (instance) => { const changedKeys = instance.changed(); const previous = instance.previous(); const lookupKeys = ['start', 'end', 'resourceId', 'canceled']; let realChange = false; lookupKeys.forEach((key) => { if ((changedKeys.indexOf(key) !== -1) && (JSON.stringify(previous[key]) !== JSON.stringify(instance[key]))){ realChange = true; } }); const previousResourceId = instance.previous('resourceId'); const currentResourceId = instance.resourceId; const resourceId = currentResourceId ? currentResourceId : previousResourceId; if (instance.hourlyRate === 0 || isNaN(instance.hourlyRate)){ if (parseFloat(instance.previous('hourlyRate') > 0)) { instance.setDataValue('hourlyRate', instance.previous('hourlyRate')); }else{ //Determine if we should apply weekend price or work day price const newStartWeekDay = moment.utc(instance.start).isoWeekday(); const isWeekend = newStartWeekDay > 5; //6 - Saturday, 7 - Sunday let hourlyRate; if (isWeekend){ hourlyRate = resourceId ? resourcesMap[resourceId].price.weekendPrice : 0; }else{ hourlyRate = resourceId ? resourcesMap[resourceId].price.price : 0; } instance.setDataValue('hourlyRate', hourlyRate); } } if (realChange){ changes.push({ oldReservation: previous, newReservation: instance.get(), }); } }); const newReservations = []; const asyncReservationUpdate = (reservation) => { return new Promise((resolve, reject) => { db.bookingReservation.update(reservation, { where: { reservationId: reservation.reservationId, }, returning: true, individualHooks: true, }) .then(([updateCount, updatedInstances]) => { try { if (updateCount === 0) { const oldReservation = { start: null, end: null, resourceId: null, }; changes.push({ oldReservation, newReservation: reservation, }); newReservations.push(reservation); } resolve(); }catch (e) { console.log('CATCH E : ', e); reject(e); } }) .catch((error) => { console.log('Error updating'); console.log(error); reject(error); }); }); }; reservations.forEach((reservation) => asyncJobs.push(asyncReservationUpdate(reservation))); Promise.all(asyncJobs) .then(() => { db.bookingReservation.removeHook('updateHook'); db.bookingReservation.bulkCreate(newReservations) .then(() => resolve(changes)) .catch((error) => reject(error)); }) .catch((error) => reject(error)); }); }; module.exports = { fetchAllBookings, writeBookingReservation, getAllFinishedBookings, getFirstNextBooking, getFirstPreviousBooking, getFirstReservationInBlock, bulkWriteReservationsWithChangesTracking, };