com.ning.billing.invoice.generator.DefaultInvoiceGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.ning.billing.invoice.generator.DefaultInvoiceGenerator.java

Source

/*
 * Copyright 2010-2013 Ning, Inc.
 *
 * Ning licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.ning.billing.invoice.generator;

import java.awt.image.DataBufferUShort;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

import javax.annotation.Nullable;

import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.joda.time.Months;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoiceApiException;
import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.model.BillingMode;
import com.ning.billing.invoice.model.DefaultInvoice;
import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
import com.ning.billing.invoice.model.InAdvanceBillingMode;
import com.ning.billing.invoice.model.InvalidDateSequenceException;
import com.ning.billing.invoice.model.InvoicingConfiguration;
import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.invoice.model.RecurringInvoiceItemData;
import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.config.InvoiceConfig;
import com.ning.billing.util.svcapi.junction.BillingEvent;
import com.ning.billing.util.svcapi.junction.BillingEventSet;
import com.ning.billing.util.svcapi.junction.BillingModeType;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.inject.Inject;

/**
 * Terminology for repair scenarii:
 *
 * - A 'repaired' item is an item that was generated and that needs to be repaired because the plan changed for that subscription on that period of time
 * - The 'repair' item is the item that cancels the (to be) repaired item; the repair item amount might not match (to be) repaired item because:
 *   * the (to be) repaired item was already adjusted so we will only repair what is left
 *   * in case of partial repair we only repair the part that is not used
 * - The 'reparee' item is only present on disk-- in the existing item list -- in case of full repair; in that case it represents the portion of the item that should still
 *   be invoiced for the plan of the repaired item. In case of partial repair it is merged with the repair item and does not exist except as a virtual item in the proposed list
 *
 *
 *
 * Example. We had a 20 subscription for a given period; we charged that amount and later discovered that only 3/4 of the time period were used after which the subscription was cancelled (immediate canellation)
 *
 * Full repair logic:
 *
 * Invoice 1:                   Invoice 2:
 *           +20 (repaired)             +5 (reparee)
 *           -20 (repair)
 *
 * Partial repair logic:
 *
 * Invoice 1:                   Invoice 2: (N/A)
 *           +20 (repaired)
 *           -15 (repair)
 *
 * The current version of the code uses partial repair logic but is able to deal with 'full repair' scenarii.
 *
 */

public class DefaultInvoiceGenerator implements InvoiceGenerator {

    private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
    private static final int ROUNDING_MODE = InvoicingConfiguration.getRoundingMode();
    private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();

    private final Clock clock;
    private final InvoiceConfig config;

    @Inject
    public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config) {
        this.clock = clock;
        this.config = config;
    }

    /*
     * adjusts target date to the maximum invoice target date, if future invoices exist
     */
    @Override
    public Invoice generateInvoice(final UUID accountId, @Nullable final BillingEventSet events,
            @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
            final Currency targetCurrency) throws InvoiceApiException {
        if ((events == null) || (events.size() == 0) || events.isAccountAutoInvoiceOff()) {
            return null;
        }

        validateTargetDate(targetDate);

        final List<InvoiceItem> existingItems = new ArrayList<InvoiceItem>();
        if (existingInvoices != null) {
            for (final Invoice invoice : existingInvoices) {
                for (final InvoiceItem item : invoice.getInvoiceItems()) {
                    if (item.getSubscriptionId() == null || // Always include migration invoices, credits, external charges etc.
                            !events.getSubscriptionIdsWithAutoInvoiceOff().contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
                        existingItems.add(item);
                    }
                }
            }
        }

        final LocalDate adjustedTargetDate = adjustTargetDate(existingInvoices, targetDate);

        final Invoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), adjustedTargetDate,
                targetCurrency);
        final UUID invoiceId = invoice.getId();

        // Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
        final List<InvoiceItem> proposedItems = generateInvoiceItems(invoiceId, accountId, events,
                adjustedTargetDate, targetCurrency);

        // Remove repaired and repair items -- since they never change and can't be regenerated
        removeRepairedAndRepairInvoiceItems(existingItems, proposedItems);

        // Remove from both lists the items in common
        removeMatchingInvoiceItems(existingItems, proposedItems);

        // We don't want the Fixed items to be repaired -- as they are setup fees that should be paid
        removeRemainingFixedItemsFromExisting(existingItems);

        // Add repair items based on what is left in existing items
        addRepairItems(existingItems, proposedItems);

        // Finally add this new items on the new invoice
        invoice.addInvoiceItems(proposedItems);

        return proposedItems.size() != 0 ? invoice : null;
    }

    private void removeRemainingFixedItemsFromExisting(final List<InvoiceItem> existingItems) {
        final Iterator<InvoiceItem> it = existingItems.iterator();
        while (it.hasNext()) {
            final InvoiceItem cur = it.next();
            if (cur.getInvoiceItemType() == InvoiceItemType.FIXED) {
                it.remove();
            }
        }
    }

    /**
     * At this point either we have 0 existingItem left or those left need to be repaired
     *
     * @param existingItems the list of remaining existing items
     * @param proposedItems the list of remaining proposed items
     */
    void addRepairItems(final List<InvoiceItem> existingItems, final List<InvoiceItem> proposedItems) {
        for (final InvoiceItem existingItem : existingItems) {
            if (existingItem.getInvoiceItemType() == InvoiceItemType.RECURRING
                    || existingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
                final BigDecimal existingAdjustedPositiveAmount = getAdjustedPositiveAmount(existingItems,
                        existingItem.getId());
                final BigDecimal amountNegated = existingItem.getAmount() == null ? null
                        : existingItem.getAmount().subtract(existingAdjustedPositiveAmount).negate();
                if (amountNegated != null && amountNegated.compareTo(BigDecimal.ZERO) < 0) {
                    final RepairAdjInvoiceItem candidateRepairItem = new RepairAdjInvoiceItem(
                            existingItem.getInvoiceId(), existingItem.getAccountId(), existingItem.getStartDate(),
                            existingItem.getEndDate(), amountNegated, existingItem.getCurrency(),
                            existingItem.getId());
                    addRepairItem(existingItem, candidateRepairItem, proposedItems);
                }
            }
        }
    }

    /**
     * Add the repair item for the (yet to be) repairedItem. It will merge the candidateRepairItem with reparee item
     *
     *
     *
     * @param repairedItem        the item being repaired
     * @param candidateRepairItem the repair item we would have if we were to repair the full period
     * @param proposedItems       the list of proposed items
     */
    void addRepairItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem,
            final List<InvoiceItem> proposedItems) {

        int nbTotalRepaireeDays = 0;

        // totalRepareeItemAmount is negative and represents the portion left after we removed the adjustments for the total period for all the reparees combined
        BigDecimal totalRepareeItemAmount = candidateRepairItem.getAmount();
        final List<InvoiceItem> reparees = new ArrayList<InvoiceItem>();
        for (final InvoiceItem cur : proposedItems) {
            if (isRepareeItemForRepairedItem(repairedItem, cur)) {
                nbTotalRepaireeDays += Days.daysBetween(cur.getStartDate(), cur.getEndDate()).getDays();
                reparees.add(cur);
                totalRepareeItemAmount = totalRepareeItemAmount.add(cur.getAmount());
            }
        }
        int nbTotalRepairedDays = Days
                .daysBetween(candidateRepairItem.getStartDate(), candidateRepairItem.getEndDate()).getDays()
                - nbTotalRepaireeDays;

        // If we repaired the full period there is no repairee item
        if (reparees.size() == 0) {
            proposedItems.add(candidateRepairItem);
            return;
        }

        // Sort the reparees based on startDate in order to create the repair items -- based on the endDate (previous repairee) -> startDate (next reparee)
        Collections.sort(reparees, new Comparator<InvoiceItem>() {
            @Override
            public int compare(final InvoiceItem o1, final InvoiceItem o2) {
                return o1.getStartDate().compareTo(o2.getStartDate());
            }
        });

        //Build the reparees
        BigDecimal totalRepairItemAmount = BigDecimal.ZERO;
        List<InvoiceItem> repairedItems = new ArrayList<InvoiceItem>();
        InvoiceItem prevReparee = null;
        final Iterator<InvoiceItem> it = reparees.iterator();
        while (it.hasNext()) {
            final InvoiceItem nextReparee = it.next();
            if (prevReparee != null) {
                // repairItemAmount is an approximation of the exact amount by simply prorating totalRepareeItemAmount in the repair period; we make sure last item is calculated based
                // on what is left so the sum of all repairs amount is exactly correct
                final BigDecimal repairItemAmount = (nextReparee.getEndDate()
                        .compareTo(candidateRepairItem.getEndDate()) != 0)
                                ? InvoiceDateUtils
                                        .calculateProrationBetweenDates(prevReparee.getEndDate(),
                                                nextReparee.getStartDate(), nbTotalRepairedDays)
                                        .multiply(totalRepareeItemAmount)
                                : totalRepareeItemAmount.subtract(totalRepairItemAmount);
                totalRepairItemAmount = totalRepairItemAmount.add(repairItemAmount);
                final RepairAdjInvoiceItem repairItem = new RepairAdjInvoiceItem(candidateRepairItem.getInvoiceId(),
                        candidateRepairItem.getAccountId(), prevReparee.getEndDate(), nextReparee.getStartDate(),
                        repairItemAmount, candidateRepairItem.getCurrency(), repairedItem.getId());
                repairedItems.add(repairItem);
            }
            prevReparee = nextReparee;
        }

        // In case we end up with a repair up to the service endDate we need to add this extra item-- this is the 'classic' case with one repairee/repair item
        if (prevReparee.getEndDate().compareTo(candidateRepairItem.getEndDate()) != 0) {
            final BigDecimal repairItemAmount = totalRepareeItemAmount.subtract(totalRepairItemAmount);
            final RepairAdjInvoiceItem repairItem = new RepairAdjInvoiceItem(candidateRepairItem.getInvoiceId(),
                    candidateRepairItem.getAccountId(), prevReparee.getEndDate(), candidateRepairItem.getEndDate(),
                    repairItemAmount, candidateRepairItem.getCurrency(), repairedItem.getId());
            repairedItems.add(repairItem);
        }

        // Finally remove all reparees from the proposed items and add all repaired items in the invoice
        for (InvoiceItem reparee : reparees) {
            proposedItems.remove(reparee);
        }
        proposedItems.addAll(repairedItems);
    }

    /**
     * Check whether or not the invoiceItem passed is the reparee for that repaired invoice item
     *
     * @param repairedInvoiceItem the repaired invoice item
     * @param invoiceItem         any invoice item to compare to
     * @return true if invoiceItem is the reparee for that repaired invoice item
     */
    @VisibleForTesting
    boolean isRepareeItemForRepairedItem(final InvoiceItem repairedInvoiceItem, final InvoiceItem invoiceItem) {
        return !repairedInvoiceItem.getId().equals(invoiceItem.getId())
                && repairedInvoiceItem.getInvoiceItemType().equals(invoiceItem.getInvoiceItemType()) &&
                // We assume the items are correctly created, so that the subscription id check implicitly
                // verifies that account id and bundle id matches
                repairedInvoiceItem.getSubscriptionId().equals(invoiceItem.getSubscriptionId()) &&
                // service period for reparee should be included in service period of repaired-- true for startDate and endDate
                repairedInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) <= 0 &&
                // Similarly, check the "portion used" is less than the original service end date. The check
                // is strict, otherwise there wouldn't be anything to repair
                ((repairedInvoiceItem.getEndDate() == null && invoiceItem.getEndDate() == null)
                        || (repairedInvoiceItem.getEndDate() != null && invoiceItem.getEndDate() != null
                                && repairedInvoiceItem.getEndDate().compareTo(invoiceItem.getEndDate()) >= 0))
                &&
                // Finally, for the tricky part... In case of complete repairs, the new item will always meet all of the
                // following conditions: same type, subscription, start date. Depending on the catalog configuration, the end
                // date check could also match (e.g. repair from annual to monthly). For that scenario, we need to default
                // to catalog checks (the rate check is a lame check for versioned catalogs).
                Objects.firstNonNull(repairedInvoiceItem.getPlanName(), "")
                        .equals(Objects.firstNonNull(invoiceItem.getPlanName(), ""))
                && Objects.firstNonNull(repairedInvoiceItem.getPhaseName(), "")
                        .equals(Objects.firstNonNull(invoiceItem.getPhaseName(), ""))
                && Objects.firstNonNull(repairedInvoiceItem.getRate(), BigDecimal.ZERO)
                        .compareTo(Objects.firstNonNull(invoiceItem.getRate(), BigDecimal.ZERO)) == 0;
    }

    // We check to see if there are any adjustments that point to the item we are trying to repair
    // If we did any CREDIT_ADJ or REFUND_ADJ, then we unfortunately we can't know what is the intent
    // was as it applies to the full Invoice, so we ignore it. That might result in an extra positive CBA
    // that would have to be corrected manually. This is the best we can do, and administrators should always
    // use ITEM_ADJUSTMENT rather than CREDIT_ADJ or REFUND_ADJ when possible.
    //
    BigDecimal getAdjustedPositiveAmount(final List<InvoiceItem> existingItems, final UUID linkedItemId) {
        BigDecimal totalAdjustedOnItem = BigDecimal.ZERO;
        final Collection<InvoiceItem> invoiceItems = Collections2.filter(existingItems,
                new Predicate<InvoiceItem>() {
                    @Override
                    public boolean apply(final InvoiceItem item) {
                        return item.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ
                                && item.getLinkedItemId() != null && item.getLinkedItemId().equals(linkedItemId);
                    }
                });

        for (final InvoiceItem invoiceItem : invoiceItems) {
            totalAdjustedOnItem = totalAdjustedOnItem.add(invoiceItem.getAmount());
        }
        return totalAdjustedOnItem.negate();
    }

    private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
        final int maximumNumberOfMonths = config.getNumberOfMonthsInFuture();

        if (Months.monthsBetween(clock.getUTCToday(), targetDate).getMonths() > maximumNumberOfMonths) {
            throw new InvoiceApiException(ErrorCode.INVOICE_TARGET_DATE_TOO_FAR_IN_THE_FUTURE,
                    targetDate.toString());
        }
    }

    private LocalDate adjustTargetDate(final List<Invoice> existingInvoices, final LocalDate targetDate) {
        if (existingInvoices == null) {
            return targetDate;
        }

        LocalDate maxDate = targetDate;

        for (final Invoice invoice : existingInvoices) {
            if (invoice.getTargetDate().isAfter(maxDate)) {
                maxDate = invoice.getTargetDate();
            }
        }
        return maxDate;
    }

    /*
     * Removes all matching items from both submitted collections
     */
    void removeMatchingInvoiceItems(final List<InvoiceItem> existingInvoiceItems,
            final List<InvoiceItem> proposedItems) {
        // We can't just use sets here as order matters (we want to keep duplicated in existingInvoiceItems)
        final Iterator<InvoiceItem> proposedItemIterator = proposedItems.iterator();
        while (proposedItemIterator.hasNext()) {
            final InvoiceItem proposedItem = proposedItemIterator.next();

            final Iterator<InvoiceItem> existingItemIterator = existingInvoiceItems.iterator();
            while (existingItemIterator.hasNext()) {
                final InvoiceItem existingItem = existingItemIterator.next();
                if (existingItem.matches(proposedItem)) {
                    existingItemIterator.remove();
                    proposedItemIterator.remove();
                    break;
                }
            }
        }
    }

    /**
     * Remove from the existing item list all repaired items-- both repaired and repair
     * If this is a partial repair, we also need to find the reparee from the proposed list
     * and remove it.
     *
     * @param existingItems input list of existing items
     * @param proposedItems input list of proposed item
     */
    void removeRepairedAndRepairInvoiceItems(final List<InvoiceItem> existingItems,
            final List<InvoiceItem> proposedItems) {

        final List<UUID> itemsToRemove = new ArrayList<UUID>();
        for (final InvoiceItem item : existingItems) {
            if (item.getInvoiceItemType() == InvoiceItemType.REPAIR_ADJ) {
                itemsToRemove.add(item.getId());
                itemsToRemove.add(item.getLinkedItemId());

                final InvoiceItem repairedInvoiceItem = getRepairedInvoiceItem(item.getLinkedItemId(),
                        existingItems);
                // if this is a full repair there is no reparee so nothing to remove; if not reparee needs to be removed from proposed list
                if (!isFullRepair(repairedInvoiceItem, item, existingItems)) {
                    removeProposedRepareeForPartialrepair(repairedInvoiceItem, proposedItems);

                }
            }
        }
        final Iterator<InvoiceItem> iterator = existingItems.iterator();
        while (iterator.hasNext()) {
            final InvoiceItem item = iterator.next();
            if (itemsToRemove.contains(item.getId())) {
                iterator.remove();
            }
        }
    }

    /**
     * A full repair is one when the whole period was repaired. we reconstruct all the adjustment + repair pointing to the repaired item
     * and if the amount matches this is a full repair.
     *
     * @param repairedItem  the repaired item
     * @param repairItem    the repair item
     * @param existingItems the list of existing items
     * @return true if this is a full repair.
     */
    private boolean isFullRepair(final InvoiceItem repairedItem, final InvoiceItem repairItem,
            final List<InvoiceItem> existingItems) {

        final BigDecimal adjustedPositiveAmount = getAdjustedPositiveAmount(existingItems, repairedItem.getId());
        final BigDecimal repairAndAdjustedPositiveAmount = repairItem.getAmount().negate()
                .add(adjustedPositiveAmount);
        return (repairedItem.getAmount().compareTo(repairAndAdjustedPositiveAmount) == 0);
    }

    /**
     * Removes the reparee from proposed list of items if it exists.
     *
     * @param repairedItem  the repaired item
     * @param proposedItems the list of existing items
     */
    protected void removeProposedRepareeForPartialrepair(final InvoiceItem repairedItem,
            final List<InvoiceItem> proposedItems) {
        final Iterator<InvoiceItem> it = proposedItems.iterator();
        while (it.hasNext()) {
            final InvoiceItem cur = it.next();
            if (isRepareeItemForRepairedItem(repairedItem, cur)) {
                it.remove();
                break;
            }
        }
    }

    private InvoiceItem getRepairedInvoiceItem(final UUID repairedInvoiceItemId,
            final List<InvoiceItem> existingItems) {
        for (InvoiceItem cur : existingItems) {
            if (cur.getId().equals(repairedInvoiceItemId)) {
                return cur;
            }
        }
        throw new IllegalStateException("Cannot find repaired invoice item " + repairedInvoiceItemId);
    }

    private List<InvoiceItem> generateInvoiceItems(final UUID invoiceId, final UUID accountId,
            final BillingEventSet events, final LocalDate targetDate, final Currency currency)
            throws InvoiceApiException {
        final List<InvoiceItem> items = new ArrayList<InvoiceItem>();

        if (events.size() == 0) {
            return items;
        }

        // Pretty-print the generated invoice items from the junction events
        final StringBuilder logStringBuilder = new StringBuilder("Invoice items generated for invoiceId ")
                .append(invoiceId).append(" and accountId ").append(accountId);

        final Iterator<BillingEvent> eventIt = events.iterator();
        BillingEvent nextEvent = eventIt.next();
        while (eventIt.hasNext()) {
            final BillingEvent thisEvent = nextEvent;
            nextEvent = eventIt.next();
            if (!events.getSubscriptionIdsWithAutoInvoiceOff().contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
                final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent
                        .getSubscription().getId()) ? nextEvent : null;
                items.addAll(processEvents(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency,
                        logStringBuilder));
            }
        }
        items.addAll(processEvents(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder));

        log.info(logStringBuilder.toString());

        return items;
    }

    // Turn a set of events into a list of invoice items. Note that the dates on the invoice items will be rounded (granularity of a day)
    private List<InvoiceItem> processEvents(final UUID invoiceId, final UUID accountId,
            final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent, final LocalDate targetDate,
            final Currency currency, final StringBuilder logStringBuilder) throws InvoiceApiException {
        final List<InvoiceItem> items = new ArrayList<InvoiceItem>();

        // Handle fixed price items
        final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent,
                targetDate, currency);
        if (fixedPriceInvoiceItem != null) {
            items.add(fixedPriceInvoiceItem);
        }

        // Handle recurring items
        final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
        if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
            final BillingMode billingMode = instantiateBillingMode(thisEvent.getBillingMode());
            final LocalDate startDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());

            if (!startDate.isAfter(targetDate)) {
                final LocalDate endDate = (nextEvent == null) ? null
                        : new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone());

                final int billCycleDayLocal = thisEvent.getBillCycleDayLocal();

                final List<RecurringInvoiceItemData> itemData;
                try {
                    itemData = billingMode.calculateInvoiceItemData(startDate, endDate, targetDate,
                            billCycleDayLocal, billingPeriod);
                } catch (InvalidDateSequenceException e) {
                    throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate,
                            targetDate);
                }

                for (final RecurringInvoiceItemData itemDatum : itemData) {
                    final BigDecimal rate = thisEvent.getRecurringPrice();

                    if (rate != null) {
                        final BigDecimal amount = itemDatum.getNumberOfCycles().multiply(rate)
                                .setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE);

                        final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId, accountId,
                                thisEvent.getSubscription().getBundleId(), thisEvent.getSubscription().getId(),
                                thisEvent.getPlan().getName(), thisEvent.getPlanPhase().getName(),
                                itemDatum.getStartDate(), itemDatum.getEndDate(), amount, rate, currency);
                        items.add(recurringItem);
                    }
                }
            }
        }

        // For debugging purposes
        logStringBuilder.append("\n").append(thisEvent);
        for (final InvoiceItem item : items) {
            logStringBuilder.append("\n\t").append(item);
        }

        return items;
    }

    private BillingMode instantiateBillingMode(final BillingModeType billingMode) {
        switch (billingMode) {
        case IN_ADVANCE:
            return new InAdvanceBillingMode();
        default:
            throw new UnsupportedOperationException();
        }
    }

    InvoiceItem generateFixedPriceItem(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent,
            final LocalDate targetDate, final Currency currency) {
        final LocalDate roundedStartDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());

        if (roundedStartDate.isAfter(targetDate)) {
            return null;
        } else {
            final BigDecimal fixedPrice = thisEvent.getFixedPrice();

            if (fixedPrice != null) {
                return new FixedPriceInvoiceItem(invoiceId, accountId, thisEvent.getSubscription().getBundleId(),
                        thisEvent.getSubscription().getId(), thisEvent.getPlan().getName(),
                        thisEvent.getPlanPhase().getName(), roundedStartDate, fixedPrice, currency);
            } else {
                return null;
            }
        }
    }
}