killbill-uncached

Details

diff --git a/api/src/main/java/com/ning/billing/entitlement/api/billing/IBillingEvent.java b/api/src/main/java/com/ning/billing/entitlement/api/billing/IBillingEvent.java
index 5db4815..db477be 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/billing/IBillingEvent.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/billing/IBillingEvent.java
@@ -84,4 +84,10 @@ public interface IBillingEvent extends Comparable<IBillingEvent> {
      * @return the billing mode for the current event
      */
     public BillingMode getBillingMode();
+
+    /**
+     *
+     * @return the description of the billing event
+     */
+    public String getDescription();
 }
diff --git a/api/src/main/java/com/ning/billing/invoice/api/BillingEvent.java b/api/src/main/java/com/ning/billing/invoice/api/BillingEvent.java
index 6457466..d17bc26 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/BillingEvent.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/BillingEvent.java
@@ -36,6 +36,7 @@ public class BillingEvent implements IBillingEvent {
     private final int billCycleDay;
     private final BillingMode billingMode;
 
+
     public BillingEvent(UUID subscriptionId, DateTime startDate, String planName, String planPhaseName, IInternationalPrice price,
                         BillingPeriod billingPeriod, int billCycleDay, BillingMode billingMode) {
         this.subscriptionId = subscriptionId;
@@ -48,6 +49,17 @@ public class BillingEvent implements IBillingEvent {
         this.billingMode = billingMode;
     }
 
+    public BillingEvent(IBillingEvent event, DateTime startDate) {
+        this.subscriptionId = event.getSubscriptionId();
+        this.startDate = startDate;
+        this.planName = event.getPlanName();
+        this.planPhaseName = event.getPlanPhaseName();
+        this.price = event.getPrice();
+        this.billingPeriod = event.getBillingPeriod();
+        this.billCycleDay = event.getBillCycleDay();
+        this.billingMode = event.getBillingMode();
+    }
+
     @Override
     public DateTime getEffectiveDate() {
         return startDate;
@@ -94,10 +106,13 @@ public class BillingEvent implements IBillingEvent {
     }
 
     @Override
-    public int compareTo(IBillingEvent billingEvent) {
-//        // strict date comparison here breaks SortedTree if multiple events occur on the same day
-//        return getEffectiveDate().compareTo(billingEvent.getEffectiveDate()) > 0 ? 1 : -1;
+    public String getDescription() {
+        return planName + "(" + planPhaseName + ")";
+    }
 
+    @Override
+    public int compareTo(IBillingEvent billingEvent) {
+        // strict date comparison here breaks SortedTree if multiple events occur on the same day
         int compareSubscriptions = getSubscriptionId().compareTo(billingEvent.getSubscriptionId());
 
         if (compareSubscriptions == 0) {
diff --git a/api/src/main/java/com/ning/billing/invoice/api/BillingEventSet.java b/api/src/main/java/com/ning/billing/invoice/api/BillingEventSet.java
index 37b536a..c85f540 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/BillingEventSet.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/BillingEventSet.java
@@ -16,34 +16,11 @@
 
 package com.ning.billing.invoice.api;
 
-import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.entitlement.api.billing.IBillingEvent;
-import org.joda.time.DateTime;
 
 import java.util.ArrayList;
 
 public class BillingEventSet extends ArrayList<IBillingEvent> {
-    private Currency targetCurrency;
-    private DateTime targetDate;
-
-    public BillingEventSet(Currency targetCurrency) {
-        this.targetCurrency = targetCurrency;
-        this.targetDate = new DateTime();
-    }
-
-    public BillingEventSet(Currency targetCurrency, DateTime targetDate) {
-        this.targetCurrency = targetCurrency;
-        this.targetDate = targetDate;
-    }
-
-    public Currency getTargetCurrency() {
-        return targetCurrency;
-    }
-
-    public DateTime getTargetDate() {
-        return targetDate;
-    }
-
     public IBillingEvent getLast() {
         if (this.size() == 0) {return null;}
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/BillingModeBase.java b/invoice/src/main/java/com/ning/billing/invoice/model/BillingModeBase.java
index 094ab2d..ba1dc88 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/BillingModeBase.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/BillingModeBase.java
@@ -53,12 +53,8 @@ public abstract class BillingModeBase implements IBillingMode {
         DateTime endBillCycleDate = calculateBillingCycleDateAfter(targetDate, firstBillCycleDate, billingCycleDay, billingPeriod);
         BigDecimal numberOfBillingPeriods = calculateNumberOfWholeBillingPeriods(firstBillCycleDate, endBillCycleDate, billingPeriod);
 
-        if (targetDate.equals(endBillCycleDate)) {
-            numberOfBillingPeriods = numberOfBillingPeriods.add(BigDecimal.ONE);
-        }
-
         return precedingProRation.add(numberOfBillingPeriods);
-}
+    }
 
     protected DateTime buildDate(final int year, final int month, final int day) {
         return new DateTime(year, month, day, 0, 0, 0, 0);
@@ -68,7 +64,9 @@ public abstract class BillingModeBase implements IBillingMode {
         return !(targetDate.isBefore(startDate) || !targetDate.isBefore(endDate));
     }
 
-    protected abstract DateTime calculateEffectiveEndDate(DateTime billCycleDate, DateTime targetDate, DateTime endDate, BillingPeriod billingPeriod);
+    public abstract DateTime calculateEffectiveEndDate(final DateTime startDate, final DateTime targetDate, final int billingCycleDay, final BillingPeriod billingPeriod);
+
+    public abstract DateTime calculateEffectiveEndDate(final DateTime startDate, final DateTime endDate, final DateTime targetDate, final int billingCycleDay, final BillingPeriod billingPeriod);
 
     protected abstract BigDecimal calculateNumberOfWholeBillingPeriods(final DateTime startDate, final DateTime endDate, final BillingPeriod billingPeriod);
 
@@ -81,4 +79,6 @@ public abstract class BillingModeBase implements IBillingMode {
     protected abstract BigDecimal calculateProRationBeforeFirstBillingPeriod(final DateTime startDate, final int billingCycleDay, final BillingPeriod billingPeriod);
 
     protected abstract BigDecimal calculateProRationAfterLastBillingCycleDate(final DateTime endDate, final DateTime previousBillThroughDate, final BillingPeriod billingPeriod);
+
+    protected abstract DateTime calculateEffectiveEndDate(final DateTime billCycleDate, final DateTime targetDate, final DateTime endDate, final BillingPeriod billingPeriod);
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DateRange.java b/invoice/src/main/java/com/ning/billing/invoice/model/DateRange.java
new file mode 100644
index 0000000..2a68a5b
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DateRange.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2011 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.model;
+
+import org.joda.time.DateTime;
+
+public class DateRange {
+    private final DateTime startDate;
+    private final DateTime endDate;
+
+    public DateRange(DateTime startDate, DateTime endDate) {
+        this.startDate = startDate;
+        this.endDate = endDate;
+    }
+
+    public DateTime getStartDate() {
+        return startDate;
+    }
+
+    public DateTime getEndDate() {
+        return endDate;
+    }
+
+    /**
+     *
+     * @param date the DateTime in question
+     * @return whether the DateRange contains (inclusively) the DateTime in question
+     */
+    public boolean contains(DateTime date) {
+        return (!date.isBefore(startDate)) && (!date.isAfter(endDate));
+    }
+
+    public boolean overlaps(DateRange range) {
+        return (this.contains(range.getStartDate()) || this.contains(range.getEndDate()));
+    }
+
+    public DateRange calculateUnionWith(DateRange range) {
+        if (this.contains(range.startDate) && this.contains(range.endDate)) {
+            return this;
+        }
+
+        if (this.contains(range.startDate)) {
+            return new DateRange(this.startDate, range.endDate);
+        }
+
+        if (this.contains(range.endDate)) {
+            return new DateRange(range.startDate, this.endDate);
+        }
+
+        return null;
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DateRangeHashMap.java b/invoice/src/main/java/com/ning/billing/invoice/model/DateRangeHashMap.java
new file mode 100644
index 0000000..b1acc73
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DateRangeHashMap.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2011 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.model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+
+public class DateRangeHashMap extends HashMap<UUID, List<DateRange>> {
+    public void collapseDateRanges() {
+        for (UUID key: this.keySet()) {
+            List<DateRange> dateRanges = this.get(key);
+
+            if (dateRanges.size() > 1) {
+                List<DateRange> newDateRanges = collapseDateRanges(dateRanges);
+
+                this.put(key, newDateRanges);
+            }
+        }
+    }
+
+    private List<DateRange> collapseDateRanges(List<DateRange> dateRanges) {
+        if (dateRanges.size() < 2) {return dateRanges;}
+
+        int originalSize = dateRanges.size();
+        List<DateRange> newDateRanges = new ArrayList<DateRange>();
+
+        DateRange range = dateRanges.get(0);
+
+        for (int i = 1; i < dateRanges.size(); i++) {
+            DateRange thisRange = dateRanges.get(i);
+            if (range.overlaps(thisRange)) {
+                range = range.calculateUnionWith(thisRange);
+                newDateRanges.add(range);
+            } else {
+                newDateRanges.add(range);
+                range = thisRange;
+            }
+        }
+
+        if (originalSize > newDateRanges.size()) {
+            newDateRanges = collapseDateRanges(newDateRanges);
+        }
+
+        return newDateRanges;
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java
index c99ff5a..94036d5 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java
@@ -24,17 +24,59 @@ import com.ning.billing.invoice.api.BillingEventSet;
 import org.joda.time.DateTime;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 
 public class DefaultInvoiceGenerator implements IInvoiceGenerator {
     @Override
-    public Invoice generateInvoice(BillingEventSet events) {
+    public Invoice generateInvoice(final BillingEventSet events, final InvoiceItemList existingItems, final DateTime targetDate, final Currency targetCurrency) {
         if (events == null) {return new Invoice();}
         if (events.size() == 0) {return new Invoice();}
 
-        Currency targetCurrency = events.getTargetCurrency();
+        InvoiceItemList currentItems = generateInvoiceItems(events, targetDate, targetCurrency);
+        InvoiceItemList itemsToPost = reconcileInvoiceItems(currentItems, existingItems);
+
         Invoice invoice = new Invoice(targetCurrency);
-        DateTime targetDate = events.getTargetDate();
+        invoice.add(itemsToPost);
+
+        return invoice;
+    }
+
+    private InvoiceItemList reconcileInvoiceItems(final InvoiceItemList currentInvoiceItems, final InvoiceItemList existingInvoiceItems) {
+        InvoiceItemList currentItems = (InvoiceItemList) currentInvoiceItems.clone();
+        InvoiceItemList existingItems = (InvoiceItemList) existingInvoiceItems.clone();
+
+        Collections.sort(currentItems);
+        Collections.sort(existingItems);
+
+        List<InvoiceItem> existingItemsToRemove = new ArrayList<InvoiceItem>();
+
+        for (InvoiceItem currentItem : currentItems) {
+            // see if there are any existing items that are covered by the current item
+            for (InvoiceItem existingItem : existingItems) {
+                if (currentItem.duplicates(existingItem)) {
+                    currentItem.subtract(existingItem);
+                    existingItemsToRemove.add(existingItem);
+                }
+            }
+        }
+
+        existingItems.removeAll(existingItemsToRemove);
+
+        // remove zero-dollar invoice items
+        currentItems.removeZeroDollarItems();
+
+        // add existing items that aren't covered by current items as credit items
+        for (InvoiceItem existingItem : existingItems) {
+            currentItems.add(existingItem.asCredit());
+        }
+
+        return currentItems;
+    }
+
+    private InvoiceItemList generateInvoiceItems(BillingEventSet events, DateTime targetDate, Currency targetCurrency) {
+        InvoiceItemList items = new InvoiceItemList();
 
         // sort events; this relies on the sort order being by subscription id then start date
         Collections.sort(events);
@@ -46,36 +88,42 @@ public class DefaultInvoiceGenerator implements IInvoiceGenerator {
             IBillingEvent nextEvent = events.get(i + 1);
 
             if (thisEvent.getSubscriptionId() == nextEvent.getSubscriptionId()) {
-                processEvents(thisEvent, nextEvent, invoice, targetDate, targetCurrency);
+                processEvents(thisEvent, nextEvent, items, targetDate, targetCurrency);
             } else {
-                processEvent(thisEvent, invoice, targetDate, targetCurrency);
+                processEvent(thisEvent, items, targetDate, targetCurrency);
             }
         }
 
         // process the last item in the event set
-        processEvent(events.getLast(), invoice, targetDate, targetCurrency);
+        if (events.size() > 0) {
+            processEvent(events.getLast(), items, targetDate, targetCurrency);
+        }
 
-        return invoice;
+        return items;
     }
 
-    private void processEvent(IBillingEvent event, Invoice invoice, DateTime targetDate, Currency targetCurrency) {
+    private void processEvent(IBillingEvent event, List<InvoiceItem> items, DateTime targetDate, Currency targetCurrency) {
         BigDecimal rate = event.getPrice(targetCurrency);
         BigDecimal invoiceItemAmount = calculateInvoiceItemAmount(event, targetDate, rate);
+        IBillingMode billingMode = getBillingMode(event.getBillingMode());
+        DateTime billThroughDate = billingMode.calculateEffectiveEndDate(event.getEffectiveDate(), targetDate, event.getBillCycleDay(), event.getBillingPeriod());
 
-        addInvoiceItem(invoice, invoiceItemAmount);
+        addInvoiceItem(items, event, billThroughDate, invoiceItemAmount, rate, targetCurrency);
     }
 
-    private void processEvents(IBillingEvent firstEvent, IBillingEvent secondEvent, Invoice invoice, DateTime targetDate, Currency targetCurrency) {
+    private void processEvents(IBillingEvent firstEvent, IBillingEvent secondEvent, List<InvoiceItem> items, DateTime targetDate, Currency targetCurrency) {
         BigDecimal rate = firstEvent.getPrice(targetCurrency);
         BigDecimal invoiceItemAmount = calculateInvoiceItemAmount(firstEvent, secondEvent, targetDate, rate);
+        IBillingMode billingMode = getBillingMode(firstEvent.getBillingMode());
+        DateTime billThroughDate = billingMode.calculateEffectiveEndDate(firstEvent.getEffectiveDate(), secondEvent.getEffectiveDate(), targetDate, firstEvent.getBillCycleDay(), firstEvent.getBillingPeriod());
 
-        addInvoiceItem(invoice, invoiceItemAmount);
+        addInvoiceItem(items, firstEvent, billThroughDate, invoiceItemAmount, rate, targetCurrency);
     }
 
-    private void addInvoiceItem(Invoice invoice, BigDecimal amount) {
-        if (!amount.equals(BigDecimal.ZERO)) {
-            InvoiceItem item = new InvoiceItem(amount);
-            invoice.add(item);
+    private void addInvoiceItem(List<InvoiceItem> items, IBillingEvent event, DateTime billThroughDate, BigDecimal amount, BigDecimal rate, Currency currency) {
+        if (!(amount.compareTo(BigDecimal.ZERO) == 0)) {
+            InvoiceItem item = new InvoiceItem(event.getSubscriptionId(), event.getEffectiveDate(), billThroughDate, event.getDescription(), amount, rate, currency);
+            items.add(item);
         }
     }
 
@@ -114,6 +162,11 @@ public class DefaultInvoiceGenerator implements IInvoiceGenerator {
     }
 
     private IBillingMode getBillingMode(BillingMode billingMode) {
-        return new InAdvanceBillingMode();
+        switch (billingMode) {
+            case IN_ADVANCE:
+                return new InAdvanceBillingMode();
+            default:
+                return null;
+        }
     }
 }
\ No newline at end of file
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/IBillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/IBillingMode.java
index e716e47..899d7fe 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/IBillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/IBillingMode.java
@@ -25,4 +25,8 @@ public interface IBillingMode {
     BigDecimal calculateNumberOfBillingCycles(DateTime startDate, DateTime endDate, DateTime targetDate, int billingCycleDay, BillingPeriod billingPeriod) throws InvalidDateSequenceException;
 
     BigDecimal calculateNumberOfBillingCycles(DateTime startDate, DateTime targetDate, int billingCycleDay, BillingPeriod billingPeriod) throws InvalidDateSequenceException;
+
+    DateTime calculateEffectiveEndDate(DateTime startDate, DateTime targetDate, int billingCycleDay, BillingPeriod billingPeriod);
+
+    DateTime calculateEffectiveEndDate(DateTime startDate, DateTime endDate, DateTime targetDate, int billingCycleDay, BillingPeriod billingPeriod);
 }
\ No newline at end of file
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/IInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/model/IInvoiceGenerator.java
index 6d03a9b..3070307 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/IInvoiceGenerator.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/IInvoiceGenerator.java
@@ -16,9 +16,11 @@
 
 package com.ning.billing.invoice.model;
 
+import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.invoice.api.BillingEventSet;
+import org.joda.time.DateTime;
 
 // TODO: Jeff -- Determine what the consequence of account-level currency changes are on repair scenarios
 public interface IInvoiceGenerator {
-    public Invoice generateInvoice(BillingEventSet events);
+    public Invoice generateInvoice(BillingEventSet events, InvoiceItemList items, DateTime targetDate, Currency targetCurrency);
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
index caf7e3d..ff8f9ac 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
@@ -28,6 +28,18 @@ public class InAdvanceBillingMode extends BillingModeBase {
     private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();
 
     @Override
+    public DateTime calculateEffectiveEndDate(final DateTime startDate, final DateTime targetDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
+        DateTime firstBillCycleDate = calculateBillingCycleDateOnOrAfter(startDate, billingCycleDay);
+        return calculateBillingCycleDateAfter(targetDate, firstBillCycleDate, billingCycleDay, billingPeriod);
+    }
+
+    @Override
+    public DateTime calculateEffectiveEndDate(final DateTime startDate, final DateTime endDate, final DateTime targetDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
+        DateTime firstBillCycleDate = calculateBillingCycleDateOnOrAfter(startDate, billingCycleDay);
+        return calculateEffectiveEndDate(firstBillCycleDate, targetDate, endDate, billingPeriod);
+    }
+
+    @Override
     protected BigDecimal calculateNumberOfWholeBillingPeriods(final DateTime startDate, final DateTime endDate, final BillingPeriod billingPeriod) {
         int numberOfMonths = Months.monthsBetween(startDate, endDate).getMonths();
         BigDecimal numberOfMonthsInPeriod = new BigDecimal(billingPeriod.getNumberOfMonths());
@@ -55,7 +67,7 @@ public class InAdvanceBillingMode extends BillingModeBase {
     protected DateTime calculateBillingCycleDateAfter(final DateTime date, final DateTime billingCycleDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
         DateTime proposedDate = billingCycleDate;
 
-        while (proposedDate.isBefore(date)) {
+        while (!proposedDate.isAfter(date)) {
             proposedDate = proposedDate.plusMonths(billingPeriod.getNumberOfMonths());
 
             if (proposedDate.dayOfMonth().get() != billingCycleDay) {
@@ -119,6 +131,7 @@ public class InAdvanceBillingMode extends BillingModeBase {
         return days.divide(daysInPeriod, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
     }
 
+    @Override
     protected DateTime calculateEffectiveEndDate(DateTime billCycleDate, DateTime targetDate, DateTime endDate, BillingPeriod billingPeriod) {
         if (targetDate.isBefore(endDate)) {
             if (targetDate.isBefore(billCycleDate)) {
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/Invoice.java b/invoice/src/main/java/com/ning/billing/invoice/model/Invoice.java
index 5ba92cf..53965f3 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/Invoice.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/Invoice.java
@@ -19,13 +19,10 @@ package com.ning.billing.invoice.model;
 import com.ning.billing.catalog.api.Currency;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
 import java.util.List;
 
 public class Invoice {
-    private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();
-
-    private final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+    private final InvoiceItemList items = new InvoiceItemList();
     private Currency currency;
 
     public Invoice() {}
@@ -34,23 +31,29 @@ public class Invoice {
         this.currency = currency;
     }
 
+    public Invoice(List<InvoiceItem> items, Currency currency) {
+        this.currency = currency;
+        this.items.addAll(items);
+    }
+
     public boolean add(InvoiceItem item) {
         return items.add(item);
     }
 
+    public boolean add(List<InvoiceItem> items) {
+        return this.items.addAll(items);
+    }
+
+    public List<InvoiceItem> getItems() {
+        return items;
+    }
+
     public Currency getCurrency() {
         return currency;
     }
 
     public BigDecimal getTotalAmount() {
-        // TODO: Jeff -- naive implementation, assumes all invoice items share the same currency
-        BigDecimal total = new BigDecimal("0");
-
-        for (InvoiceItem item : items) {
-            total = total.add(item.getAmount());
-        }
-
-        return total.setScale(NUMBER_OF_DECIMALS);
+        return items.getTotalAmount();
     }
 
     public int getNumberOfItems() {
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItem.java b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItem.java
index 45880bb..c49edf1 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItem.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItem.java
@@ -16,26 +16,94 @@
 
 package com.ning.billing.invoice.model;
 
+import com.ning.billing.catalog.api.Currency;
+import org.joda.time.DateTime;
+
 import java.math.BigDecimal;
+import java.util.UUID;
+
+public class InvoiceItem implements Comparable<InvoiceItem> {
+    private final UUID subscriptionId;
+    private DateTime startDate;
+    private DateTime endDate;
+    private final String description;
+    private BigDecimal amount;
+    private final BigDecimal rate;
+    private final Currency currency;
 
-public class InvoiceItem {
-//    private final String description;
-//    private final DateTime startDate;
-//    private final DateTime endDate;
-    private final BigDecimal amount;
-//    private final Currency currency;
-
-    // TODO: Jeff -- determine if a default constructor is required for InvoiceItem
-    //public InvoiceItem(DateTime startDate, DateTime endDate, String description, BigDecimal amount, Currency currency) {
-    public InvoiceItem(BigDecimal amount) {
-        //this.description = description;
+    public InvoiceItem(UUID subscriptionId, DateTime startDate, DateTime endDate, String description, BigDecimal amount, BigDecimal rate, Currency currency) {
+        this.subscriptionId = subscriptionId;
+        this.startDate = startDate;
+        this.endDate = endDate;
+        this.description = description;
         this.amount = amount;
-//        this.currency = currency;
-//        this.startDate = startDate;
-//        this.endDate = endDate;
+        this.rate = rate;
+        this.currency = currency;
+    }
+
+    public InvoiceItem asCredit() {
+        return new InvoiceItem(subscriptionId, startDate, endDate, description, amount.negate(), rate, currency);
+    }
+
+    public UUID getSubscriptionId() {
+        return subscriptionId;
+    }
+
+    public DateTime getStartDate() {
+        return startDate;
+    }
+
+    public DateTime getEndDate() {
+        return endDate;
     }
 
     public BigDecimal getAmount() {
         return amount;
     }
+
+    public BigDecimal getRate() {
+        return rate;
+    }
+
+    public Currency getCurrency() {
+        return currency;
+    }
+
+    @Override
+    public int compareTo(InvoiceItem invoiceItem) {
+        int compareSubscriptions = getSubscriptionId().compareTo(invoiceItem.getSubscriptionId());
+
+        if (compareSubscriptions == 0) {
+            return getStartDate().compareTo(invoiceItem.getStartDate());
+        } else {
+            return compareSubscriptions;
+        }
+    }
+
+    // TODO: deal with error cases
+    public void subtract(InvoiceItem that) {
+        if (this.startDate.equals(that.startDate) && this.endDate.equals(that.endDate)) {
+            this.startDate = this.endDate;
+            this.amount = this.amount.subtract(that.amount);
+        } else {
+            if (this.startDate.equals(that.startDate)) {
+                this.startDate = that.endDate;
+                this.amount = this.amount.subtract(that.amount);
+            }
+
+            if (this.endDate.equals(that.endDate)) {
+                this.endDate = that.startDate;
+                this.amount = this.amount.subtract(that.amount);
+            }
+        }
+    }
+
+    public boolean duplicates(InvoiceItem that) {
+        if(!this.getSubscriptionId().equals(that.getSubscriptionId())) {return false;}
+        if(!this.getRate().equals(that.getRate())) {return false;}
+        if(!this.getCurrency().equals(that.getCurrency())) {return false;}
+
+        DateRange thisDateRange = new DateRange(this.getStartDate(), this.getEndDate());
+        return thisDateRange.contains(that.getStartDate()) && thisDateRange.contains(that.getEndDate());
+    }
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
new file mode 100644
index 0000000..617405d
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2011 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.model;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class InvoiceItemList extends ArrayList<InvoiceItem> {
+    private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();
+
+    public BigDecimal getTotalAmount() {
+        // TODO: Jeff -- naive implementation, assumes all invoice items share the same currency
+        BigDecimal total = new BigDecimal("0");
+
+        for (InvoiceItem item : this) {
+            total = total.add(item.getAmount());
+        }
+
+        return total.setScale(NUMBER_OF_DECIMALS);
+    }
+
+    public List<InvoiceItem> getBySubscriptionId(UUID subscriptionId) {
+        List<InvoiceItem> thisList = new ArrayList<InvoiceItem>();
+
+        for (InvoiceItem item : this) {
+            if (item.getSubscriptionId() == subscriptionId) {
+                thisList.add(item);
+            }
+        }
+
+        return thisList;
+    }
+
+    public void removeZeroDollarItems() {
+        List<InvoiceItem> itemsToRemove = new ArrayList<InvoiceItem>();
+
+        for (InvoiceItem item : this) {
+            if (item.getAmount().compareTo(BigDecimal.ZERO) == 0) {
+                itemsToRemove.add(item);
+            }
+        }
+
+        this.removeAll(itemsToRemove);
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/DefaultInvoiceGeneratorTests.java b/invoice/src/test/java/com/ning/billing/invoice/tests/DefaultInvoiceGeneratorTests.java
index cfa0ce1..8e6d671 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/DefaultInvoiceGeneratorTests.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/DefaultInvoiceGeneratorTests.java
@@ -22,12 +22,11 @@ import com.ning.billing.entitlement.api.billing.BillingMode;
 import com.ning.billing.entitlement.api.billing.IBillingEvent;
 import com.ning.billing.invoice.api.BillingEvent;
 import com.ning.billing.invoice.api.BillingEventSet;
-import com.ning.billing.invoice.model.DefaultInvoiceGenerator;
-import com.ning.billing.invoice.model.IInvoiceGenerator;
-import com.ning.billing.invoice.model.Invoice;
+import com.ning.billing.invoice.model.*;
 import org.joda.time.DateTime;
 import org.testng.annotations.Test;
 
+import java.math.BigDecimal;
 import java.util.UUID;
 
 import static org.testng.Assert.assertEquals;
@@ -38,8 +37,8 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
     private final IInvoiceGenerator generator = new DefaultInvoiceGenerator();
 
     @Test
-    public void testWithNullEventSet() {
-        Invoice invoice = generator.generateInvoice(null);
+    public void testWithNullEventSetAndNullInvoiceSet() {
+        Invoice invoice = generator.generateInvoice(null, null, new DateTime(), Currency.USD);
 
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 0);
@@ -48,9 +47,10 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
 
     @Test
     public void testWithEmptyEventSet() {
-        BillingEventSet events = new BillingEventSet(Currency.USD);
+        BillingEventSet events = new BillingEventSet();
 
-        Invoice invoice = generator.generateInvoice(events);
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, new DateTime(), Currency.USD);
 
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 0);
@@ -58,9 +58,8 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
     }
 
     @Test
-    public void testWithSingleSimpleEvent() {
-        DateTime targetDate = buildDateTime(2011, 10, 3);
-        BillingEventSet events = new BillingEventSet(Currency.USD, targetDate);
+    public void testWithSingleMonthlyEvent() {
+        BillingEventSet events = new BillingEventSet();
 
         UUID subscriptionId = UUID.randomUUID();
         DateTime startDate = buildDateTime(2011, 9, 1);
@@ -72,7 +71,10 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
 
         events.add(event);
 
-        Invoice invoice = generator.generateInvoice(events);
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+        
+        DateTime targetDate = buildDateTime(2011, 10, 3);
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
 
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 1);
@@ -80,9 +82,37 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
     }
 
     @Test
-    public void testTwoSubscriptionsWithAlignedBillingDates() {
-        DateTime targetDate = buildDateTime(2011, 10, 3);
-        BillingEventSet events = new BillingEventSet(Currency.USD, targetDate);
+    public void testWithSingleMonthlyEventWithLeadingProRation() {
+        BillingEventSet events = new BillingEventSet();
+
+        UUID subscriptionId = UUID.randomUUID();
+        DateTime startDate = buildDateTime(2011, 9, 1);
+        String planName = "World Domination";
+        String phaseName = "Build Space Laser";
+        BigDecimal rate = TEN;
+        IBillingEvent event = new BillingEvent(subscriptionId, startDate, planName, phaseName,
+                                               new InternationalPriceMock(rate), BillingPeriod.MONTHLY,
+                                               15, BillingMode.IN_ADVANCE);
+
+        events.add(event);
+
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+        
+        DateTime targetDate = buildDateTime(2011, 10, 3);        
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+
+        assertNotNull(invoice);
+        assertEquals(invoice.getNumberOfItems(), 1);
+
+        BigDecimal expectedNumberOfBillingCycles;
+        expectedNumberOfBillingCycles = ONE.add(FOURTEEN.divide(THIRTY_ONE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
+        BigDecimal expectedAmount = expectedNumberOfBillingCycles.multiply(rate).setScale(NUMBER_OF_DECIMALS);
+        assertEquals(invoice.getTotalAmount(), expectedAmount);
+    }
+
+    @Test
+    public void testTwoMonthlySubscriptionsWithAlignedBillingDates() {
+        BillingEventSet events = new BillingEventSet();
 
         IBillingEvent event1 = new BillingEvent(UUID.randomUUID(), buildDateTime(2011, 9, 1),
                                                "World Domination", "Build Space Laser",
@@ -96,7 +126,9 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
                                                1, BillingMode.IN_ADVANCE);
         events.add(event2);
 
-        Invoice invoice = generator.generateInvoice(events);
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+        DateTime targetDate = buildDateTime(2011, 10, 3);
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
 
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 2);
@@ -104,9 +136,45 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
     }
 
     @Test
-    public void testOnePlan_ThreePhases_ChangeEOT() {
+    public void testOnePlan_TwoMonthlyPhases_ChangeImmediate() {
+        BillingEventSet events = new BillingEventSet();
+
+        UUID subscriptionId = UUID.randomUUID();
+        IBillingEvent event1 = new BillingEvent(subscriptionId, buildDateTime(2011, 9, 1),
+                                               "World Domination", "Build Space Laser",
+                                               new InternationalPriceMock(FIVE), BillingPeriod.MONTHLY,
+                                               1, BillingMode.IN_ADVANCE);
+        events.add(event1);
+
+        IBillingEvent event2 = new BillingEvent(subscriptionId, buildDateTime(2011, 10, 15),
+                                               "World Domination", "Incinerate James Bond",
+                                               new InternationalPriceMock(TEN), BillingPeriod.MONTHLY,
+                                               15, BillingMode.IN_ADVANCE);
+        events.add(event2);
+
+        InvoiceItemList invoiceItems = new InvoiceItemList();
         DateTime targetDate = buildDateTime(2011, 12, 3);
-        BillingEventSet events = new BillingEventSet(Currency.USD, targetDate);
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+
+        assertNotNull(invoice);
+        assertEquals(invoice.getNumberOfItems(), 2);
+
+        BigDecimal numberOfCyclesEvent1;
+        numberOfCyclesEvent1 = ONE.add(FOURTEEN.divide(THIRTY_ONE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
+
+        BigDecimal numberOfCyclesEvent2 = TWO;
+
+        BigDecimal expectedValue;
+        expectedValue = numberOfCyclesEvent1.multiply(FIVE);
+        expectedValue = expectedValue.add(numberOfCyclesEvent2.multiply(TEN));
+        expectedValue = expectedValue.setScale(NUMBER_OF_DECIMALS);
+
+        assertEquals(invoice.getTotalAmount(), expectedValue);
+    }
+
+    @Test
+    public void testOnePlan_ThreeMonthlyPhases_ChangeEOT() {
+        BillingEventSet events = new BillingEventSet();
 
         UUID subscriptionId = UUID.randomUUID();
         IBillingEvent event1 = new BillingEvent(subscriptionId, buildDateTime(2011, 9, 1),
@@ -127,10 +195,136 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
                                                1, BillingMode.IN_ADVANCE);
         events.add(event3);
 
-        Invoice invoice = generator.generateInvoice(events);
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+        DateTime targetDate = buildDateTime(2011, 12, 3);
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
 
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 3);
         assertEquals(invoice.getTotalAmount(), FIVE.add(TEN).add(TWO.multiply(THIRTY)).setScale(NUMBER_OF_DECIMALS));
     }
-}
+
+    @Test
+    public void testSingleEventWithExistingInvoice() {
+        BillingEventSet events = new BillingEventSet();
+
+        UUID subscriptionId = UUID.randomUUID();
+        DateTime startDate = buildDateTime(2011, 9, 1);
+
+        BigDecimal rate = FIVE;
+        IBillingEvent event1 = new BillingEvent(subscriptionId, startDate,
+                                               "World Domination", "Build Space Laser",
+                                               new InternationalPriceMock(rate), BillingPeriod.MONTHLY,
+                                               1, BillingMode.IN_ADVANCE);
+        events.add(event1);
+
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItem invoiceItem = new InvoiceItem(subscriptionId, startDate, buildDateTime(2012, 1, 1), "",
+                                                 rate.multiply(FOUR), rate, Currency.USD);
+        invoiceItems.add(invoiceItem);
+
+        DateTime targetDate = buildDateTime(2011, 12, 3);
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+
+        assertNotNull(invoice);
+        assertEquals(invoice.getNumberOfItems(), 0);
+        assertEquals(invoice.getTotalAmount(), ZERO);
+    }
+
+    @Test
+    public void testMultiplePlansWithUtterChaos() {
+        // plan 1: change of term from monthly to annual followed by immediate cancellation; (covers term change, cancel, double pro-ration)
+        // plan 2: single plan that moves from trial to discount to evergreen; BCD = 10 (covers phase change)
+        // plan 3: change of term from annual (BCD = 20) to monthly (BCD = 31; immediate)
+        // plan 4: change of plan, effective EOT, BCD = 10, start = 2/10/2011 (covers change of plan, multiple BCD)
+
+        BillingMode billingMode = BillingMode.IN_ADVANCE;
+        BillingEventSet events = new BillingEventSet();
+
+        UUID subscriptionId1 = UUID.randomUUID();
+        DateTime startDate1 = buildDateTime(2011, 1, 5);
+        String planName1 = "World Domination";
+
+        IBillingEvent event;
+        BigDecimal expectedAmount;
+        InvoiceItemList invoiceItems = new InvoiceItemList();
+
+        event = new BillingEvent(subscriptionId1, startDate1,
+                                 planName1, "Conceive diabolical plan",
+                                 new InternationalPriceMock(EIGHT), BillingPeriod.MONTHLY, 5, billingMode);
+        events.add(event);
+
+        // invoice for 2011/1/5 through 2011/2/5 on plan 1
+        expectedAmount = EIGHT;
+
+        // initial invoice on subscription creation
+        testInvoiceGeneration(events, invoiceItems, startDate1, 1, expectedAmount);
+        assertEquals(invoiceItems.size(), 1);
+
+        // attempt to invoice again the following day; should have no invoice items
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 1, 6), 0, ZERO);
+        assertEquals(invoiceItems.size(), 1);
+
+        // add a second plan to the account
+        UUID subscriptionId2 = UUID.randomUUID();
+        DateTime startDate2 = buildDateTime(2011, 3, 10);
+        String planName2 = "Build Invoicing System";
+
+        event = new BillingEvent(subscriptionId2, startDate2,
+                                 planName2, "Implement and test pro-ration algorithm",
+                                 new InternationalPriceMock(TWENTY), BillingPeriod.MONTHLY, 10, billingMode);
+        events.add(event);
+
+        // invoice for 2011/2/5 - 2011/4/5 on plan 1; invoice for 2011/3/10 - 2011/4/10 on plan 2
+        expectedAmount = EIGHT.multiply(TWO).add(TWENTY).setScale(NUMBER_OF_DECIMALS);
+        testInvoiceGeneration(events, invoiceItems, startDate2, 2, expectedAmount);
+        assertEquals(invoiceItems.size(), 3);
+
+        // first plan rolls into discount period on 4/5
+        DateTime phaseChangeDate = buildDateTime(2011, 4, 5);
+        event = new BillingEvent(subscriptionId1, phaseChangeDate,
+                                 planName1, "Hire minions",
+                                 new InternationalPriceMock(TWELVE), BillingPeriod.MONTHLY, 5, billingMode);
+        events.add(event);
+
+        // on plan creation, invoice for 2011/4/5 through 2011/5/5 on plan 1
+        testInvoiceGeneration(events, invoiceItems, phaseChangeDate, 1, TWELVE);
+        assertEquals(invoiceItems.size(), 4);
+
+        // on 2011/4/11, invoice for 2011/4/10 - 2011/5/10 on plan 2
+        DateTime billRunDate = buildDateTime(2011, 4, 11);
+        testInvoiceGeneration(events, invoiceItems, billRunDate, 1, TWENTY);
+        assertEquals(invoiceItems.size(), 5);
+
+        // on 2011/4/29, cancel plan 1, effective immediately
+        DateTime plan1CancelDate = buildDateTime(2011, 4, 29);
+        event = new BillingEvent(subscriptionId1, plan1CancelDate,
+                                 planName1, "Defeated by James Bond",
+                                 new InternationalPriceMock(ZERO), BillingPeriod.MONTHLY, 5, billingMode);
+        events.add(event);
+
+        // generate correcting invoice item for cancellation
+        Invoice invoice = generator.generateInvoice(events, new InvoiceItemList(), plan1CancelDate, Currency.USD);
+        BigDecimal totalToDate = invoice.getTotalAmount();
+
+        BigDecimal invoicedAmount = ZERO;
+        for (InvoiceItem item : invoiceItems) {
+            invoicedAmount = invoicedAmount.add(item.getAmount());
+        }
+
+        BigDecimal creditAmount = totalToDate.subtract(invoicedAmount).setScale(NUMBER_OF_DECIMALS);
+
+        testInvoiceGeneration(events, invoiceItems, plan1CancelDate, 2, creditAmount);
+    }
+
+    private void testInvoiceGeneration(BillingEventSet events, InvoiceItemList invoiceItems, DateTime targetDate, int expectedNumberOfItems, BigDecimal expectedAmount) {
+        Currency currency = Currency.USD;
+        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, currency);
+        invoiceItems.addAll(invoice.getItems());
+        assertNotNull(invoice);
+        assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
+        assertEquals(invoice.getTotalAmount(), expectedAmount);
+    }
+
+    // TODO: Jeff C -- how do we ensure that an annual add-on is properly aligned *at the end* with the base plan?
+}
\ No newline at end of file