killbill-aplcache

Details

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index 2d6e805..6447349 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -18,11 +18,7 @@
 
 package org.killbill.billing.invoice.generator;
 
-import java.math.BigDecimal;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -34,61 +30,33 @@ import org.joda.time.Months;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.callcontext.InternalCallContext;
-import org.killbill.billing.catalog.api.BillingMode;
-import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.catalog.api.Usage;
-import org.killbill.billing.catalog.api.UsageType;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
 import org.killbill.billing.invoice.model.DefaultInvoice;
-import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
-import org.killbill.billing.invoice.model.InvalidDateSequenceException;
-import org.killbill.billing.invoice.model.RecurringInvoiceItem;
-import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
-import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
-import org.killbill.billing.invoice.tree.AccountItemTree;
-import org.killbill.billing.invoice.usage.RawUsageOptimizer;
-import org.killbill.billing.invoice.usage.RawUsageOptimizer.RawUsageOptimizerResult;
-import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear;
-import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.SubscriptionConsumableInArrearItemsAndNextNotificationDate;
-import org.killbill.billing.junction.BillingEvent;
 import org.killbill.billing.junction.BillingEventSet;
 import org.killbill.billing.util.config.InvoiceConfig;
-import org.killbill.billing.util.currency.KillBillMoney;
 import org.killbill.clock.Clock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.inject.Inject;
 
 public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
-    private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
-
     private final Clock clock;
     private final InvoiceConfig config;
-    private final RawUsageOptimizer rawUsageOptimizer;
-    final BillingModeGenerator billingModeGenerator;
+
+    private final FixedAndRecurringInvoiceItemGenerator recurringInvoiceItemGenerator;
+    private final UsageInvoiceItemGenerator usageInvoiceItemGenerator;
 
     @Inject
-    public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config, final RawUsageOptimizer rawUsageOptimizer) {
+    public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config, final FixedAndRecurringInvoiceItemGenerator recurringInvoiceItemGenerator, final UsageInvoiceItemGenerator usageInvoiceItemGenerator) {
         this.clock = clock;
         this.config = config;
-        this.rawUsageOptimizer = rawUsageOptimizer;
-        this.billingModeGenerator = new DefaultBillingModeGenerator();
+        this.recurringInvoiceItemGenerator = recurringInvoiceItemGenerator;
+        this.usageInvoiceItemGenerator = usageInvoiceItemGenerator;
     }
 
     /*
@@ -110,159 +78,15 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         final UUID invoiceId = invoice.getId();
         final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
 
-        final List<InvoiceItem> fixedAndRecurringItems = generateFixedAndRecurringInvoiceItems(account.getId(), invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates);
+        final List<InvoiceItem> fixedAndRecurringItems = recurringInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
         invoice.addInvoiceItems(fixedAndRecurringItems);
 
-        final List<InvoiceItem> usageItems = generateUsageConsumableInArrearItems(account, invoiceId, events, existingInvoices, targetDate, perSubscriptionFutureNotificationDates, context);
+        final List<InvoiceItem> usageItems = usageInvoiceItemGenerator.generateItems(account, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates, context);
         invoice.addInvoiceItems(usageItems);
 
         return new InvoiceWithMetadata(invoice.getInvoiceItems().isEmpty() ? null : invoice, perSubscriptionFutureNotificationDates);
     }
 
-    private List<InvoiceItem> generateUsageConsumableInArrearItems(final Account account,
-                                                                   final UUID invoiceId, final BillingEventSet eventSet,
-                                                                   @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
-                                                                   final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates,
-                                                                   final InternalCallContext internalCallContext) throws InvoiceApiException {
-
-        final Map<UUID, List<InvoiceItem>> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices);
-        try {
-            final List<InvoiceItem> items = Lists.newArrayList();
-            final Iterator<BillingEvent> events = eventSet.iterator();
-
-            RawUsageOptimizerResult rawUsageOptimizerResult = null;
-            List<BillingEvent> curEvents = Lists.newArrayList();
-            UUID curSubscriptionId = null;
-            while (events.hasNext()) {
-                final BillingEvent event = events.next();
-                // Skip events that are posterior to the targetDate
-                final LocalDate eventLocalEffectiveDate = new LocalDate(event.getEffectiveDate(), event.getAccount().getTimeZone());
-                if (eventLocalEffectiveDate.isAfter(targetDate)) {
-                    continue;
-                }
-
-                // Optimize to do the usage query only once after we know there are indeed some usage items
-                if (rawUsageOptimizerResult == null &&
-                    Iterables.any(event.getUsages(), new Predicate<Usage>() {
-                        @Override
-                        public boolean apply(@Nullable final Usage input) {
-                            return (input.getUsageType() == UsageType.CONSUMABLE &&
-                                    input.getBillingMode() == BillingMode.IN_ARREAR);
-                        }
-                    })) {
-                    rawUsageOptimizerResult = rawUsageOptimizer.getConsumableInArrearUsage(new LocalDate(event.getEffectiveDate(), account.getTimeZone()), targetDate, Iterables.concat(perSubscriptionConsumableInArrearUsageItems.values()), eventSet.getUsages(), internalCallContext);
-                }
-
-                // None of the billing events report any usage (CONSUMABLE/IN_ARREAR) sections
-                if (rawUsageOptimizerResult == null) {
-                    continue;
-                }
-
-                final UUID subscriptionId = event.getSubscription().getId();
-                if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
-                    final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
-                    final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
-
-                    final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
-                    final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
-                    items.addAll(newInArrearUsageItems);
-                    updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
-                    curEvents = Lists.newArrayList();
-                }
-                curSubscriptionId = subscriptionId;
-                curEvents.add(event);
-            }
-            if (curSubscriptionId != null) {
-                final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
-                final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
-
-                final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
-                final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
-                items.addAll(newInArrearUsageItems);
-                updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
-            }
-            return items;
-
-        } catch (CatalogApiException e) {
-            throw new InvoiceApiException(e);
-        }
-    }
-
-    private void updatePerSubscriptionNextNotificationUsageDate(final UUID subscriptionId, final Map<String, LocalDate> nextBillingCycleDates, final BillingMode usageBillingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
-        if (usageBillingMode == BillingMode.IN_ADVANCE) {
-            throw new IllegalStateException("Not implemented Yet)");
-        }
-
-        SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
-        if (subscriptionFutureNotificationDates == null) {
-            subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(null);
-            perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
-        }
-        for (final String usageName : nextBillingCycleDates.keySet()) {
-            subscriptionFutureNotificationDates.updateNextUsageDateIfRequired(usageName, usageBillingMode, nextBillingCycleDates.get(usageName));
-        }
-    }
-
-    private Map<UUID, List<InvoiceItem>> extractPerSubscriptionExistingConsumableInArrearUsageItems(final Map<String, Usage> knownUsage, @Nullable final List<Invoice> existingInvoices) {
-
-        if (existingInvoices == null || existingInvoices.isEmpty()) {
-            return ImmutableMap.of();
-        }
-
-        final Map<UUID, List<InvoiceItem>> result = new HashMap<UUID, List<InvoiceItem>>();
-        final Iterable<InvoiceItem> usageConsumableInArrearItems = Iterables.concat(Iterables.transform(existingInvoices, new Function<Invoice, Iterable<InvoiceItem>>() {
-            @Override
-            public Iterable<InvoiceItem> apply(final Invoice input) {
-
-                return Iterables.filter(input.getInvoiceItems(), new Predicate<InvoiceItem>() {
-                    @Override
-                    public boolean apply(final InvoiceItem input) {
-                        if (input.getInvoiceItemType() == InvoiceItemType.USAGE) {
-                            final Usage usage = knownUsage.get(input.getUsageName());
-                            return usage.getUsageType() == UsageType.CONSUMABLE && usage.getBillingMode() == BillingMode.IN_ARREAR;
-                        }
-                        return false;
-                    }
-                });
-            }
-        }));
-
-        for (InvoiceItem cur : usageConsumableInArrearItems) {
-            List<InvoiceItem> perSubscriptionUsageItems = result.get(cur.getSubscriptionId());
-            if (perSubscriptionUsageItems == null) {
-                perSubscriptionUsageItems = new LinkedList<InvoiceItem>();
-                result.put(cur.getSubscriptionId(), perSubscriptionUsageItems);
-            }
-            perSubscriptionUsageItems.add(cur);
-        }
-        return result;
-    }
-
-    private List<InvoiceItem> generateFixedAndRecurringInvoiceItems(final UUID accountId, final UUID invoiceId, final BillingEventSet eventSet,
-                                                                    @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
-                                                                    final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
-        final AccountItemTree accountItemTree = new AccountItemTree(accountId, invoiceId);
-        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.
-                        !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
-                                 .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
-                        accountItemTree.addExistingItem(item);
-                    }
-                }
-            }
-        }
-
-        // 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 = new ArrayList<InvoiceItem>();
-        processRecurringBillingEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate);
-        processFixedBillingEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems);
-
-        accountItemTree.mergeWithProposedItems(proposedItems);
-        return accountItemTree.getResultingItemList();
-    }
-
     private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
         final int maximumNumberOfMonths = config.getNumberOfMonthsInFuture();
 
@@ -285,162 +109,4 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         }
         return maxDate;
     }
-
-    private List<InvoiceItem> processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
-                                                            final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems,
-                                                            final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
-
-        if (events.size() == 0) {
-            return proposedItems;
-        }
-
-        // Pretty-print the generated invoice items from the junction events
-        final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items 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;
-                final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
-                proposedItems.addAll(newProposedItems);
-            }
-        }
-        final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
-        proposedItems.addAll(newProposedItems);
-
-        log.info(logStringBuilder.toString());
-
-        return proposedItems;
-    }
-
-    private List<InvoiceItem> processFixedBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) {
-        final Iterator<BillingEvent> eventIt = events.iterator();
-        while (eventIt.hasNext()) {
-            final BillingEvent thisEvent = eventIt.next();
-
-            final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency);
-            if (fixedPriceInvoiceItem != null) {
-                proposedItems.add(fixedPriceInvoiceItem);
-            }
-        }
-        return proposedItems;
-    }
-
-    // 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> processRecurringEvent(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
-                                                    final LocalDate targetDate, final Currency currency,
-                                                    final StringBuilder logStringBuilder, final BillingMode billingMode,
-                                                    final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
-        final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
-
-        // Handle recurring items
-        final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
-        if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
-            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 RecurringInvoiceItemDataWithNextBillingCycleDate itemDataWithNextBillingCycleDate;
-                try {
-                    itemDataWithNextBillingCycleDate = billingModeGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod, billingMode);
-                } catch (InvalidDateSequenceException e) {
-                    throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
-                }
-
-                // STEPH Move all this logic into RecurringInvoiceItemData
-                for (final RecurringInvoiceItemData itemDatum : itemDataWithNextBillingCycleDate.getItemData()) {
-                    final BigDecimal rate = thisEvent.getRecurringPrice();
-
-                    if (rate != null) {
-                        final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
-
-                        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);
-                    }
-                }
-                updatePerSubscriptionNextNotificationDate(thisEvent.getSubscription().getId(), itemDataWithNextBillingCycleDate.getNextBillingCycleDate(), items, billingMode, perSubscriptionFutureNotificationDate);
-
-            }
-        }
-
-        // For debugging purposes
-        logStringBuilder.append("\n")
-                        .append(thisEvent);
-        for (final InvoiceItem item : items) {
-            logStringBuilder.append("\n\t")
-                            .append(item);
-        }
-        return items;
-    }
-
-    private void updatePerSubscriptionNextNotificationDate(final UUID subscriptionId, final LocalDate nextBillingCycleDate, final List<InvoiceItem> newProposedItems, final BillingMode billingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
-
-        LocalDate nextNotificationDate = null;
-        switch (billingMode) {
-            case IN_ADVANCE:
-                for (final InvoiceItem item : newProposedItems) {
-                    if ((item.getEndDate() != null) &&
-                        (item.getAmount() == null ||
-                         item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
-                        if (nextNotificationDate == null) {
-                            nextNotificationDate = item.getEndDate();
-                        } else {
-                            nextNotificationDate = nextNotificationDate.compareTo(item.getEndDate()) > 0 ? nextNotificationDate : item.getEndDate();
-                        }
-                    }
-                }
-                break;
-            case IN_ARREAR:
-                nextNotificationDate = nextBillingCycleDate;
-                break;
-            default:
-                throw new IllegalStateException("Unrecognized billing mode " + billingMode);
-        }
-        if (nextNotificationDate != null) {
-            SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
-            if (subscriptionFutureNotificationDates == null) {
-                subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(billingMode);
-                perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
-            }
-            subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(nextNotificationDate);
-
-        }
-    }
-
-    private 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;
-            }
-        }
-    }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
new file mode 100644
index 0000000..19ceeee
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.generator;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
+import org.killbill.billing.invoice.tree.AccountItemTree;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.util.currency.KillBillMoney;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationBeforeFirstBillingPeriod;
+
+public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator {
+
+    private static final Logger log = LoggerFactory.getLogger(FixedAndRecurringInvoiceItemGenerator.class);
+
+    @Inject
+    public FixedAndRecurringInvoiceItemGenerator() {
+    }
+
+    public List<InvoiceItem> generateItems(final Account account, final UUID invoiceId, final BillingEventSet eventSet,
+                                           @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
+                                           final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate,
+                                           final InternalCallContext internalCallContext) throws InvoiceApiException {
+        final AccountItemTree accountItemTree = new AccountItemTree(account.getId(), invoiceId);
+        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.
+                        !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
+                                 .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
+                        accountItemTree.addExistingItem(item);
+                    }
+                }
+            }
+        }
+
+        // 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 = new ArrayList<InvoiceItem>();
+        processRecurringBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate);
+        processFixedBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems);
+
+        accountItemTree.mergeWithProposedItems(proposedItems);
+        return accountItemTree.getResultingItemList();
+    }
+
+    private List<InvoiceItem> processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
+                                                            final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems,
+                                                            final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
+
+        if (events.size() == 0) {
+            return proposedItems;
+        }
+
+        // Pretty-print the generated invoice items from the junction events
+        final StringBuilder logStringBuilder = new StringBuilder("Proposed Invoice items 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;
+                final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
+                proposedItems.addAll(newProposedItems);
+            }
+        }
+        final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode(), perSubscriptionFutureNotificationDate);
+        proposedItems.addAll(newProposedItems);
+
+        log.info(logStringBuilder.toString());
+
+        return proposedItems;
+    }
+
+    private List<InvoiceItem> processFixedBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) {
+        final Iterator<BillingEvent> eventIt = events.iterator();
+        while (eventIt.hasNext()) {
+            final BillingEvent thisEvent = eventIt.next();
+
+            final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency);
+            if (fixedPriceInvoiceItem != null) {
+                proposedItems.add(fixedPriceInvoiceItem);
+            }
+        }
+        return proposedItems;
+    }
+
+    // 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> processRecurringEvent(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
+                                                    final LocalDate targetDate, final Currency currency,
+                                                    final StringBuilder logStringBuilder, final BillingMode billingMode,
+                                                    final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
+        final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+
+        // Handle recurring items
+        final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
+        if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
+            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 RecurringInvoiceItemDataWithNextBillingCycleDate itemDataWithNextBillingCycleDate;
+                try {
+                    itemDataWithNextBillingCycleDate = generateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod, billingMode);
+                } catch (InvalidDateSequenceException e) {
+                    throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
+                }
+
+                for (final RecurringInvoiceItemData itemDatum : itemDataWithNextBillingCycleDate.getItemData()) {
+                    final BigDecimal rate = thisEvent.getRecurringPrice();
+
+                    if (rate != null) {
+                        final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
+
+                        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);
+                    }
+                }
+                updatePerSubscriptionNextNotificationDate(thisEvent.getSubscription().getId(), itemDataWithNextBillingCycleDate.getNextBillingCycleDate(), items, billingMode, perSubscriptionFutureNotificationDate);
+
+            }
+        }
+
+        // For debugging purposes
+        logStringBuilder.append("\n")
+                        .append(thisEvent);
+        for (final InvoiceItem item : items) {
+            logStringBuilder.append("\n\t")
+                            .append(item);
+        }
+        return items;
+    }
+
+    private void updatePerSubscriptionNextNotificationDate(final UUID subscriptionId, final LocalDate nextBillingCycleDate, final List<InvoiceItem> newProposedItems, final BillingMode billingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+
+        LocalDate nextNotificationDate = null;
+        switch (billingMode) {
+            case IN_ADVANCE:
+                for (final InvoiceItem item : newProposedItems) {
+                    if ((item.getEndDate() != null) &&
+                        (item.getAmount() == null ||
+                         item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+                        if (nextNotificationDate == null) {
+                            nextNotificationDate = item.getEndDate();
+                        } else {
+                            nextNotificationDate = nextNotificationDate.compareTo(item.getEndDate()) > 0 ? nextNotificationDate : item.getEndDate();
+                        }
+                    }
+                }
+                break;
+            case IN_ARREAR:
+                nextNotificationDate = nextBillingCycleDate;
+                break;
+            default:
+                throw new IllegalStateException("Unrecognized billing mode " + billingMode);
+        }
+        if (nextNotificationDate != null) {
+            SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
+            if (subscriptionFutureNotificationDates == null) {
+                subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(billingMode);
+                perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
+            }
+            subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(nextNotificationDate);
+
+        }
+    }
+
+    public RecurringInvoiceItemDataWithNextBillingCycleDate generateInvoiceItemData(final LocalDate startDate, @Nullable final LocalDate endDate,
+                                                                                    final LocalDate targetDate,
+                                                                                    final int billingCycleDayLocal,
+                                                                                    final BillingPeriod billingPeriod,
+                                                                                    final BillingMode billingMode) throws InvalidDateSequenceException {
+        if (endDate != null && endDate.isBefore(startDate)) {
+            throw new InvalidDateSequenceException();
+        }
+        if (targetDate.isBefore(startDate)) {
+            throw new InvalidDateSequenceException();
+        }
+
+        final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
+
+        final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod, billingMode);
+
+        // We are not billing for less than a day
+        if (!billingIntervalDetail.hasSomethingToBill()) {
+            return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail);
+        }
+        //
+        // If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do
+        // is to charge for that period
+        //
+        if (endDate != null && !endDate.isAfter(billingIntervalDetail.getFirstBillingCycleDate())) {
+            final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod);
+            final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods);
+            results.add(itemData);
+            return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail);
+        }
+
+        //
+        // Leading proration if
+        // i) The first firstBillingCycleDate is strictly after our start date AND
+        // ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check)
+        //
+        if (billingIntervalDetail.getFirstBillingCycleDate().isAfter(startDate)) {
+            final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, billingIntervalDetail.getFirstBillingCycleDate(), billingPeriod);
+            if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
+                // Not common - add info in the logs for debugging purposes
+                final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, billingIntervalDetail.getFirstBillingCycleDate(), leadingProRationPeriods);
+                log.info("Adding pro-ration: {}", itemData);
+                results.add(itemData);
+            }
+        }
+
+        //
+        // Calculate the effectiveEndDate from the firstBillingCycleDate:
+        // - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration
+        // - If not, this is the last billingCycleDate calculation right after the targetDate
+        //
+        final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate();
+
+        //
+        // Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods
+        //
+        final LocalDate lastBillingCycleDate = billingIntervalDetail.getLastBillingCycleDate();
+        final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(billingIntervalDetail.getFirstBillingCycleDate(), lastBillingCycleDate, billingPeriod);
+
+        for (int i = 0; i < numberOfWholeBillingPeriods; i++) {
+            final LocalDate servicePeriodStartDate;
+            if (results.size() > 0) {
+                // Make sure the periods align, especially with the pro-ration calculations above
+                servicePeriodStartDate = results.get(results.size() - 1).getEndDate();
+            } else if (i == 0) {
+                // Use the specified start date
+                servicePeriodStartDate = startDate;
+            } else {
+                throw new IllegalStateException("We should at least have one invoice item!");
+            }
+
+            // Make sure to align the end date with the BCD
+            final LocalDate servicePeriodEndDate = billingIntervalDetail.getFutureBillingDateFor(i + 1);
+            results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE));
+        }
+
+        //
+        // Now we check if indeed we need a trailing proration and add that incomplete item
+        //
+        if (effectiveEndDate.isAfter(lastBillingCycleDate)) {
+            final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod);
+            if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
+                // Not common - add info in the logs for debugging purposes
+                final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods);
+                log.info("Adding trailing pro-ration: {}", itemData);
+                results.add(itemData);
+            }
+        }
+        return new RecurringInvoiceItemDataWithNextBillingCycleDate(results, billingIntervalDetail);
+    }
+
+    private 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;
+            }
+        }
+    }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
index 3ff29a6..09f41ee 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
@@ -31,7 +31,6 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.junction.BillingEventSet;
 
 public interface InvoiceGenerator {
-
-    public InvoiceWithMetadata generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
-                                   LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
+    InvoiceWithMetadata generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
+                                        LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
new file mode 100644
index 0000000..6009549
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.generator;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.junction.BillingEventSet;
+
+public abstract class InvoiceItemGenerator {
+
+    public abstract List<InvoiceItem> generateItems(final Account account, final UUID invoiceId, final BillingEventSet eventSet,
+                                                    @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
+                                                    final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate,
+                                                    final InternalCallContext context) throws InvoiceApiException;
+
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
new file mode 100644
index 0000000..7750be8
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/UsageInvoiceItemGenerator.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.generator;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.catalog.api.UsageType;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.usage.RawUsageOptimizer;
+import org.killbill.billing.invoice.usage.RawUsageOptimizer.RawUsageOptimizerResult;
+import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear;
+import org.killbill.billing.invoice.usage.SubscriptionConsumableInArrear.SubscriptionConsumableInArrearItemsAndNextNotificationDate;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+public class UsageInvoiceItemGenerator extends InvoiceItemGenerator {
+
+    private static final Logger log = LoggerFactory.getLogger(UsageInvoiceItemGenerator.class);
+
+    private final RawUsageOptimizer rawUsageOptimizer;
+
+    @Inject
+    public UsageInvoiceItemGenerator(final RawUsageOptimizer rawUsageOptimizer) {
+        this.rawUsageOptimizer = rawUsageOptimizer;
+    }
+
+
+    @Override
+    public List<InvoiceItem> generateItems(final Account account,
+                                           final UUID invoiceId,
+                                           final BillingEventSet eventSet,
+                                           @Nullable final List<Invoice> existingInvoices,
+                                           final LocalDate targetDate,
+                                           final Currency targetCurrency,
+                                           final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates,
+                                           final InternalCallContext internalCallContext) throws InvoiceApiException {
+
+        final Map<UUID, List<InvoiceItem>> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices);
+        try {
+            final List<InvoiceItem> items = Lists.newArrayList();
+            final Iterator<BillingEvent> events = eventSet.iterator();
+
+            RawUsageOptimizerResult rawUsageOptimizerResult = null;
+            List<BillingEvent> curEvents = Lists.newArrayList();
+            UUID curSubscriptionId = null;
+            while (events.hasNext()) {
+                final BillingEvent event = events.next();
+                // Skip events that are posterior to the targetDate
+                final LocalDate eventLocalEffectiveDate = new LocalDate(event.getEffectiveDate(), event.getAccount().getTimeZone());
+                if (eventLocalEffectiveDate.isAfter(targetDate)) {
+                    continue;
+                }
+
+                // Optimize to do the usage query only once after we know there are indeed some usage items
+                if (rawUsageOptimizerResult == null &&
+                    Iterables.any(event.getUsages(), new Predicate<Usage>() {
+                        @Override
+                        public boolean apply(@Nullable final Usage input) {
+                            return (input.getUsageType() == UsageType.CONSUMABLE &&
+                                    input.getBillingMode() == BillingMode.IN_ARREAR);
+                        }
+                    })) {
+                    rawUsageOptimizerResult = rawUsageOptimizer.getConsumableInArrearUsage(new LocalDate(event.getEffectiveDate(), account.getTimeZone()), targetDate, Iterables.concat(perSubscriptionConsumableInArrearUsageItems.values()), eventSet.getUsages(), internalCallContext);
+                }
+
+                // None of the billing events report any usage (CONSUMABLE/IN_ARREAR) sections
+                if (rawUsageOptimizerResult == null) {
+                    continue;
+                }
+
+                final UUID subscriptionId = event.getSubscription().getId();
+                if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
+                    final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
+                    final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
+
+                    final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
+                    final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
+                    items.addAll(newInArrearUsageItems);
+                    updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
+                    curEvents = Lists.newArrayList();
+                }
+                curSubscriptionId = subscriptionId;
+                curEvents.add(event);
+            }
+            if (curSubscriptionId != null) {
+                final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
+                final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
+
+                final SubscriptionConsumableInArrearItemsAndNextNotificationDate subscriptionResult = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
+                final List<InvoiceItem> newInArrearUsageItems = subscriptionResult.getInvoiceItems();
+                items.addAll(newInArrearUsageItems);
+                updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, subscriptionResult.getPerUsageNotificationDates(), BillingMode.IN_ARREAR, perSubscriptionFutureNotificationDates);
+            }
+            return items;
+
+        } catch (CatalogApiException e) {
+            throw new InvoiceApiException(e);
+        }
+    }
+
+    private void updatePerSubscriptionNextNotificationUsageDate(final UUID subscriptionId, final Map<String, LocalDate> nextBillingCycleDates, final BillingMode usageBillingMode, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+        if (usageBillingMode == BillingMode.IN_ADVANCE) {
+            throw new IllegalStateException("Not implemented Yet)");
+        }
+
+        SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
+        if (subscriptionFutureNotificationDates == null) {
+            subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(null);
+            perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
+        }
+        for (final String usageName : nextBillingCycleDates.keySet()) {
+            subscriptionFutureNotificationDates.updateNextUsageDateIfRequired(usageName, usageBillingMode, nextBillingCycleDates.get(usageName));
+        }
+    }
+
+    private Map<UUID, List<InvoiceItem>> extractPerSubscriptionExistingConsumableInArrearUsageItems(final Map<String, Usage> knownUsage, @Nullable final List<Invoice> existingInvoices) {
+
+        if (existingInvoices == null || existingInvoices.isEmpty()) {
+            return ImmutableMap.of();
+        }
+
+        final Map<UUID, List<InvoiceItem>> result = new HashMap<UUID, List<InvoiceItem>>();
+        final Iterable<InvoiceItem> usageConsumableInArrearItems = Iterables.concat(Iterables.transform(existingInvoices, new Function<Invoice, Iterable<InvoiceItem>>() {
+            @Override
+            public Iterable<InvoiceItem> apply(final Invoice input) {
+
+                return Iterables.filter(input.getInvoiceItems(), new Predicate<InvoiceItem>() {
+                    @Override
+                    public boolean apply(final InvoiceItem input) {
+                        if (input.getInvoiceItemType() == InvoiceItemType.USAGE) {
+                            final Usage usage = knownUsage.get(input.getUsageName());
+                            return usage.getUsageType() == UsageType.CONSUMABLE && usage.getBillingMode() == BillingMode.IN_ARREAR;
+                        }
+                        return false;
+                    }
+                });
+            }
+        }));
+
+        for (InvoiceItem cur : usageConsumableInArrearItems) {
+            List<InvoiceItem> perSubscriptionUsageItems = result.get(cur.getSubscriptionId());
+            if (perSubscriptionUsageItems == null) {
+                perSubscriptionUsageItems = new LinkedList<InvoiceItem>();
+                result.put(cur.getSubscriptionId(), perSubscriptionUsageItems);
+            }
+            perSubscriptionUsageItems.add(cur);
+        }
+        return result;
+    }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
index f106da5..1973ff2 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
@@ -39,7 +39,9 @@ import org.killbill.billing.invoice.api.user.DefaultInvoiceUserApi;
 import org.killbill.billing.invoice.dao.DefaultInvoiceDao;
 import org.killbill.billing.invoice.dao.InvoiceDao;
 import org.killbill.billing.invoice.generator.DefaultInvoiceGenerator;
+import org.killbill.billing.invoice.generator.FixedAndRecurringInvoiceItemGenerator;
 import org.killbill.billing.invoice.generator.InvoiceGenerator;
+import org.killbill.billing.invoice.generator.UsageInvoiceItemGenerator;
 import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
 import org.killbill.billing.invoice.notification.DefaultNextBillingDatePoster;
 import org.killbill.billing.invoice.notification.EmailInvoiceNotifier;
@@ -133,6 +135,8 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
 
     protected void installInvoiceGenerator() {
         bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton();
+        bind(FixedAndRecurringInvoiceItemGenerator.class).asEagerSingleton();
+        bind(UsageInvoiceItemGenerator.class).asEagerSingleton();
     }
 
     protected void installInvoicePluginApi() {
@@ -157,7 +161,8 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
         installInvoicePaymentApi();
         installInvoiceMigrationApi();
         installResourceBundleFactory();
-        bind(RawUsageOptimizer.class).asEagerSingleton();;
+        bind(RawUsageOptimizer.class).asEagerSingleton();
+        ;
         bind(InvoiceApiHelper.class).asEagerSingleton();
     }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 9c5ec23..4d45ea4 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -133,7 +133,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
                 return 10;
             }
         };
-        this.generator = new DefaultInvoiceGenerator(clock, invoiceConfig, null);
         this.account = new MockAccountBuilder().name(UUID.randomUUID().toString().substring(1, 8))
                                                .firstNameLength(6)
                                                .email(UUID.randomUUID().toString().substring(1, 8))
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
index 1069bfc..2e2692e 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
@@ -27,6 +27,7 @@ import org.killbill.billing.invoice.api.InvoicePaymentApi;
 import org.killbill.billing.invoice.api.InvoiceUserApi;
 import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
 import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.generator.FixedAndRecurringInvoiceItemGenerator;
 import org.killbill.billing.invoice.generator.InvoiceGenerator;
 import org.killbill.billing.invoice.glue.TestInvoiceModuleNoDB;
 import org.killbill.billing.invoice.usage.RawUsageOptimizer;
@@ -97,7 +98,8 @@ public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     protected ResourceBundleFactory resourceBundleFactory;
     @Inject
     protected RawUsageOptimizer rawUsageOptimizer;
-
+    @Inject
+    protected FixedAndRecurringInvoiceItemGenerator fixedAndRecurringInvoiceItemGenerator;
     @Override
     protected KillbillConfigSource getConfigSource() {
         return getConfigSource("/resource.properties");
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java
index 471b9f0..ea1fd4b 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/inAdvance/TestValidationProRation.java
@@ -18,11 +18,9 @@ package org.killbill.billing.invoice.proRations.inAdvance;
 
 import org.joda.time.LocalDate;
 import org.killbill.billing.catalog.api.BillingMode;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
 import org.testng.annotations.Test;
 
 import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
 import org.killbill.billing.invoice.model.InvalidDateSequenceException;
 import org.killbill.billing.invoice.proRations.ProRationTestBase;
 
@@ -40,11 +38,6 @@ public class TestValidationProRation extends ProRationTestBase {
         return BillingMode.IN_ADVANCE;
     }
 
-    @Override
-    protected BillingModeGenerator getBillingModeGenerator() {
-        return new DefaultBillingModeGenerator();
-    }
-
     @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
     public void testTargetStartEnd() throws InvalidDateSequenceException {
         final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 30);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java
index 2a31e14..14c5275 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/ProRationTestBase.java
@@ -19,15 +19,12 @@ package org.killbill.billing.invoice.proRations;
 import static org.killbill.billing.invoice.TestInvoiceHelper.*;
 
 import java.math.BigDecimal;
-import java.util.List;
 
 import org.joda.time.LocalDate;
 
 import org.killbill.billing.catalog.api.BillingMode;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
-import org.killbill.billing.invoice.model.BillingModeGenerator;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
 import org.killbill.billing.invoice.model.InvalidDateSequenceException;
 import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
 import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillingCycleDate;
@@ -37,10 +34,6 @@ import static org.testng.Assert.fail;
 
 public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
 
-    protected BillingModeGenerator getBillingModeGenerator() {
-        return new DefaultBillingModeGenerator();
-    }
-
     protected abstract BillingPeriod getBillingPeriod();
 
     protected abstract BillingMode getBillingMode();
@@ -73,7 +66,7 @@ public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
     }
 
     protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
-        final RecurringInvoiceItemDataWithNextBillingCycleDate items = getBillingModeGenerator().generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
+        final RecurringInvoiceItemDataWithNextBillingCycleDate items = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
 
         BigDecimal numberOfBillingCycles = ZERO;
         for (final RecurringInvoiceItemData item : items.getItemData()) {
@@ -84,7 +77,7 @@ public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
     }
 
     protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
-        final RecurringInvoiceItemDataWithNextBillingCycleDate items = getBillingModeGenerator().generateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
+        final RecurringInvoiceItemDataWithNextBillingCycleDate items = fixedAndRecurringInvoiceItemGenerator.generateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod(), getBillingMode());
 
         BigDecimal numberOfBillingCycles = ZERO;
         for (final RecurringInvoiceItemData item : items.getItemData()) {