de.appsolve.padelcampus.utils.BookingUtil.java Source code

Java tutorial

Introduction

Here is the source code for de.appsolve.padelcampus.utils.BookingUtil.java

Source

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package de.appsolve.padelcampus.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.paypal.base.codec.binary.Base64;
import de.appsolve.padelcampus.constants.*;
import de.appsolve.padelcampus.data.*;
import de.appsolve.padelcampus.db.dao.*;
import de.appsolve.padelcampus.db.model.*;
import de.appsolve.padelcampus.exceptions.CalendarConfigException;
import de.appsolve.padelcampus.exceptions.MailException;
import de.jollyday.HolidayCalendar;
import de.jollyday.HolidayManager;
import de.jollyday.ManagerParameters;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static de.appsolve.padelcampus.constants.Constants.*;

/**
 * @author dominik
 */
@Component
public class BookingUtil {

    private static final Logger LOG = Logger.getLogger(BookingUtil.class);

    @Autowired
    Msg msg;

    @Autowired
    OfferDAOI offerDAO;

    @Autowired
    FacilityDAOI facilityDAO;

    @Autowired
    BookingDAOI bookingDAO;

    @Autowired
    ContactDAOI contactDAO;

    @Autowired
    CalendarConfigDAOI calendarConfigDAO;

    @Autowired
    CalendarConfigUtil calendarConfigUtil;

    @Autowired
    BookingMailSettingsDAOI bookingMailSettingsDAO;

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    MailUtils mailUtils;

    @Autowired
    PayPalConfigDAOI payPalConfigDAO;

    @Autowired
    PayDirektConfigDAOI payDirektConfigDAO;

    @Autowired
    PayMillConfigDAOI payMillConfigDAO;

    @Autowired
    SessionUtil sessionUtil;

    public static String generateUUID() {
        return UUID.randomUUID().toString();
    }

    public List<TimeSlot> getTimeSlotsForDate(LocalDate selectedDate, List<CalendarConfig> allCalendarConfigs,
            List<Booking> existingBookings, Boolean onlyFutureTimeSlots, Boolean preventOverlapping)
            throws CalendarConfigException {

        List<CalendarConfig> calendarConfigs = calendarConfigUtil.getCalendarConfigsMatchingDate(allCalendarConfigs,
                selectedDate);
        Iterator<CalendarConfig> iterator = calendarConfigs.iterator();
        while (iterator.hasNext()) {
            CalendarConfig calendarConfig = iterator.next();
            if (isHoliday(selectedDate, calendarConfig)) {
                iterator.remove();
            }
        }

        List<TimeSlot> timeSlots = new ArrayList<>();
        if (calendarConfigs.size() > 0) {
            LocalDate today = new LocalDate(DEFAULT_TIMEZONE);

            //sort all calendar configurations for selected date by start time
            Collections.sort(calendarConfigs);

            CalendarConfig previousConfig = null;
            LocalDateTime time = null;
            LocalDateTime now = new LocalDateTime(DEFAULT_TIMEZONE);

            //generate list of bookable time slots
            int i = 0;
            for (CalendarConfig config : calendarConfigs) {
                i++;
                LocalDateTime startDateTime = getLocalDateTime(selectedDate, config.getStartTime());
                if (time == null) {
                    //on first iteration
                    time = startDateTime;
                } else if (!time.plusMinutes(previousConfig.getMinInterval()).equals(startDateTime)) {
                    //reset basePriceLastConfig as this is a non contiguous offer
                    previousConfig = null;
                    time = startDateTime;
                }
                LocalDateTime endDateTime = getLocalDateTime(selectedDate, config.getEndTime());
                while (time.plusMinutes(config.getMinDuration()).compareTo(endDateTime) <= 0) {
                    BigDecimal pricePerMinDuration;
                    if (previousConfig == null) {
                        pricePerMinDuration = config.getBasePrice();
                    } else {
                        BigDecimal previousConfigBasePricePerMinute = getPricePerMinute(previousConfig);
                        pricePerMinDuration = previousConfigBasePricePerMinute
                                .multiply(new BigDecimal(previousConfig.getMinInterval()), MathContext.DECIMAL128);
                        BigDecimal basePricePerMinute = getPricePerMinute(config);
                        pricePerMinDuration = pricePerMinDuration.add(basePricePerMinute.multiply(
                                new BigDecimal(config.getMinDuration() - previousConfig.getMinInterval()),
                                MathContext.DECIMAL128));
                        previousConfig = null;
                    }
                    pricePerMinDuration = pricePerMinDuration.setScale(2, RoundingMode.HALF_EVEN);
                    if (onlyFutureTimeSlots) {
                        if (selectedDate.isAfter(today) || time.isAfter(now)) {
                            addTimeSlot(timeSlots, time, config, pricePerMinDuration);
                        }
                    } else {
                        addTimeSlot(timeSlots, time, config, pricePerMinDuration);
                    }
                    time = time.plusMinutes(config.getMinInterval());
                }
                //make sure to display the last min interval of the day
                if (config.getMinInterval() < config.getMinDuration() && i == calendarConfigs.size()) {
                    if (time.plusMinutes(config.getMinInterval()).compareTo(endDateTime) <= 0) {
                        addTimeSlot(timeSlots, time, config, null);
                    }
                }
                previousConfig = config;
            }
            //sort time slots by time
            Collections.sort(timeSlots);

            //decrease court count for every blocking booking
            for (TimeSlot timeSlot : timeSlots) {
                checkForBookedCourts(timeSlot, existingBookings, preventOverlapping);
            }
        }
        return timeSlots;
    }

    private void addTimeSlot(List<TimeSlot> timeSlots, LocalDateTime time, CalendarConfig config,
            BigDecimal pricePerMinDuration) {
        TimeSlot timeSlot = new TimeSlot();
        timeSlot.setDate(time.toLocalDate());
        timeSlot.setStartTime(time.toLocalTime());
        timeSlot.setEndTime(time.toLocalTime().plusMinutes(config.getMinDuration()));
        timeSlot.setConfig(config);
        timeSlot.setPricePerMinDuration(pricePerMinDuration);

        //only add timeSlot if timeSlots does not already contain an entry that overlaps
        if (!overlaps(timeSlot, timeSlots)) {
            timeSlots.add(timeSlot);
        }
    }

    private boolean overlaps(TimeSlot timeSlot, List<TimeSlot> timeSlots) {
        for (TimeSlot slot : timeSlots) {
            if (timeSlot.getStartTime().equals(slot.getStartTime())) {
                return configMatches(timeSlot, slot);
            } else if (timeSlot.getStartTime().isBefore(slot.getStartTime())) {
                //make sure timeSlot ends before slot starts
                if (timeSlot.getEndTime().isAfter(slot.getStartTime())) {
                    return configMatches(timeSlot, slot);
                }
            } else {
                //disabled, as we do want overlapping time slots from different calendar configurations
                //                //only test if these time slots come from different configurations as we DO want overlapping time slots (when min interval is smaller than min duration)
                //                if (!timeSlot.getConfig().equals(slot.getConfig())){
                //
                //                    //if timeSlot starts after slot, make sure it also starts before slot ends
                //                    if (timeSlot.getStartTime().isBefore(slot.getEndTime())){
                //                        return true;
                //                    }
                //                }
            }
        }
        return false;
    }

    private boolean configMatches(TimeSlot timeSlot, TimeSlot slot) {
        return timeSlot.getConfig().equals(slot.getConfig());
    }

    public void checkForBookedCourts(TimeSlot timeSlot, List<Booking> confirmedBookings,
            Boolean preventOverlapping) {
        LocalTime startTime = timeSlot.getStartTime();
        LocalTime endTime = timeSlot.getEndTime();

        for (Booking booking : confirmedBookings) {

            if (timeSlot.getDate().equals(booking.getBookingDate())) {
                LocalTime bookingStartTime = booking.getBookingTime();
                LocalTime bookingEndTime = bookingStartTime.plusMinutes(booking.getDuration().intValue());
                Boolean addBooking = false;
                if (preventOverlapping) {
                    if (startTime.isBefore(bookingEndTime)) {
                        if (endTime.isAfter(bookingStartTime)) {
                            addBooking = true;
                        }
                    }
                } else {
                    //for displaying allocations
                    if (startTime.compareTo(bookingStartTime) >= 0) {
                        if (endTime.compareTo(bookingEndTime) <= 0) {
                            addBooking = true;
                        }
                    }
                }
                if (addBooking) {
                    Offer offer = booking.getOffer();
                    for (Offer timeSlotOffer : timeSlot.getConfig().getOffers()) {
                        if (offer.equals(timeSlotOffer)) {
                            timeSlot.addBooking(booking);
                            break;
                        }
                    }
                }
            }
        }
    }

    public boolean isHoliday(LocalDate date, CalendarConfig calendarConfig) {
        String holidayKey = calendarConfig.getHolidayKey();
        boolean isHoliday = false;
        if (!holidayKey.equals(NO_HOLIDAY_KEY)) {
            String[] holidayKeySplit = holidayKey.split("-");
            String country = holidayKeySplit[0];
            String region = holidayKeySplit[1];

            HolidayManager countryHolidays = HolidayManager
                    .getInstance(ManagerParameters.create(HolidayCalendar.valueOf(country)));
            isHoliday = countryHolidays.isHoliday(date, region);
        }
        return isHoliday;
    }

    public void addWeekView(HttpServletRequest request, LocalDate selectedDate, List<Facility> selectedFacilities,
            ModelAndView mav, Boolean onlyFutureTimeSlots, Boolean preventOverlapping)
            throws JsonProcessingException {
        //calculate date configuration for datepicker
        LocalDate today = new LocalDate(DEFAULT_TIMEZONE);
        LocalDate firstDay = today.dayOfMonth().withMinimumValue();
        LocalDate lastDay = getLastBookableDay(request).plusDays(1);
        List<CalendarConfig> calendarConfigs = calendarConfigDAO.findBetween(firstDay, lastDay);
        Collections.sort(calendarConfigs);
        Map<String, DatePickerDayConfig> dayConfigs = getDayConfigMap(firstDay, lastDay, calendarConfigs);

        List<Booking> confirmedBookings = bookingDAO.findBlockedBookingsBetween(firstDay, lastDay);

        //calculate available time slots
        List<TimeSlot> timeSlots = new ArrayList<>();
        List<LocalDate> weekDays = new ArrayList<>();
        for (int i = 1; i <= CalendarWeekDay.values().length; i++) {
            LocalDate date = selectedDate.withDayOfWeek(i);
            weekDays.add(date);
            if ((!onlyFutureTimeSlots || !date.isBefore(today)) && lastDay.isAfter(date)) {
                try {
                    //generate list of bookable time slots
                    timeSlots.addAll(getTimeSlotsForDate(date, calendarConfigs, confirmedBookings,
                            onlyFutureTimeSlots, preventOverlapping));
                } catch (CalendarConfigException e) {
                    //safe to ignore
                }
            }
        }

        SortedSet<Offer> offers = new TreeSet<>();
        List<TimeRange> rangeList = new ArrayList<>();
        //Map<TimeRange, List<TimeSlot>> rangeList = new TreeMap<>();
        for (TimeSlot slot : timeSlots) {
            Set<Offer> slotOffers = slot.getConfig().getOffers();
            offers.addAll(slotOffers);

            TimeRange range = new TimeRange();
            range.setStartTime(slot.getStartTime());
            range.setEndTime(slot.getEndTime());

            if (rangeList.contains(range)) {
                range = rangeList.get(rangeList.indexOf(range));
            } else {
                rangeList.add(range);
            }

            List<TimeSlot> slotis = range.getTimeSlots();
            slotis.add(slot);
            range.setTimeSlots(slotis);
        }
        Collections.sort(rangeList);

        List<Offer> selectedOffers = new ArrayList<>();
        if (selectedFacilities.isEmpty()) {
            selectedOffers = offerDAO.findAll();
        } else {
            for (Facility facility : selectedFacilities) {
                selectedOffers.addAll(facility.getOffers());
            }
        }
        Collections.sort(selectedOffers);

        mav.addObject("dayConfigs", objectMapper.writeValueAsString(dayConfigs));
        mav.addObject("maxDate", lastDay.toString());
        mav.addObject("Day", selectedDate);
        mav.addObject("NextMonday", selectedDate.plusDays(8 - selectedDate.getDayOfWeek()));
        mav.addObject("PrevSunday", selectedDate.minusDays(selectedDate.getDayOfWeek()));
        mav.addObject("WeekDays", weekDays);
        mav.addObject("RangeMap", rangeList);
        mav.addObject("Offers", offers);
        mav.addObject("SelectedOffers", selectedOffers);
        mav.addObject("SelectedFacilities", selectedFacilities);
        mav.addObject("Facilities", facilityDAO.findAll());
    }

    public Long getBookingSlotsLeft(TimeSlot timeSlot, Offer offer, List<Booking> confirmedBookings) {
        checkForBookedCourts(timeSlot, confirmedBookings, true);

        Long bookingSlotsLeft = offer.getMaxConcurrentBookings();
        for (Booking existingBooking : timeSlot.getBookings()) {
            if (existingBooking.getOffer().equals(offer)) {
                bookingSlotsLeft--;
            }
        }
        return bookingSlotsLeft;
    }

    public Map<String, DatePickerDayConfig> getDayConfigMap(LocalDate firstDay, LocalDate lastDay,
            List<CalendarConfig> calendarConfigs) {
        Map<String, DatePickerDayConfig> dayConfigs = new HashMap<>();
        for (LocalDate date = firstDay; date.isBefore(lastDay); date = date.plusDays(1)) {
            DatePickerDayConfig dayConfig = new DatePickerDayConfig();
            dayConfig.setSelectable(Boolean.FALSE);
            for (CalendarConfig calendarConfig : calendarConfigs) {
                for (CalendarWeekDay weekDay : calendarConfig.getCalendarWeekDays()) {
                    if (weekDay.ordinal() + 1 == date.getDayOfWeek()) {
                        if (!isHoliday(date, calendarConfig)) {
                            if (calendarConfig.getStartDate().compareTo(date) <= 0
                                    && calendarConfig.getEndDate().compareTo(date) >= 0) {
                                dayConfig.setSelectable(Boolean.TRUE);
                            }
                        }
                    }
                }
            }
            dayConfigs.put(date.toString(), dayConfig);
        }
        return dayConfigs;
    }

    public LocalDateTime getLocalDateTime(LocalDate selectedDate, LocalTime startTime) {
        return new LocalDateTime(selectedDate.getYear(), selectedDate.getMonthOfYear(),
                selectedDate.getDayOfMonth(), startTime.getHourOfDay(), startTime.getMinuteOfHour());
    }

    public BigDecimal getPricePerMinute(CalendarConfig config) {
        return config.getBasePrice().divide(new BigDecimal(config.getMinDuration().toString()),
                MathContext.DECIMAL128);
    }

    public void sendBookingConfirmation(HttpServletRequest request, Booking booking)
            throws MailException, IOException {
        Mail mail = new Mail();
        mail.setSubject(msg.get("BookingSuccessfulMailSubject"));
        mail.setTemplateId("HTMLEmail");
        String htmlBodyTemplate = msg.get("BookingSuccessfulMailBodyHtml");
        BookingMailSettings bookingMailSettings = bookingMailSettingsDAO.findFirst();
        if (bookingMailSettings != null) {
            if (!StringUtils.isEmpty(bookingMailSettings.getHtmlBodyTemplate())) {
                htmlBodyTemplate = bookingMailSettings.getHtmlBodyTemplate();
            }
        }
        // replace variables
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_PLAYER.name(),
                booking.getPlayer().toString());
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_DATE.name(),
                FormatUtils.DATE_MEDIUM.print(booking.getBookingDate()));
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_TIME.name(),
                FormatUtils.TIME_HUMAN_READABLE.print(booking.getBookingTime()));
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_OFFER.name(), booking.getName());
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_PAYMENT_METHOD.name(),
                msg.get(booking.getPaymentMethod().toString()));
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_AMOUNT.name(),
                booking.getPaymentMethod() == PaymentMethod.ExternalVoucher ? "" : booking.getAmount().toString());
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_CURRENCY.name(),
                booking.getCurrency().getSymbol());
        htmlBodyTemplate = htmlBodyTemplate.replace(
                BookingMailVariables.BOOKING_CANCELLATION_POLICY_DEADLINE.name(),
                CANCELLATION_POLICY_DEADLINE.toString());
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_CANCELLATION_URL.name(),
                RequestUtil.getBaseURL(request) + "/bookings/booking/" + booking.getUUID() + "/cancel");
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.BOOKING_INVOICE_URL.name(),
                RequestUtil.getBaseURL(request) + "/invoices/booking/" + booking.getUUID());
        htmlBodyTemplate = htmlBodyTemplate.replace(BookingMailVariables.HOMEPAGE_URL.name(),
                RequestUtil.getBaseURL(request));

        mail.setHtmlBody(htmlBodyTemplate);

        mail.addRecipient(booking.getPlayer());
        mail.setAttachments(getAttachments(booking));
        mailUtils.send(mail, request);
    }

    public void sendEventBookingConfirmation(HttpServletRequest request, Booking booking)
            throws MailException, IOException {
        Mail mail = new Mail();
        mail.setSubject(msg.get("EventBookingSuccessfulMailSubject"));
        mail.setBody(msg.get("BookingEventSuccessfulMailBody",
                new Object[] { booking.getPlayer().toString(),
                        FormatUtils.DATE_MEDIUM.print(booking.getBookingDate()),
                        FormatUtils.TIME_HUMAN_READABLE.print(booking.getBookingTime()), booking.getName(),
                        msg.get(booking.getPaymentMethod().toString()),
                        booking.getPaymentMethod() == PaymentMethod.ExternalVoucher ? "" : booking.getAmount(),
                        booking.getCurrency(),
                        (StringUtils.isEmpty(booking.getEvent().getConfirmationMailRemark()) ? msg.get("None")
                                : booking.getEvent().getConfirmationMailRemark()),
                        RequestUtil.getBaseURL(request) + "/invoices/booking/" + booking.getUUID(),
                        RequestUtil.getBaseURL(request) }));
        mail.addRecipient(booking.getPlayer());
        mail.setAttachments(getAttachments(booking));
        mailUtils.send(mail, request);
    }

    public void sendNewBookingNotification(HttpServletRequest request, Booking booking)
            throws MailException, IOException {
        Set<Contact> contactsToNotifyOnBooking = Sets.newHashSet(contactDAO.findAllForBookings());
        if (!contactsToNotifyOnBooking.isEmpty()) {
            Mail mail = new Mail();
            mail.setSubject(msg.get("BookingSuccessfulMailSubjectAdmin",
                    new Object[] { FormatUtils.DATE_HUMAN_READABLE.print(booking.getBookingDate()),
                            FormatUtils.TIME_HUMAN_READABLE.print(booking.getBookingTime()),
                            booking.getPlayer().toString() }));
            mail.setBody(msg.get("BookingSuccessfulMailBodyAdmin", getDetailBody(request, booking)));
            mail.setRecipients(contactsToNotifyOnBooking);
            mailUtils.send(mail, request);
        }
    }

    public void sendBookingCancellationNotification(HttpServletRequest request, Booking booking)
            throws MailException, IOException {
        List<Contact> contactsToNotifyOnBookingCancellation = contactDAO.findAllForBookingCancellations();
        if (!contactsToNotifyOnBookingCancellation.isEmpty()) {
            Mail mail = new Mail();
            mail.setSubject(msg.get("BookingCancelledAdminMailSubject",
                    new Object[] { FormatUtils.DATE_HUMAN_READABLE.print(booking.getBookingDate()),
                            FormatUtils.TIME_HUMAN_READABLE.print(booking.getBookingTime()),
                            booking.getPlayer().toString() }));
            mail.setBody(msg.get("BookingCancelledAdminMailBody", getDetailBody(request, booking)));
            mail.addRecipient(booking.getPlayer());
            mailUtils.send(mail, request);
        }
    }

    public LocalDate getLastBookableDay(HttpServletRequest request) {
        Integer maxDate = Constants.CALENDAR_MAX_DATE;
        if (sessionUtil.getPrivileges(request).contains(Privilege.ManageBookings)) {
            maxDate = Constants.CALENDAR_MAX_DATE_ADMINS;
        }
        return new LocalDate(DEFAULT_TIMEZONE).plusDays(maxDate);
    }

    private Object[] getDetailBody(HttpServletRequest request, Booking booking) {
        return new Object[] { booking.getPlayer().toString(),
                FormatUtils.DATE_HUMAN_READABLE.print(booking.getBookingDate()),
                FormatUtils.TIME_HUMAN_READABLE.print(booking.getBookingTime()), booking.getName(),
                msg.get(booking.getPaymentMethod().toString()), booking.getAmount(), booking.getCurrency(),
                RequestUtil.getBaseURL(request) + "/invoices/booking/" + booking.getUUID(),
                RequestUtil.getBaseURL(request) + "/admin/reports/booking/" + booking.getId() };
    }

    public OfferDurationPrice getOfferDurationPrice(List<CalendarConfig> configs, List<Booking> confirmedBookings,
            LocalDate selectedDate, LocalTime selectedTime, Offer selectedOffer) throws CalendarConfigException {
        List<TimeSlot> timeSlotsForDate = getTimeSlotsForDate(selectedDate, configs, confirmedBookings,
                Boolean.TRUE, Boolean.TRUE);
        boolean validStartTime = false;
        for (TimeSlot timeSlot : timeSlotsForDate) {
            if (timeSlot.getStartTime().equals(selectedTime)) {
                validStartTime = true;
                break;
            }
        }

        OfferDurationPrice offerDurationPrices = null;
        if (validStartTime) {
            //convert to required data structure
            Map<Offer, List<CalendarConfig>> offerConfigMap = new HashMap<>();
            for (CalendarConfig config : configs) {
                for (Offer offer : config.getOffers()) {
                    if (offer.equals(selectedOffer)) {
                        List<CalendarConfig> list = offerConfigMap.get(offer);
                        if (list == null) {
                            list = new ArrayList<>();
                        }
                        list.add(config);

                        //sort by start time
                        Collections.sort(list);
                        offerConfigMap.put(offer, list);
                    }
                }
            }

            Iterator<Map.Entry<Offer, List<CalendarConfig>>> iterator = offerConfigMap.entrySet().iterator();
            //for every offer
            while (iterator.hasNext()) {
                Map.Entry<Offer, List<CalendarConfig>> entry = iterator.next();
                Offer offer = entry.getKey();
                List<CalendarConfig> configsForOffer = entry.getValue();

                //make sure the first configuration starts before the requested booking time
                if (selectedTime.compareTo(configsForOffer.get(0).getStartTime()) < 0) {
                    continue;
                }

                LocalDateTime endTime = null;
                Integer duration = configsForOffer.get(0).getMinDuration();
                BigDecimal pricePerMinute;
                BigDecimal price = null;
                CalendarConfig previousConfig = null;
                Map<Integer, BigDecimal> durationPriceMap = new TreeMap<>();
                Boolean isContiguous = true;
                for (CalendarConfig config : configsForOffer) {

                    //make sure there is no gap between calendar configurations
                    if (endTime == null) {
                        //first run
                        endTime = getLocalDateTime(selectedDate, selectedTime).plusMinutes(config.getMinDuration());
                    } else {
                        //break if there are durations available and calendar configs are not contiguous
                        if (!durationPriceMap.isEmpty()) {
                            //we substract min interval before the comparison as it has been added during the last iteration
                            LocalDateTime configStartDateTime = getLocalDateTime(selectedDate,
                                    config.getStartTime());
                            if (!endTime.minusMinutes(config.getMinInterval()).equals(configStartDateTime)) {
                                break;
                            }
                        }
                    }

                    Integer interval = config.getMinInterval();

                    pricePerMinute = getPricePerMinute(config);

                    //as long as the endTime is before the end time configured in the calendar
                    LocalDateTime configEndDateTime = getLocalDateTime(selectedDate, config.getEndTime());
                    while (endTime.compareTo(configEndDateTime) <= 0) {
                        TimeSlot timeSlot = new TimeSlot();
                        timeSlot.setDate(selectedDate);
                        timeSlot.setStartTime(selectedTime);
                        timeSlot.setEndTime(endTime.toLocalTime());
                        timeSlot.setConfig(config);
                        Long bookingSlotsLeft = getBookingSlotsLeft(timeSlot, offer, confirmedBookings);

                        //we only allow contiguous bookings for any given offer
                        if (bookingSlotsLeft < 1) {
                            isContiguous = false;
                            break;
                        }

                        if (price == null) {
                            //see if previousConfig endTime - minInterval matches the selected time. if so, take half of the previous config price as a basis
                            if (previousConfig != null && previousConfig.getEndTime()
                                    .minusMinutes(previousConfig.getMinInterval()).equals(selectedTime)) {
                                BigDecimal previousConfigPricePerMinute = getPricePerMinute(previousConfig);
                                price = previousConfigPricePerMinute.multiply(
                                        new BigDecimal(previousConfig.getMinInterval()), MathContext.DECIMAL128);
                                price = price.add(pricePerMinute.multiply(
                                        new BigDecimal(duration - previousConfig.getMinInterval()),
                                        MathContext.DECIMAL128));
                            } else {
                                price = pricePerMinute.multiply(new BigDecimal(duration.toString()),
                                        MathContext.DECIMAL128);
                            }
                        } else {
                            //add price for additional interval
                            price = price.add(pricePerMinute.multiply(new BigDecimal(interval.toString()),
                                    MathContext.DECIMAL128));
                        }
                        price = price.setScale(2, RoundingMode.HALF_EVEN);
                        durationPriceMap.put(duration, price);

                        //increase the duration by the configured minimum interval and determine the new end time for the next iteration
                        duration += interval;
                        endTime = endTime.plusMinutes(interval);
                    }

                    if (!durationPriceMap.isEmpty()) {
                        OfferDurationPrice odp = new OfferDurationPrice();
                        odp.setOffer(offer);
                        odp.setDurationPriceMap(durationPriceMap);
                        odp.setConfig(config);
                        offerDurationPrices = odp;
                    }

                    if (!isContiguous) {
                        //we only allow coniguous bookings for one offer. process next offer
                        break;
                    }
                    previousConfig = config;

                }
            }
        }
        return offerDurationPrices;
    }

    public List<PaymentMethod> getActivePaymentMethods() {
        //determine valid payment methods
        List<PaymentMethod> paymentMethods = new ArrayList<>();
        //always support subscriptions, cash and vouchers
        paymentMethods.add(PaymentMethod.Subscription);
        paymentMethods.add(PaymentMethod.Cash);
        paymentMethods.add(PaymentMethod.ExternalVoucher);
        paymentMethods.add(PaymentMethod.Voucher);

        //check if PayPal config exists and is active
        PayPalConfig paypalConfig = payPalConfigDAO.findFirst();
        if (paypalConfig != null && paypalConfig.getActive()) {
            paymentMethods.add(PaymentMethod.PayPal);
        }

        //check if PayDirekt config exists and is active
        PayDirektConfig payDirektConfig = payDirektConfigDAO.findFirst();
        if (payDirektConfig != null && payDirektConfig.getActive()) {
            paymentMethods.add(PaymentMethod.PayDirekt);
        }

        //check if PayMill config exists
        PayMillConfig payMillConfig = payMillConfigDAO.findFirst();
        if (payMillConfig != null) {
            if (payMillConfig.getEnableDirectDebit()) {
                paymentMethods.add(PaymentMethod.DirectDebit);
            }
            if (payMillConfig.getEnableCreditCard()) {
                paymentMethods.add(PaymentMethod.CreditCard);
            }
        }
        return paymentMethods;
    }

    private Set<Attachment> getAttachments(Booking booking) {
        Set<Attachment> attachments = new HashSet<>();
        Attachment attachment = getCalendarAttachment(booking);
        if (attachment != null) {
            attachments.add(attachment);
        }
        return attachments;
    }

    private Attachment getCalendarAttachment(Booking booking) {
        try {
            LocalDate startLocalDate = booking.getEvent() == null ? booking.getBookingDate()
                    : booking.getEvent().getStartDate();
            //DateTime startUtcDate = startLocalDate.toDateTimeAtStartOfDay(Constants.DEFAULT_TIMEZONE);
            LocalTime startTime = booking.getEvent() == null ? booking.getBookingTime()
                    : booking.getEvent().getStartTime();
            LocalTime endTime = booking.getEvent() == null ? booking.getBookingEndTime()
                    : booking.getEvent().getStartTime().plusHours(2);
            String calendarEntryName = booking.getEvent() == null ? booking.getOffer().getName()
                    : booking.getEvent().getName();

            String organizerName = booking.getPlayer().toString();
            String organizerEmail = booking.getPlayer().getEmail();
            String summary = String.format("%s - %s", calendarEntryName, booking.getCustomer().getName());
            String dtstamp = FormatUtils.DATE_TIME_MACHINE_READABLE.print(DateTime.now(Constants.DEFAULT_TIMEZONE));
            String dtstart = FormatUtils.DATE_MACHINE_READABLE.print(startLocalDate)
                    + FormatUtils.TIME_MACHINE_READABLE.print(startTime);
            String dtend = FormatUtils.DATE_MACHINE_READABLE.print(startLocalDate)
                    + FormatUtils.TIME_MACHINE_READABLE.print(endTime);
            String uid = booking.getUUID().toString();
            String calendarTemplate = FileUtil.getFileContents("calendar_template.ics");
            String data = String.format(calendarTemplate, organizerName, organizerName, organizerEmail, summary,
                    summary, dtstamp, DEFAULT_TIMEZONE_STRING, dtstart, DEFAULT_TIMEZONE_STRING, dtend, uid);

            Attachment attachment = new Attachment();
            attachment.setType("text/calendar");
            attachment.setName(String.format("%s %s.ics", booking.getCustomer().getDomainName(),
                    calendarEntryName.replace(" ", "_")));
            attachment.setData(Base64.encodeBase64String(data.getBytes(StandardCharsets.UTF_8)));
            return attachment;
        } catch (Exception e) {
            LOG.error(e.getMessage(), e);
        }
        return null;
    }

}