killbill-uncached

1. invoicing scenario tests 2. cancellation algorithm (also

11/1/2011 7:39:07 PM

Details

diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/IInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/IInvoiceDao.java
new file mode 100644
index 0000000..5c686b9
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/IInvoiceDao.java
@@ -0,0 +1,20 @@
+/*
+ * 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.dao;
+
+public interface IInvoiceDao {
+}
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 ba1dc88..d141f27 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
@@ -56,12 +56,12 @@ public abstract class BillingModeBase implements IBillingMode {
         return precedingProRation.add(numberOfBillingPeriods);
     }
 
-    protected DateTime buildDate(final int year, final int month, final int day) {
+    DateTime buildDate(final int year, final int month, final int day) {
         return new DateTime(year, month, day, 0, 0, 0, 0);
     }
 
-    protected boolean isBetween(DateTime targetDate, DateTime startDate, DateTime endDate) {
-        return !(targetDate.isBefore(startDate) || !targetDate.isBefore(endDate));
+    boolean isNotBetween(DateTime targetDate, DateTime startDate, DateTime endDate) {
+        return (targetDate.isBefore(startDate) || !targetDate.isBefore(endDate));
     }
 
     public abstract DateTime calculateEffectiveEndDate(final DateTime startDate, final DateTime targetDate, final int billingCycleDay, 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
index 2a68a5b..9f21ca6 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/DateRange.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DateRange.java
@@ -27,14 +27,6 @@ public class DateRange {
         this.endDate = endDate;
     }
 
-    public DateTime getStartDate() {
-        return startDate;
-    }
-
-    public DateTime getEndDate() {
-        return endDate;
-    }
-
     /**
      *
      * @param date the DateTime in question
@@ -45,7 +37,7 @@ public class DateRange {
     }
 
     public boolean overlaps(DateRange range) {
-        return (this.contains(range.getStartDate()) || this.contains(range.getEndDate()));
+        return (this.contains(range.startDate) || this.contains(range.endDate));
     }
 
     public DateRange calculateUnionWith(DateRange range) {
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 94036d5..29d4370 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
@@ -30,17 +30,14 @@ import java.util.List;
 
 public class DefaultInvoiceGenerator implements IInvoiceGenerator {
     @Override
-    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();}
+    public InvoiceItemList generateInvoiceItems(final BillingEventSet events, final InvoiceItemList existingItems, final DateTime targetDate, final Currency targetCurrency) {
+        if (events == null) {return new InvoiceItemList();}
+        if (events.size() == 0) {return new InvoiceItemList();}
 
         InvoiceItemList currentItems = generateInvoiceItems(events, targetDate, targetCurrency);
         InvoiceItemList itemsToPost = reconcileInvoiceItems(currentItems, existingItems);
 
-        Invoice invoice = new Invoice(targetCurrency);
-        invoice.add(itemsToPost);
-
-        return invoice;
+        return itemsToPost;
     }
 
     private InvoiceItemList reconcileInvoiceItems(final InvoiceItemList currentInvoiceItems, final InvoiceItemList existingInvoiceItems) {
@@ -64,6 +61,9 @@ public class DefaultInvoiceGenerator implements IInvoiceGenerator {
 
         existingItems.removeAll(existingItemsToRemove);
 
+        // remove cancelling pairs of invoice items
+        existingItems.removeCancellingPairs();
+
         // remove zero-dollar invoice items
         currentItems.removeZeroDollarItems();
 
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 3070307..468686b 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
@@ -22,5 +22,5 @@ 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, InvoiceItemList items, DateTime targetDate, Currency targetCurrency);
+    public InvoiceItemList generateInvoiceItems(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 ff8f9ac..37b5820 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
@@ -142,7 +142,7 @@ public class InAdvanceBillingMode extends BillingModeBase {
             DateTime startOfPeriod = billCycleDate;
             DateTime startOfNextPeriod = billCycleDate.plusMonths(numberOfMonthsInPeriod);
 
-            while (!isBetween(targetDate, startOfPeriod, startOfNextPeriod)) {
+            while (isNotBetween(targetDate, startOfPeriod, startOfNextPeriod)) {
                 startOfPeriod = startOfNextPeriod;
                 startOfNextPeriod = startOfPeriod.plusMonths(numberOfMonthsInPeriod);
             }
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 53965f3..02ce760 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
@@ -55,9 +55,5 @@ public class Invoice {
     public BigDecimal getTotalAmount() {
         return items.getTotalAmount();
     }
-
-    public int getNumberOfItems() {
-        return items.size();
-    }
 }
 
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 c49edf1..ae3ab53 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
@@ -106,4 +106,20 @@ public class InvoiceItem implements Comparable<InvoiceItem> {
         DateRange thisDateRange = new DateRange(this.getStartDate(), this.getEndDate());
         return thisDateRange.contains(that.getStartDate()) && thisDateRange.contains(that.getEndDate());
     }
+
+    /**
+     * indicates whether the supplied item is a cancelling item for this item
+     * @param that
+     * @return
+     */
+    public boolean cancels(InvoiceItem that) {
+        if(!this.getSubscriptionId().equals(that.getSubscriptionId())) {return false;}
+        if(!this.getEndDate().equals(that.getEndDate())) {return false;}
+        if(!this.getStartDate().equals(that.getStartDate())) {return false;}
+        if(!this.getAmount().equals(that.getAmount().negate())) {return false;}
+        if(!this.getRate().equals(that.getRate())) {return false;}
+        if(!this.getCurrency().equals(that.getCurrency())) {return false;}
+
+        return true;
+    }
 }
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
index 617405d..679c735 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemList.java
@@ -19,7 +19,6 @@ 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();
@@ -35,24 +34,29 @@ public class InvoiceItemList extends ArrayList<InvoiceItem> {
         return total.setScale(NUMBER_OF_DECIMALS);
     }
 
-    public List<InvoiceItem> getBySubscriptionId(UUID subscriptionId) {
-        List<InvoiceItem> thisList = new ArrayList<InvoiceItem>();
+    public void removeZeroDollarItems() {
+        List<InvoiceItem> itemsToRemove = new ArrayList<InvoiceItem>();
 
         for (InvoiceItem item : this) {
-            if (item.getSubscriptionId() == subscriptionId) {
-                thisList.add(item);
+            if (item.getAmount().compareTo(BigDecimal.ZERO) == 0) {
+                itemsToRemove.add(item);
             }
         }
 
-        return thisList;
+        this.removeAll(itemsToRemove);
     }
 
-    public void removeZeroDollarItems() {
+    public void removeCancellingPairs() {
         List<InvoiceItem> itemsToRemove = new ArrayList<InvoiceItem>();
 
-        for (InvoiceItem item : this) {
-            if (item.getAmount().compareTo(BigDecimal.ZERO) == 0) {
-                itemsToRemove.add(item);
+        for (int firstItemIndex = 0; firstItemIndex < this.size(); firstItemIndex++) {
+            for (int secondItemIndex = firstItemIndex + 1; secondItemIndex < this.size(); secondItemIndex++) {
+                InvoiceItem firstItem = this.get(firstItemIndex);
+                InvoiceItem secondItem = this.get(secondItemIndex);
+                if (firstItem.cancels(secondItem)) {
+                    itemsToRemove.add(firstItem);
+                    itemsToRemove.add(secondItem);
+                }
             }
         }
 
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 8e6d671..4e7b756 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,7 +22,10 @@ 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.*;
+import com.ning.billing.invoice.model.DefaultInvoiceGenerator;
+import com.ning.billing.invoice.model.IInvoiceGenerator;
+import com.ning.billing.invoice.model.InvoiceItem;
+import com.ning.billing.invoice.model.InvoiceItemList;
 import org.joda.time.DateTime;
 import org.testng.annotations.Test;
 
@@ -38,23 +41,23 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
 
     @Test
     public void testWithNullEventSetAndNullInvoiceSet() {
-        Invoice invoice = generator.generateInvoice(null, null, new DateTime(), Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(null, null, new DateTime(), Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 0);
-        assertEquals(invoice.getTotalAmount(), ZERO);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 0);
+        assertEquals(invoiceItems.getTotalAmount(), ZERO);
     }
 
     @Test
     public void testWithEmptyEventSet() {
         BillingEventSet events = new BillingEventSet();
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, new DateTime(), Currency.USD);
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, new DateTime(), Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 0);
-        assertEquals(invoice.getTotalAmount(), ZERO);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 0);
+        assertEquals(invoiceItems.getTotalAmount(), ZERO);
     }
 
     @Test
@@ -71,14 +74,14 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
 
         events.add(event);
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
         
         DateTime targetDate = buildDateTime(2011, 10, 3);
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 1);
-        assertEquals(invoice.getTotalAmount(), TWENTY);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 1);
+        assertEquals(invoiceItems.getTotalAmount(), TWENTY);
     }
 
     @Test
@@ -96,18 +99,18 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
 
         events.add(event);
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
         
         DateTime targetDate = buildDateTime(2011, 10, 3);        
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 1);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 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);
+        assertEquals(invoiceItems.getTotalAmount(), expectedAmount);
     }
 
     @Test
@@ -126,13 +129,13 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
                                                1, BillingMode.IN_ADVANCE);
         events.add(event2);
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
         DateTime targetDate = buildDateTime(2011, 10, 3);
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 2);
-        assertEquals(invoice.getTotalAmount(), FIVE.multiply(TWO).add(TEN).setScale(NUMBER_OF_DECIMALS));
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 2);
+        assertEquals(invoiceItems.getTotalAmount(), FIVE.multiply(TWO).add(TEN).setScale(NUMBER_OF_DECIMALS));
     }
 
     @Test
@@ -152,12 +155,12 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
                                                15, BillingMode.IN_ADVANCE);
         events.add(event2);
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
         DateTime targetDate = buildDateTime(2011, 12, 3);
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 2);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 2);
 
         BigDecimal numberOfCyclesEvent1;
         numberOfCyclesEvent1 = ONE.add(FOURTEEN.divide(THIRTY_ONE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
@@ -169,7 +172,7 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
         expectedValue = expectedValue.add(numberOfCyclesEvent2.multiply(TEN));
         expectedValue = expectedValue.setScale(NUMBER_OF_DECIMALS);
 
-        assertEquals(invoice.getTotalAmount(), expectedValue);
+        assertEquals(invoiceItems.getTotalAmount(), expectedValue);
     }
 
     @Test
@@ -195,13 +198,13 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
                                                1, BillingMode.IN_ADVANCE);
         events.add(event3);
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
         DateTime targetDate = buildDateTime(2011, 12, 3);
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 3);
-        assertEquals(invoice.getTotalAmount(), FIVE.add(TEN).add(TWO.multiply(THIRTY)).setScale(NUMBER_OF_DECIMALS));
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 3);
+        assertEquals(invoiceItems.getTotalAmount(), FIVE.add(TEN).add(TWO.multiply(THIRTY)).setScale(NUMBER_OF_DECIMALS));
     }
 
     @Test
@@ -218,112 +221,194 @@ public class DefaultInvoiceGeneratorTests extends InvoicingTestBase {
                                                1, BillingMode.IN_ADVANCE);
         events.add(event1);
 
-        InvoiceItemList invoiceItems = new InvoiceItemList();
+        InvoiceItemList existingInvoiceItems = new InvoiceItemList();
         InvoiceItem invoiceItem = new InvoiceItem(subscriptionId, startDate, buildDateTime(2012, 1, 1), "",
                                                  rate.multiply(FOUR), rate, Currency.USD);
-        invoiceItems.add(invoiceItem);
+        existingInvoiceItems.add(invoiceItem);
 
         DateTime targetDate = buildDateTime(2011, 12, 3);
-        Invoice invoice = generator.generateInvoice(events, invoiceItems, targetDate, Currency.USD);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, Currency.USD);
 
-        assertNotNull(invoice);
-        assertEquals(invoice.getNumberOfItems(), 0);
-        assertEquals(invoice.getTotalAmount(), ZERO);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), 0);
+        assertEquals(invoiceItems.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 1: change of phase from trial to discount followed by immediate cancellation; (covers phase change, cancel, 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();
+        // plan 3: change of term from monthly (BCD = 20) to annual (BCD = 31; immediate)
+        // plan 4: change of plan, effective EOT, BCD = 7 (covers change of plan)
+        // plan 5: addon to plan 2, with bill cycle alignment to plan; immediate cancellation
 
         UUID subscriptionId1 = UUID.randomUUID();
-        DateTime startDate1 = buildDateTime(2011, 1, 5);
-        String planName1 = "World Domination";
+        String planName1 = "Change from trial to discount with immediate cancellation";
+        String plan1PhaseName1 = "Trial"; String plan1PhaseName2 = "Discount"; String plan1phase3 = "Cancel";
+        DateTime plan1StartDate = buildDateTime(2011, 1, 5);
+        DateTime plan1PhaseChangeDate = buildDateTime(2011, 4, 5);
+        DateTime plan1CancelDate = buildDateTime(2011, 4, 29);
+
+        UUID subscriptionId2 = UUID.randomUUID();
+        String planName2 = "Change phase from trial to discount to evergreen";
+        String plan2PhaseName1 = "Trial"; String plan2PhaseName2 = "Discount"; String plan2PhaseName3 = "Evergreen";
+        DateTime plan2StartDate = buildDateTime(2011, 3, 10);
+        DateTime plan2PhaseChangeToDiscountDate = buildDateTime(2011, 6, 10);
+        DateTime plan2PhaseChangeToEvergreenDate = buildDateTime(2011, 9, 10);
+
+        UUID subscriptionId3 = UUID.randomUUID();
+        String planName3 = "Upgrade with immediate change, BCD = 31";
+        String plan3PhaseName1 = "Evergreen monthly"; String plan3PhaseName2 = "Evergreen annual";
+        DateTime plan3StartDate = buildDateTime(2011, 5, 20);
+        DateTime plan3UpgradeToAnnualDate = buildDateTime(2011, 7, 31);
+
+        UUID subscriptionId4 = UUID.randomUUID();
+        String planName4a = "Plan change effective EOT; plan 1";
+        String planName4b = "Plan change effective EOT; plan 2";
+        String plan4PhaseName = "Evergreen";
+        DateTime plan4StartDate = buildDateTime(2011, 6, 7);
+        DateTime plan4ChangeOfPlanDate = buildDateTime(2011, 8, 7);
+
+        UUID subscriptionId5 = UUID.randomUUID();
+        String planName5 = "Add-on";
+        String plan5PhaseName1 = "Evergreen"; String plan5PhaseName2 = "Cancel";
+        DateTime plan5StartDate = buildDateTime(2011, 6, 21);
+        DateTime plan5CancelDate = buildDateTime(2011, 10, 7);
 
-        IBillingEvent event;
         BigDecimal expectedAmount;
         InvoiceItemList invoiceItems = new InvoiceItemList();
+        BillingEventSet events = new BillingEventSet();
 
-        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
+        // on 1/5/2011, create subscription 1 (trial)
+        events.add(createBillingEvent(subscriptionId1, plan1StartDate, planName1, plan1PhaseName1, EIGHT, 5));
         expectedAmount = EIGHT;
+        testInvoiceGeneration(events, invoiceItems, plan1StartDate, 1, expectedAmount);
 
-        // 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 2/5/2011, invoice subscription 1 (trial)
+        expectedAmount = EIGHT;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 2, 5) , 1, expectedAmount);
 
-        // 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);
+        // on 3/5/2011, invoice subscription 1 (trial)
+        expectedAmount = EIGHT;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 3, 5), 1, expectedAmount);
+
+        // on 3/10/2011, create subscription 2 (trial)
+        events.add(createBillingEvent(subscriptionId2, plan2StartDate, planName2, plan2PhaseName1, TWENTY, 10));
+        expectedAmount = TWENTY;
+        testInvoiceGeneration(events, invoiceItems, plan2StartDate, 1, expectedAmount);
+
+        // on 4/5/2011, invoice subscription 1 (discount)
+        events.add(createBillingEvent(subscriptionId1, plan1PhaseChangeDate, planName1, plan1PhaseName2, TWELVE, 5));
+        expectedAmount = TWELVE;
+        testInvoiceGeneration(events, invoiceItems, plan1PhaseChangeDate, 1, expectedAmount);
+
+        // on 4/10/2011, invoice subscription 2 (trial)
+        expectedAmount = TWENTY;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 4, 10), 1, expectedAmount);
+
+        // on 4/29/2011, cancel subscription 1
+        events.add(createBillingEvent(subscriptionId1, plan1CancelDate, planName1, plan1phase3, ZERO, 5));
+        expectedAmount = TWELVE.multiply(SIX.divide(THIRTY, NUMBER_OF_DECIMALS, ROUNDING_METHOD)).negate().setScale(NUMBER_OF_DECIMALS);
+        testInvoiceGeneration(events, invoiceItems, plan1CancelDate, 2, expectedAmount);
+
+        // on 5/10/2011, invoice subscription 2 (trial)
+        expectedAmount = TWENTY;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 5, 10), 1, expectedAmount);
+
+        // on 5/20/2011, create subscription 3 (monthly)
+        events.add(createBillingEvent(subscriptionId3, plan3StartDate, planName3, plan3PhaseName1, TEN, 20));
+        expectedAmount = TEN;
+        testInvoiceGeneration(events, invoiceItems, plan3StartDate, 1, expectedAmount);
+
+        // on 6/7/2011, create subscription 4
+        events.add(createBillingEvent(subscriptionId4, plan4StartDate, planName4a, plan4PhaseName, FIFTEEN, 7));
+        expectedAmount = FIFTEEN;
+        testInvoiceGeneration(events, invoiceItems, plan4StartDate, 1, expectedAmount);
+
+        // on 6/10/2011, invoice subscription 2 (discount)
+        events.add(createBillingEvent(subscriptionId2, plan2PhaseChangeToDiscountDate, planName2, plan2PhaseName2, THIRTY, 10));
+        expectedAmount = THIRTY;
+        testInvoiceGeneration(events, invoiceItems, plan2PhaseChangeToDiscountDate, 1, expectedAmount);
+
+        // on 6/20/2011, invoice subscription 3 (monthly)
+        expectedAmount = TEN;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 6, 20), 1, expectedAmount);
+
+        // on 6/21/2011, create add-on (subscription 5)
+        events.add(createBillingEvent(subscriptionId5, plan5StartDate, planName5, plan5PhaseName1, TWENTY, 10));
+        expectedAmount = TWENTY.multiply(NINETEEN.divide(THIRTY, NUMBER_OF_DECIMALS, ROUNDING_METHOD)).setScale(NUMBER_OF_DECIMALS);
+        testInvoiceGeneration(events, invoiceItems, plan5StartDate, 1, expectedAmount);
+
+        // on 7/7/2011, invoice subscription 4 (plan 1)
+        expectedAmount = FIFTEEN;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 7, 7), 1, expectedAmount);
+
+        // on 7/10/2011, invoice subscription 2 (discount), invoice subscription 5
+        expectedAmount = THIRTY.add(TWENTY);
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 7, 10), 2, expectedAmount);
+
+        // on 7/20/2011, invoice subscription 3 (monthly)
+        expectedAmount = TEN;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 7, 20), 1, expectedAmount);
+
+        // on 7/31/2011, convert subscription 3 to annual
+        events.add(createAnnualBillingEvent(subscriptionId3, plan3UpgradeToAnnualDate, planName3, plan3PhaseName2, ONE_HUNDRED, 31));
+        expectedAmount = ONE_HUNDRED.subtract(TEN);
+        expectedAmount = expectedAmount.add(TEN.multiply(ELEVEN.divide(THIRTY_ONE, NUMBER_OF_DECIMALS, ROUNDING_METHOD)));
+        expectedAmount = expectedAmount.setScale(NUMBER_OF_DECIMALS);
+        testInvoiceGeneration(events, invoiceItems, plan3UpgradeToAnnualDate, 3, expectedAmount);
+
+        // on 8/7/2011, invoice subscription 4 (plan 2)
+        events.add(createBillingEvent(subscriptionId4, plan4ChangeOfPlanDate, planName4b, plan4PhaseName, TWENTY_FOUR, 7));
+        expectedAmount = TWENTY_FOUR;
+        testInvoiceGeneration(events, invoiceItems, plan4ChangeOfPlanDate, 1, expectedAmount);
+
+        // on 8/10/2011, invoice plan 2 (discount), invoice subscription 5
+        expectedAmount = THIRTY.add(TWENTY);
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 8, 10), 2, expectedAmount);
+
+        // on 9/7/2011, invoice subscription 4 (plan 2)
+        expectedAmount = TWENTY_FOUR;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 9, 7), 1, expectedAmount);
+
+        // on 9/10/2011, invoice plan 2 (evergreen), invoice subscription 5
+        events.add(createBillingEvent(subscriptionId2, plan2PhaseChangeToEvergreenDate, planName2, plan2PhaseName3, FORTY, 10));
+        expectedAmount = FORTY.add(TWENTY);
+        testInvoiceGeneration(events, invoiceItems, plan2PhaseChangeToEvergreenDate, 2, expectedAmount);
+
+        // on 10/7/2011, invoice subscription 4 (plan 2), cancel subscription 5
+        events.add(createBillingEvent(subscriptionId5, plan5CancelDate, planName5, plan5PhaseName2, ZERO, 10));
+        expectedAmount = TWENTY_FOUR.add(TWENTY.multiply(THREE.divide(THIRTY)).negate().setScale(NUMBER_OF_DECIMALS));
+        testInvoiceGeneration(events, invoiceItems, plan5CancelDate, 3, expectedAmount);
+
+        // on 10/10/2011, invoice plan 2 (evergreen)
+        expectedAmount = FORTY ;
+        testInvoiceGeneration(events, invoiceItems, buildDateTime(2011, 10, 10), 1, expectedAmount);
+    }
 
-        // generate correcting invoice item for cancellation
-        Invoice invoice = generator.generateInvoice(events, new InvoiceItemList(), plan1CancelDate, Currency.USD);
-        BigDecimal totalToDate = invoice.getTotalAmount();
+    private BillingEvent createBillingEvent(UUID subscriptionId, DateTime startDate, String planName, String planPhaseName,
+                                            BigDecimal rate, int billCycleDay) {
+        return new BillingEvent(subscriptionId, startDate, planName, planPhaseName,
+                                new InternationalPriceMock(rate), BillingPeriod.MONTHLY,
+                                billCycleDay, BillingMode.IN_ADVANCE);
 
-        BigDecimal invoicedAmount = ZERO;
-        for (InvoiceItem item : invoiceItems) {
-            invoicedAmount = invoicedAmount.add(item.getAmount());
-        }
+    }
 
-        BigDecimal creditAmount = totalToDate.subtract(invoicedAmount).setScale(NUMBER_OF_DECIMALS);
+    private BillingEvent createAnnualBillingEvent(UUID subscriptionId, DateTime startDate, String planName, String planPhaseName,
+                                                  BigDecimal rate, int billCycleDay) {
+        return new BillingEvent(subscriptionId, startDate, planName, planPhaseName,
+                                new InternationalPriceMock(rate), BillingPeriod.ANNUAL,
+                                billCycleDay, BillingMode.IN_ADVANCE);
 
-        testInvoiceGeneration(events, invoiceItems, plan1CancelDate, 2, creditAmount);
     }
 
-    private void testInvoiceGeneration(BillingEventSet events, InvoiceItemList invoiceItems, DateTime targetDate, int expectedNumberOfItems, BigDecimal expectedAmount) {
+    private void testInvoiceGeneration(BillingEventSet events, InvoiceItemList existingInvoiceItems, 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);
+        InvoiceItemList invoiceItems = generator.generateInvoiceItems(events, existingInvoiceItems, targetDate, currency);
+        existingInvoiceItems.addAll(invoiceItems);
+        assertNotNull(invoiceItems);
+        assertEquals(invoiceItems.size(), expectedNumberOfItems);
+        assertEquals(invoiceItems.getTotalAmount(), expectedAmount);
     }
 
     // TODO: Jeff C -- how do we ensure that an annual add-on is properly aligned *at the end* with the base plan?
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/InvoicingTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/tests/InvoicingTestBase.java
index 9413b4c..afc9d1c 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/InvoicingTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/InvoicingTestBase.java
@@ -34,34 +34,36 @@ public abstract class InvoicingTestBase {
     protected static final BigDecimal THREE = new BigDecimal("3.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal FOUR = new BigDecimal("4.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal FIVE = new BigDecimal("5.0").setScale(NUMBER_OF_DECIMALS);
-
+    protected static final BigDecimal SIX = new BigDecimal("6.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal SEVEN = new BigDecimal("7.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal EIGHT = new BigDecimal("8.0").setScale(NUMBER_OF_DECIMALS);
 
     protected static final BigDecimal TEN = new BigDecimal("10.0").setScale(NUMBER_OF_DECIMALS);
-
+    protected static final BigDecimal ELEVEN = new BigDecimal("11.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal TWELVE = new BigDecimal("12.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal THIRTEEN = new BigDecimal("13.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal FOURTEEN = new BigDecimal("14.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal FIFTEEN = new BigDecimal("15.0").setScale(NUMBER_OF_DECIMALS);
 
+    protected static final BigDecimal NINETEEN = new BigDecimal("19.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal TWENTY = new BigDecimal("20.0").setScale(NUMBER_OF_DECIMALS);
-    protected static final BigDecimal TWENTY_ONE = new BigDecimal("21.0").setScale(NUMBER_OF_DECIMALS);
 
     protected static final BigDecimal TWENTY_FOUR = new BigDecimal("24.0").setScale(NUMBER_OF_DECIMALS);
 
-    protected static final BigDecimal TWENTY_SIX = new BigDecimal("26.0").setScale(NUMBER_OF_DECIMALS);
-
     protected static final BigDecimal TWENTY_EIGHT = new BigDecimal("28.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal TWENTY_NINE = new BigDecimal("29.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal THIRTY = new BigDecimal("30.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal THIRTY_ONE = new BigDecimal("31.0").setScale(NUMBER_OF_DECIMALS);
 
+    protected static final BigDecimal FORTY = new BigDecimal("40.0").setScale(NUMBER_OF_DECIMALS);
+
     protected static final BigDecimal EIGHTY_NINE = new BigDecimal("89.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal NINETY = new BigDecimal("90.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal NINETY_ONE = new BigDecimal("91.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal NINETY_TWO = new BigDecimal("92.0").setScale(NUMBER_OF_DECIMALS);
 
+    protected static final BigDecimal ONE_HUNDRED = new BigDecimal("100.0").setScale(NUMBER_OF_DECIMALS);
+
     protected static final BigDecimal THREE_HUNDRED_AND_SIXTY_FIVE = new BigDecimal("365.0").setScale(NUMBER_OF_DECIMALS);
     protected static final BigDecimal THREE_HUNDRED_AND_SIXTY_SIX = new BigDecimal("366.0").setScale(NUMBER_OF_DECIMALS);