killbill-memoizeit

Merge branch 'invoice2'

3/2/2014 9:55:47 PM

Changes

invoice/src/test/java/com/ning/billing/invoice/generator/DefaultInvoiceGeneratorWithSwitchRepairLogic.java 65(+0 -65)

invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java 474(+0 -474)

invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorUnit.java 232(+0 -232)

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java
index c52cdbd..3dca515 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixIntegrationModule.java
@@ -42,7 +42,7 @@ import com.ning.billing.currency.glue.CurrencyModule;
 import com.ning.billing.entitlement.EntitlementService;
 import com.ning.billing.entitlement.glue.DefaultEntitlementModule;
 import com.ning.billing.invoice.api.InvoiceService;
-import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic;
+import com.ning.billing.invoice.generator.DefaultInvoiceGenerator;
 import com.ning.billing.invoice.generator.InvoiceGenerator;
 import com.ning.billing.invoice.glue.DefaultInvoiceModule;
 import com.ning.billing.junction.glue.DefaultJunctionModule;
@@ -144,8 +144,7 @@ public class BeatrixIntegrationModule extends AbstractModule {
         }
 
         protected void installInvoiceGenerator() {
-            bind(InvoiceGenerator.class).to(DefaultInvoiceGeneratorWithSwitchRepairLogic.class).asEagerSingleton();
-            bind(DefaultInvoiceGeneratorWithSwitchRepairLogic.class).asEagerSingleton();
+            bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton();
         }
     }
 
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 810e51b..c5cd4b0 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -181,8 +181,8 @@ public class TestOverdueIntegration extends TestOverdueBase {
 
         invoiceChecker.checkInvoice(account.getId(), 3, callContext,
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-102.13")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("102.13")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-104.83")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("104.83")));
 
         // Add 10 days to generate next invoice. We verify that we indeed have a notification for nextBillingDate
         addDaysAndCheckForCompletion(10, NextEvent.INVOICE, NextEvent.PAYMENT);
@@ -190,7 +190,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
         invoiceChecker.checkInvoice(account.getId(), 4, callContext,
                                     // Item for the upgraded recurring plan
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 2), new LocalDate(2012, 8, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("-102.13")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 2), new LocalDate(2012, 8, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("-104.83")));
 
         invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 8, 31), callContext);
 
@@ -276,14 +276,14 @@ public class TestOverdueIntegration extends TestOverdueBase {
         invoiceChecker.checkInvoice(account.getId(), 3, callContext,
                                     // New invoice for the partial period since we unblocked on the 1st and so are missing the 31 july
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-166.64")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 7, 31), InvoiceItemType.CBA_ADJ, new BigDecimal("166.64")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-169.32")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 7, 31), InvoiceItemType.CBA_ADJ, new BigDecimal("169.32")));
 
 
         invoiceChecker.checkInvoice(account.getId(), 4, callContext,
                                     // New invoice for the partial period since we unblocked on the 1st and so are missing the 31 july
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 7, 31), InvoiceItemType.CBA_ADJ, new BigDecimal("-166.64")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 7, 31), InvoiceItemType.CBA_ADJ, new BigDecimal("-169.32")));
 
         // Move one month ahead, and check if we get the next invoice
         addDaysAndCheckForCompletion(31, NextEvent.INVOICE, NextEvent.PAYMENT);
@@ -378,13 +378,13 @@ public class TestOverdueIntegration extends TestOverdueBase {
         invoiceChecker.checkInvoice(account.getId(), 3, callContext,
                                     // New invoice for the partial period since we unblocked on the 1st and so are missing the 31 july
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-166.64")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 8, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("166.64")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-169.32")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 8, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("169.32")));
 
         invoiceChecker.checkInvoice(account.getId(), 4, callContext,
                                     // New invoice for the partial period since we unblocked on the 1st and so are missing the 31 july
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("241.88")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 8, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-166.64")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 8, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("-169.32")));
 
         // Move one month ahead, and check if we get the next invoice
         addDaysAndCheckForCompletion(30, NextEvent.INVOICE, NextEvent.PAYMENT);
@@ -465,16 +465,16 @@ public class TestOverdueIntegration extends TestOverdueBase {
 
         invoiceChecker.checkInvoice(account.getId(), 3, callContext,
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-102.13")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("102.13")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-104.83")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("104.83")));
 
         // Do an upgrade now
         checkChangePlanWithOverdueState(baseEntitlement, false, false);
 
         invoiceChecker.checkRepairedInvoice(account.getId(), 3, callContext,
                                             new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-102.13")),
-                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("102.13")),
+                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 10), new LocalDate(2012, 7, 23), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-104.83")),
+                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("104.83")),
                                             new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-64.51")),
                                             new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 23), new LocalDate(2012, 7, 23), InvoiceItemType.CBA_ADJ, new BigDecimal("64.51")));
 
@@ -487,7 +487,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
         invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
 
         // Verify the account balance:
-        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-11.79")), 0);
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-14.49")), 0);
     }
 
     @Test(groups = "slow", description = "Test overdue stages and follow with an immediate change of plan and use of credit", enabled=false)
@@ -539,6 +539,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
 
         allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(false);
 
+
         invoiceChecker.checkInvoice(account.getId(), 2, callContext,
                                     // New invoice for the part that was unblocked up to the BCD
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2013, 5, 31), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
@@ -642,8 +643,8 @@ public class TestOverdueIntegration extends TestOverdueBase {
         invoiceChecker.checkInvoice(account.getId(), 3, callContext,
                                     // Item for the upgraded recurring plan
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 15), new LocalDate(2012, 7, 25), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-76.60")),
-                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("76.60")));
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 15), new LocalDate(2012, 7, 25), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-80.63")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("80.63")));
 
 
 
@@ -653,8 +654,8 @@ public class TestOverdueIntegration extends TestOverdueBase {
 
         invoiceChecker.checkRepairedInvoice(account.getId(), 3, callContext,
                                             new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 15), new LocalDate(2012, 7, 25), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-76.60")),
-                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("76.60")),
+                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 15), new LocalDate(2012, 7, 25), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-80.63")),
+                                            new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("80.63")),
                                             new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 31), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-48.37")),
                                             new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 25), new LocalDate(2012, 7, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("48.37")));
 
@@ -664,7 +665,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
 
         invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
 
-        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-8.88")), 0);
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(new BigDecimal("-12.91")), 0);
     }
 
     @Test(groups = "slow", description = "Test overdue from non paid external charge")
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
index 6a15797..77fa87a 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
@@ -208,8 +208,8 @@ public class TestBundleTransfer extends TestIntegrationBase {
         // CHECK OLD ACCOUNTS ITEMS
         ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 9), InvoiceItemType.RECURRING, new BigDecimal("66.66")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 3), new LocalDate(2012, 5, 9), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-49.99")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 3), new LocalDate(2012, 5, 3), InvoiceItemType.CBA_ADJ, new BigDecimal("49.99")));
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 3), new LocalDate(2012, 5, 9), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-50.00")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 3), new LocalDate(2012, 5, 3), InvoiceItemType.CBA_ADJ, new BigDecimal("50.00")));
         invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
 
         // CHECK NEW ACCOUNT ITEMS
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
index c6d30c9..77104d0 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
@@ -111,7 +111,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
     protected static final Logger log = LoggerFactory.getLogger(TestIntegrationBase.class);
     protected static long AT_LEAST_ONE_MONTH_MS = 32L * 24L * 3600L * 1000L;
 
-    protected static final long DELAY = 10000;
+    protected static final long DELAY = 10000; // * 100000;
 
     @Inject
     protected Lifecycle lifecycle;
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index 153a605..c3b6a6f 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -22,8 +22,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import javax.inject.Inject;
-
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 import org.testng.Assert;
@@ -45,8 +43,6 @@ import com.ning.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoiceItemType;
-import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic;
-import com.ning.billing.invoice.generator.DefaultInvoiceGeneratorWithSwitchRepairLogic.REPAIR_INVOICE_LOGIC;
 import com.ning.billing.payment.api.Payment;
 import com.ning.billing.payment.api.PaymentStatus;
 
@@ -57,14 +53,9 @@ import static org.testng.Assert.assertNotNull;
 
 public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
 
-    @Inject
-    private DefaultInvoiceGeneratorWithSwitchRepairLogic invoiceGenerator;
-
     @AfterMethod(groups = "slow")
     public void afterMethod() throws Exception {
         super.afterMethod();
-        // Make sure to reset to default invoice logic so subsequent test will pass
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
     }
 
     @Test(groups = "slow")
@@ -138,7 +129,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         // Force a plan change
         //
         changeEntitlementAndCheckForCompletion(bpEntitlement, "Blowdart", term, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
-
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
         assertEquals(invoices.size(), 3);
 
@@ -151,9 +141,10 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10")),
                 // TODO PIERRE The cba start_date/end_date are created using the callcontext
                 new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.CBA_ADJ, new BigDecimal("10")),
-                // You can check here that 239.95 - 249.95/31 = 231.88
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-231.88")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("231.88")));
+                // The pro-rated piece is ~ 249.95 - (249.95 / 31) ~ 241.88. However we adjusted the item so max available amount is  249.95 - 10 = 239.95.
+                // So we consume all of it since max amount is less than 241.88.
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-239.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("239.95")));
         invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
 
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -286,8 +277,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("8.02")),
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 5, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("-8.02")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.69")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.69")));
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.70")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.70")));
         invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
 
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -323,8 +314,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("8.02")),
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 5, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("-8.02")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.69")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.69")));
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.70")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.70")));
         invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
 
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -365,8 +356,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("8.02")),
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 7), new LocalDate(2012, 5, 7), InvoiceItemType.CBA_ADJ, new BigDecimal("-8.02")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.69")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.69")));
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-7.70")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 8), new LocalDate(2012, 5, 8), InvoiceItemType.CBA_ADJ, new BigDecimal("7.70")));
         invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
 
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
@@ -494,222 +485,8 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
     }
 
     @Test(groups = "slow")
-    public void testInvoiceLogicWithFullRepairFollowedByPartialRepair() throws Exception {
-
-        // START TEST WITH OLD FULL_REPAIR LOGIC
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.FULL_REPAIR);
-
-        final LocalDate today = new LocalDate(2012, 4, 1);
-        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
-
-        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
-        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
-
-        final String productName = "Shotgun";
-        final BillingPeriod term = BillingPeriod.ANNUAL;
-        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
-
-        //
-        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
-        //
-        DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
-        assertNotNull(bpEntitlement);
-
-        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
-
-        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
-
-        // Move out of trials for interesting invoices adjustments
-        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
-        clock.addDays(40);
-        assertListenerStatus();
-
-        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 2);
-        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        //
-        // FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
-        //
-        changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.MONTHLY, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
-
-        assertListenerStatus();
-
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 3);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
-        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
-
-        // NOW SWITCH BACK TO PARTIAL REPAIR LOGIC AND GENERATE NEXT 2 INVOICES
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
-
-        busHandler.pushExpectedEvents(NextEvent.INVOICE);
-        clock.addMonths(1);
-        assertListenerStatus();
-
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 4);
-
-        // RECHECK PREVIOUS INVOICE DID NOT CHANGE
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2399.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2399.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
-        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
-
-        // AND THEN CHECK NEW INVOICE
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 11), new LocalDate(2012, 6, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
-        invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
-
-        busHandler.pushExpectedEvents(NextEvent.INVOICE);
-        clock.addMonths(1);
-        assertListenerStatus();
-
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 5);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 1), new LocalDate(2012, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 11), new LocalDate(2012, 7, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
-        invoiceChecker.checkInvoice(invoices.get(4).getId(), callContext, toBeChecked);
-    }
-
-    @Test(groups = "slow")
-    public void testInvoiceLogicWithFullRepairFollowedByPartialRepairWithItemAdjustment() throws Exception {
-
-        // START TEST WITH OLD FULL_REPAIR LOGIC
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.FULL_REPAIR);
-
-        final LocalDate today = new LocalDate(2012, 4, 1);
-        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
-
-        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
-        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
-
-        final String productName = "Shotgun";
-        final BillingPeriod term = BillingPeriod.ANNUAL;
-        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
-
-        //
-        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
-        //
-        DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
-        assertNotNull(bpEntitlement);
-        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
-
-        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
-
-        // Move out of trials for interesting invoices adjustments
-        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
-        clock.addDays(40);
-        assertListenerStatus();
-
-        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 2);
-        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        //
-        // ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
-        //
-        final Invoice invoice1 = invoices.get(1);
-        final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), callContext);
-        final ExpectedPaymentCheck expectedPaymentCheck = new ExpectedPaymentCheck(clock.getUTCNow().toLocalDate(), new BigDecimal("2399.95"), PaymentStatus.SUCCESS, invoice1.getId(), Currency.USD);
-        final Payment payment1 = payments.get(0);
-
-        final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
-        iias.put(invoice1.getInvoiceItems().get(0).getId(), new BigDecimal("10.00"));
-        busHandler.pushExpectedEvents(NextEvent.INVOICE_ADJUSTMENT);
-        paymentApi.createRefundWithItemsAdjustments(account, payment1.getId(), iias, callContext);
-        assertListenerStatus();
-
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 2);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
-                // TODO SETPH the  ITEM_ADJ seems to be created with the callcontext getCreatedDate()
-                new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10.00")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        //
-        // FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
-        //
-        changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.MONTHLY, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT);
-
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 3);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2389.95")),
-                new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10.00")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2389.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
-        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
-
-        // NOW SWITCH BACK TO PARTIAL REPAIR LOGIC AND GENERATE NEXT 2 INVOICES
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
-
-        busHandler.pushExpectedEvents(NextEvent.INVOICE);
-        clock.addMonths(1);
-        assertListenerStatus();
-
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
-        assertEquals(invoices.size(), 4);
-
-        // RECHECK PREVIOUS INVOICE DID NOT CHANGE
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2389.95")),
-                new ExpectedInvoiceItemCheck(callContext.getCreatedDate().toLocalDate(), callContext.getCreatedDate().toLocalDate(), InvoiceItemType.ITEM_ADJ, new BigDecimal("-10.00")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("2389.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 11), InvoiceItemType.RECURRING, new BigDecimal("65.76")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("169.32")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-235.08")));
-        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
-
-        // AND THEN CHECK NEW INVOICE
-        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 11), new LocalDate(2012, 6, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-249.95")));
-        invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
-    }
-
-    @Test(groups = "slow")
     public void testRepairWithFullItemAdjustment() throws Exception {
 
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
-
         final LocalDate today = new LocalDate(2013, 7, 19);
         final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
 
@@ -740,7 +517,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
         invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
 
-
         // Move clock to 2013-09-17
         clock.addDays(30);
         busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE_ADJUSTMENT);
@@ -755,8 +531,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2013, 9, 17), InvoiceItemType.CBA_ADJ, new BigDecimal("2202.67")));
         invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
 
-
-
         //
         // ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
         //
@@ -789,8 +563,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
     @Test(groups = "slow")
     public void testRepairWithPartialItemAdjustment() throws Exception {
 
-        invoiceGenerator.setDefaultRepairLogic(REPAIR_INVOICE_LOGIC.PARTIAL_REPAIR);
-
         final LocalDate today = new LocalDate(2013, 7, 19);
         final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
 
@@ -821,7 +593,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2013, 8, 18), new LocalDate(2014, 8, 18), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
         invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
 
-
         // Move clock to 2013-09-17
         clock.addDays(30);
         busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE_ADJUSTMENT);
@@ -836,8 +607,6 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2013, 9, 17), new LocalDate(2013, 9, 17), InvoiceItemType.CBA_ADJ, new BigDecimal("2202.67")));
         invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
 
-
-
         //
         // ITEM ADJUSTMENT PRIOR TO DOING THE REPAIR
         //
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java
new file mode 100644
index 0000000..f8abb84
--- /dev/null
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2010-2014 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.beatrix.integration;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import com.ning.billing.ObjectType;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import com.ning.billing.catalog.api.BillingActionPolicy;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.DefaultEntitlement;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.util.tag.ControlTagType;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+public class TestIntegrationWithDifferentBillingPeriods extends TestIntegrationBase {
+
+    @Test(groups = "slow")
+    public void testChangeMonthlyToAnnual() throws Exception {
+
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+        final String productName = "Shotgun";
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(31);
+        assertListenerStatus();
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+        //
+        // MOVE MONTHLY TO ANNUAL
+        //
+        clock.addDays(10);
+
+        changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.ANNUAL, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT, NextEvent.PAYMENT);
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 3);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 5, 12), InvoiceItemType.CBA_ADJ, new BigDecimal("161.27")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-161.27")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2327.71")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 5, 12), InvoiceItemType.CBA_ADJ, new BigDecimal("-161.27")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+    }
+
+    @Test(groups = "slow")
+    public void testChangeMonthlyToQuarterly() throws Exception {
+
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+        final String productName = "Pistol";
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(31);
+        assertListenerStatus();
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+        //
+        // MOVE MONTHLY TO QUARTERLY
+        //
+        clock.addDays(10);
+
+        changeEntitlementAndCheckForCompletion(bpEntitlement, productName, BillingPeriod.QUARTERLY, BillingActionPolicy.IMMEDIATE, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_ADJUSTMENT, NextEvent.PAYMENT);
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 3);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 5, 12), InvoiceItemType.CBA_ADJ, new BigDecimal("19.32")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.32")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("61.58")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 5, 12), InvoiceItemType.CBA_ADJ, new BigDecimal("-19.32")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+        // Move to 1020-08-01
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(20);
+        clock.addMonths(2);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 4);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 11, 1), InvoiceItemType.RECURRING, new BigDecimal("69.95")));
+        invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+
+    }
+
+    @Test(groups = "slow")
+    public void testPauseResumeAnnual() throws Exception {
+
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+        final String productName = "Shotgun";
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(31);
+        assertListenerStatus();
+
+        // 2012-5-12
+        clock.addDays(10);
+
+        busHandler.pushExpectedEvents(NextEvent.PAUSE, NextEvent.BLOCK, NextEvent.INVOICE_ADJUSTMENT);
+        entitlementApi.pause(bpEntitlement.getBundleId(), clock.getUTCNow().toLocalDate(), callContext);
+        assertListenerStatus();
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2012, 5, 12), InvoiceItemType.CBA_ADJ, new BigDecimal("2327.71")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2327.71")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+        // 2012-6-4
+        clock.addDays(23);
+
+        busHandler.pushExpectedEvents(NextEvent.RESUME, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT);
+        entitlementApi.resume(bpEntitlement.getBundleId(), clock.getUTCNow().toLocalDate(), callContext);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 3);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 4), new LocalDate(2013, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("2380.27")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 4), new LocalDate(2012, 6, 4), InvoiceItemType.CBA_ADJ, new BigDecimal("-2327.71")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addYears(1);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 4);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 6, 1), new LocalDate(2014, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+    }
+
+    @Test(groups = "slow")
+    public void testPauseResumeAnnualWithInvoicingOff() throws Exception {
+
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime(DateTimeZone.UTC).getMillis() - clock.getUTCNow().getMillis());
+
+        final String productName = "Shotgun";
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), callContext).size(), 1);
+
+        assertEquals(bpEntitlement.getSubscriptionBase().getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(31);
+        assertListenerStatus();
+
+        // Auto invoice off
+        busHandler.pushExpectedEvents(NextEvent.TAG);
+        tagUserApi.addTag(account.getId(), ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), callContext);
+        assertListenerStatus();
+
+        // 2012-5-12
+        clock.addDays(10);
+
+        busHandler.pushExpectedEvents(NextEvent.PAUSE, NextEvent.BLOCK);
+        entitlementApi.pause(bpEntitlement.getBundleId(), clock.getUTCNow().toLocalDate(), callContext);
+        assertListenerStatus();
+
+        // 2012-6-4
+        clock.addDays(23);
+        busHandler.pushExpectedEvents(NextEvent.RESUME, NextEvent.BLOCK);
+        entitlementApi.resume(bpEntitlement.getBundleId(), clock.getUTCNow().toLocalDate(), callContext);
+        assertListenerStatus();
+
+
+        busHandler.pushExpectedEvents(NextEvent.TAG, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
+        tagUserApi.removeTag(account.getId(), ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), callContext);
+        assertListenerStatus();
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 3);
+
+
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 4), new LocalDate(2012, 6, 4), InvoiceItemType.CBA_ADJ, new BigDecimal("2327.71")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 12), new LocalDate(2013, 5, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-2327.71")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 4), new LocalDate(2013, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("2380.27")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 4), new LocalDate(2012, 6, 4), InvoiceItemType.CBA_ADJ, new BigDecimal("-2327.71")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addYears(1);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 4);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2013, 6, 1), new LocalDate(2014, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
+    }
+}
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestSubscription.java
index c99bca8..a8b442b 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestSubscription.java
@@ -109,8 +109,8 @@ public class TestSubscription extends TestIntegrationBase {
         invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
 
         toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("137.76")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-137.76")));
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2334.19")),
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 11), new LocalDate(2012, 5, 11), InvoiceItemType.CBA_ADJ, new BigDecimal("-2334.19")));
         invoiceChecker.checkInvoice(invoices.get(3).getId(), callContext, toBeChecked);
 
     }
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
index c0ab36a..e80d59e 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
@@ -20,6 +20,8 @@ import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
@@ -39,7 +41,9 @@ import com.ning.billing.invoice.api.InvoiceUserApi;
 import com.ning.billing.subscription.api.SubscriptionBase;
 import com.ning.billing.util.callcontext.CallContext;
 
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
 import static org.testng.Assert.assertNotNull;
@@ -92,7 +96,34 @@ public class InvoiceChecker {
         final List<InvoiceItem> actual = invoice.getInvoiceItems();
         Assert.assertEquals(actual.size(), expected.size());
         for (final ExpectedInvoiceItemCheck cur : expected) {
+
             boolean found = false;
+
+            // First try to find exact match; this is necessary because the for loop below might encounter a similar item -- for instance
+            // same type, same dates, but different amount and choke.
+            final InvoiceItem foundItem = Iterables.tryFind(actual, new Predicate<InvoiceItem>() {
+                @Override
+                public boolean apply(final InvoiceItem input) {
+                    if (input.getInvoiceItemType() != cur.getType() || (cur.shouldCheckDates() && input.getStartDate().compareTo(cur.getStartDate()) != 0)) {
+                        return false;
+                    }
+                    if (input.getAmount().compareTo(cur.getAmount()) != 0) {
+                        return false;
+                    }
+
+                    if (!cur.shouldCheckDates() ||
+                        (cur.getEndDate() == null && input.getEndDate() == null) ||
+                        (cur.getEndDate() != null && input.getEndDate() != null && cur.getEndDate().compareTo(input.getEndDate()) == 0)) {
+                        return true;
+                    }
+                    return false;
+                }
+            }).orNull();
+            if (foundItem != null) {
+                continue;
+            }
+
+            // If we could not find it, we still loop again, so that error message helps to debug when there is a 'similar' item.
             for (final InvoiceItem in : actual) {
                 // Match first on type and start date
                 if (in.getInvoiceItemType() != cur.getType() || (cur.shouldCheckDates() && in.getStartDate().compareTo(cur.getStartDate()) != 0)) {
diff --git a/beatrix/src/test/resources/catalogSample.xml b/beatrix/src/test/resources/catalogSample.xml
index d6531b1..e7e47d2 100644
--- a/beatrix/src/test/resources/catalogSample.xml
+++ b/beatrix/src/test/resources/catalogSample.xml
@@ -322,6 +322,40 @@
                 </recurringPrice>
             </finalPhase>
         </plan>
+        <plan name="pistol-quarterly">
+            <product>Pistol</product>
+            <initialPhases>
+                <phase type="TRIAL">
+                    <duration>
+                        <unit>DAYS</unit>
+                        <number>30</number>
+                    </duration>
+                    <billingPeriod>NO_BILLING_PERIOD</billingPeriod>
+                    <fixedPrice> <!-- empty price implies $0 -->
+                    </fixedPrice>
+                </phase>
+            </initialPhases>
+            <finalPhase type="EVERGREEN">
+                <duration>
+                    <unit>UNLIMITED</unit>
+                </duration>
+                <billingPeriod>QUARTERLY</billingPeriod>
+                <recurringPrice>
+                    <price>
+                        <currency>GBP</currency>
+                        <value>69.95</value>
+                    </price>
+                    <price>
+                        <currency>EUR</currency>
+                        <value>69.95</value>
+                    </price>
+                    <price>
+                        <currency>USD</currency>
+                        <value>69.95</value>
+                    </price>
+                </recurringPrice>
+            </finalPhase>
+        </plan>
         <plan name="pistol-annual">
             <product>Pistol</product>
             <initialPhases>
@@ -867,6 +901,7 @@
                 <plan>shotgun-monthly</plan>
                 <plan>assault-rifle-monthly</plan>
                 <plan>pistol-annual</plan>
+                <plan>pistol-quarterly</plan>
                 <plan>shotgun-annual</plan>
                 <plan>assault-rifle-annual</plan>
                 <plan>laser-scope-monthly</plan>
diff --git a/invoice/src/main/java/com/ning/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/com/ning/billing/invoice/generator/BillingIntervalDetail.java
index c54a084..8c1d9cc 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/generator/BillingIntervalDetail.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/generator/BillingIntervalDetail.java
@@ -20,10 +20,9 @@ import org.joda.time.LocalDate;
 
 import com.ning.billing.catalog.api.BillingPeriod;
 
-public class BillingIntervalDetail {
-
+import com.google.common.annotations.VisibleForTesting;
 
-    private final boolean shouldUsePatch = true;
+public class BillingIntervalDetail {
 
     private final LocalDate startDate;
     private final LocalDate endDate;
@@ -68,7 +67,8 @@ public class BillingIntervalDetail {
         calculateLastBillingCycleDate();
     }
 
-    private void calculateFirstBillingCycleDate() {
+    @VisibleForTesting
+    void calculateFirstBillingCycleDate() {
 
         final int lastDayOfMonth = startDate.dayOfMonth().getMaximumValue();
         final LocalDate billingCycleDate;
@@ -78,11 +78,12 @@ public class BillingIntervalDetail {
             billingCycleDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), billingCycleDay, startDate.getChronology());
         }
 
+        final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
         LocalDate proposedDate = billingCycleDate;
         while (proposedDate.isBefore(startDate)) {
-            proposedDate = proposedDate.plusMonths(1);
+            proposedDate = proposedDate.plusMonths(numberOfMonthsInPeriod);
         }
-        firstBillingCycleDate = proposedDate;
+        firstBillingCycleDate = alignProposedBillCycleDate(proposedDate);
     }
 
     private void calculateEffectiveEndDate() {
@@ -144,9 +145,6 @@ public class BillingIntervalDetail {
     // We start from a billCycleDate
     //
     private LocalDate alignProposedBillCycleDate(final LocalDate proposedDate) {
-        if (!shouldUsePatch) {
-            return proposedDate;
-        }
         final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
 
         int proposedBillCycleDate = proposedDate.getDayOfMonth();
diff --git a/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
index 7397f07..6b02d6c 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -18,9 +18,6 @@ package com.ning.billing.invoice.generator;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -28,7 +25,6 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
-import org.joda.time.Days;
 import org.joda.time.LocalDate;
 import org.joda.time.Months;
 import org.slf4j.Logger;
@@ -41,7 +37,6 @@ import com.ning.billing.clock.Clock;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoiceItem;
-import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.model.BillingMode;
 import com.ning.billing.invoice.model.DefaultInvoice;
 import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
@@ -50,47 +45,14 @@ import com.ning.billing.invoice.model.InvalidDateSequenceException;
 import com.ning.billing.invoice.model.InvoicingConfiguration;
 import com.ning.billing.invoice.model.RecurringInvoiceItem;
 import com.ning.billing.invoice.model.RecurringInvoiceItemData;
-import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.invoice.tree.AccountItemTree;
 import com.ning.billing.junction.BillingEvent;
 import com.ning.billing.junction.BillingEventSet;
 import com.ning.billing.junction.BillingModeType;
 import com.ning.billing.util.config.InvoiceConfig;
 
-import com.google.common.base.Objects;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.Lists;
 import com.google.inject.Inject;
 
-/**
- * Terminology for repair scenarii:
- * <p/>
- * - A 'repaired' item is an item that was generated and that needs to be repaired because the plan changed for that subscription on that period of time
- * - The 'repair' item is the item that cancels the (to be) repaired item; the repair item amount might not match the (to be) repaired item because:
- * * the (to be) repaired item was already adjusted so we will only repair what is left
- * * in case of partial repair we only repair the part that is not used
- * - The 'reparee' item is only present on disk-- in the existing item list -- in case of full repair; in that case it represents the portion of the item that should still
- * be invoiced for the plan of the repaired item. In case of partial repair it is merged with the repair item and does not exist except as a virtual item in the proposed list
- * <p/>
- * <p/>
- * <p/>
- * Example. We had a 20 subscription for a given period; we charged that amount and later discovered that only 3/4 of the time period were used after which the subscription was cancelled (immediate canellation)
- * <p/>
- * Full repair logic:
- * <p/>
- * Invoice 1:                   Invoice 2:
- * +20 (repaired)             +5 (reparee)
- * -20 (repair)
- * <p/>
- * Partial repair logic:
- * <p/>
- * Invoice 1:                   Invoice 2: (N/A)
- * +20 (repaired)
- * -15 (repair)
- * <p/>
- * The current version of the code uses partial repair logic but is able to deal with 'full repair' scenarii.
- */
-
 public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
     private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
@@ -120,14 +82,15 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
         validateTargetDate(targetDate);
 
-        final List<InvoiceItem> existingItems = new ArrayList<InvoiceItem>();
+        final AccountItemTree tree = new AccountItemTree(accountId);
+
         if (existingInvoices != null) {
             for (final Invoice invoice : existingInvoices) {
                 for (final InvoiceItem item : invoice.getInvoiceItems()) {
                     if (item.getSubscriptionId() == null || // Always include migration invoices, credits, external charges etc.
                         !events.getSubscriptionIdsWithAutoInvoiceOff()
                                .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
-                        existingItems.add(item);
+                        tree.addExistingItem(item);
                     }
                 }
             }
@@ -141,191 +104,11 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         // Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
         final List<InvoiceItem> proposedItems = generateInvoiceItems(invoiceId, accountId, events, adjustedTargetDate, targetCurrency);
 
-        // Remove repaired and repair items -- since they never change and can't be regenerated
-        removeRepairedAndRepairInvoiceItems(existingItems, proposedItems);
-
-        // Remove from both lists the items in common
-        removeMatchingInvoiceItems(existingItems, proposedItems);
-
-        // We don't want the Fixed items to be repaired -- as they are setup fees that should be paid
-        removeRemainingFixedItemsFromExisting(existingItems);
-
-        // Add repair items based on what is left in existing items
-        addRepairItems(existingItems, proposedItems);
-
-        // Finally add this new items on the new invoice
-        invoice.addInvoiceItems(proposedItems);
-
-        return proposedItems.size() != 0 ? invoice : null;
-    }
-
-    private void removeRemainingFixedItemsFromExisting(final List<InvoiceItem> existingItems) {
-        final Iterator<InvoiceItem> it = existingItems.iterator();
-        while (it.hasNext()) {
-            final InvoiceItem cur = it.next();
-            if (cur.getInvoiceItemType() == InvoiceItemType.FIXED) {
-                it.remove();
-            }
-        }
-    }
-
-    /**
-     * At this point either we have 0 existingItem left or those left need to be repaired
-     *
-     * @param existingItems the list of remaining existing items
-     * @param proposedItems the list of remaining proposed items
-     */
-    void addRepairItems(final List<InvoiceItem> existingItems, final List<InvoiceItem> proposedItems) {
-        for (final InvoiceItem existingItem : existingItems) {
-            if (existingItem.getInvoiceItemType() == InvoiceItemType.RECURRING ||
-                existingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
-                final BigDecimal existingAdjustedPositiveAmount = getAdjustedPositiveAmount(existingItems, existingItem.getId());
-                final BigDecimal amountNegated = existingItem.getAmount() == null ? null : existingItem.getAmount().subtract(existingAdjustedPositiveAmount).negate();
-                if (amountNegated != null && amountNegated.compareTo(BigDecimal.ZERO) < 0) {
-                    final RepairAdjInvoiceItem candidateRepairItem = new RepairAdjInvoiceItem(existingItem.getInvoiceId(), existingItem.getAccountId(), existingItem.getStartDate(), existingItem.getEndDate(), amountNegated, existingItem.getCurrency(), existingItem.getId());
-                    addRepairsForItem(existingItem, candidateRepairItem, proposedItems);
-                }
-            }
-        }
-    }
-
-    /**
-     * Add the repair item for the (yet to be) repairedItem. It will merge the candidateRepairItem with reparee item
-     *
-     * @param repairedItem        the item being repaired
-     * @param candidateRepairItem the repair item we would have if we were to repair the full period
-     * @param proposedItems       the list of proposed items
-     */
-    void addRepairsForItem(final InvoiceItem repairedItem, final RepairAdjInvoiceItem candidateRepairItem, final List<InvoiceItem> proposedItems) {
-
-        int nbTotalRepaireeDays = 0;
-
-        // totalRepareeItemAmount is negative and represents the portion left after we removed the adjustments for the total period for all the reparees combined
-        BigDecimal totalRepareeItemAmount = candidateRepairItem.getAmount();
-        final List<InvoiceItem> reparees = new ArrayList<InvoiceItem>();
-        for (final InvoiceItem cur : proposedItems) {
-            if (isRepareeItemForRepairedItem(repairedItem, cur)) {
-                nbTotalRepaireeDays += Days.daysBetween(cur.getStartDate(), cur.getEndDate()).getDays();
-                reparees.add(cur);
-                totalRepareeItemAmount = totalRepareeItemAmount.add(cur.getAmount());
-            }
-        }
-        int nbTotalRepairedDays = Days.daysBetween(candidateRepairItem.getStartDate(), candidateRepairItem.getEndDate()).getDays() - nbTotalRepaireeDays;
-
-        // If we repaired the full period there is no repairee item
-        if (reparees.size() == 0) {
-            proposedItems.add(candidateRepairItem);
-            return;
-        }
-
-        // Sort the reparees based on startDate in order to create the repair items -- based on the endDate (previous repairee) -> startDate (next reparee)
-        Collections.sort(reparees, new Comparator<InvoiceItem>() {
-            @Override
-            public int compare(final InvoiceItem o1, final InvoiceItem o2) {
-                return o1.getStartDate().compareTo(o2.getStartDate());
-            }
-        });
-
-        //Build the repaired
-        BigDecimal totalRepairItemAmount = BigDecimal.ZERO;
-        List<InvoiceItem> repairedItems = new ArrayList<InvoiceItem>();
-        InvoiceItem prevReparee = null;
-        final Iterator<InvoiceItem> it = reparees.iterator();
-        while (it.hasNext()) {
-            final InvoiceItem nextReparee = it.next();
-            if (prevReparee != null || nextReparee.getStartDate().compareTo(repairedItem.getStartDate()) > 0) {
-                final LocalDate repairedStartDate = (prevReparee == null) ? repairedItem.getStartDate() : prevReparee.getEndDate();
-                final LocalDate repairedEndDateDate = nextReparee.getStartDate();
-                // repairItemAmount is an approximation of the exact amount by simply prorating totalRepareeItemAmount in the repair period; we make sure last item is calculated based
-                // on what is left so the sum of all repairs amount is exactly correct
-                final BigDecimal repairItemAmount = (nextReparee.getEndDate().compareTo(candidateRepairItem.getEndDate()) != 0) ?
-                                                    InvoiceDateUtils.calculateProrationBetweenDates(repairedStartDate, repairedEndDateDate, nbTotalRepairedDays).multiply(totalRepareeItemAmount) :
-                                                    totalRepareeItemAmount.subtract(totalRepairItemAmount);
-                totalRepairItemAmount = totalRepairItemAmount.add(repairItemAmount);
-                final RepairAdjInvoiceItem repairItem = new RepairAdjInvoiceItem(candidateRepairItem.getInvoiceId(), candidateRepairItem.getAccountId(), repairedStartDate, repairedEndDateDate, repairItemAmount, candidateRepairItem.getCurrency(), repairedItem.getId());
-                repairedItems.add(repairItem);
-            }
-            prevReparee = nextReparee;
-        }
-
-        // In case we end up with a repair up to the service endDate we need to add this extra item-- this is the 'classic' case with one repairee/repair item
-        if (prevReparee.getEndDate().compareTo(candidateRepairItem.getEndDate()) != 0) {
-            final BigDecimal repairItemAmount = totalRepareeItemAmount.subtract(totalRepairItemAmount);
-            final RepairAdjInvoiceItem repairItem = new RepairAdjInvoiceItem(candidateRepairItem.getInvoiceId(), candidateRepairItem.getAccountId(), prevReparee.getEndDate(), candidateRepairItem.getEndDate(), repairItemAmount, candidateRepairItem.getCurrency(), repairedItem.getId());
-            repairedItems.add(repairItem);
-        }
-
-        // Finally remove all reparees from the proposed items and add all repaired items in the invoice
-        for (InvoiceItem reparee : reparees) {
-            proposedItems.remove(reparee);
-        }
-        proposedItems.addAll(repairedItems);
-    }
+        tree.mergeWithProposedItems(proposedItems);
+        final List<InvoiceItem> finalItems = tree.getResultingItemList();
+        invoice.addInvoiceItems(finalItems);
 
-    /**
-     * Check whether or not the invoiceItem passed is the reparee for that repaired invoice item
-     *
-     * @param repairedInvoiceItem the repaired invoice item
-     * @param invoiceItem         any invoice item to compare to
-     * @return true if invoiceItem is the reparee for that repaired invoice item
-     */
-    boolean isRepareeItemForRepairedItem(final InvoiceItem repairedInvoiceItem, final InvoiceItem invoiceItem) {
-        return !repairedInvoiceItem.getId().equals(invoiceItem.getId()) &&
-               repairedInvoiceItem.getInvoiceItemType().equals(invoiceItem.getInvoiceItemType()) &&
-               // We assume the items are correctly created, so that the subscription id check implicitly
-               // verifies that account id and bundle id matches
-               repairedInvoiceItem.getSubscriptionId().equals(invoiceItem.getSubscriptionId()) &&
-               // service period for reparee should be included in service period of repaired-- true for startDate and endDate
-               repairedInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) <= 0 &&
-               // Similarly, check the "portion used" is less than the original service end date. The check
-               // is strict, otherwise there wouldn't be anything to repair
-               ((repairedInvoiceItem.getEndDate() == null && invoiceItem.getEndDate() == null) ||
-                (repairedInvoiceItem.getEndDate() != null && invoiceItem.getEndDate() != null &&
-                 repairedInvoiceItem.getEndDate().compareTo(invoiceItem.getEndDate()) >= 0)) &&
-               // Finally, for the tricky part... In case of complete repairs, the new item will always meet all of the
-               // following conditions: same type, subscription, start date. Depending on the catalog configuration, the end
-               // date check could also match (e.g. repair from annual to monthly). For that scenario, we need to default
-               // to catalog checks (the rate check is a lame check for versioned catalogs).
-               Objects.firstNonNull(repairedInvoiceItem.getPlanName(), "").equals(Objects.firstNonNull(invoiceItem.getPlanName(), "")) &&
-               Objects.firstNonNull(repairedInvoiceItem.getPhaseName(), "").equals(Objects.firstNonNull(invoiceItem.getPhaseName(), "")) &&
-               Objects.firstNonNull(repairedInvoiceItem.getRate(), BigDecimal.ZERO).compareTo(Objects.firstNonNull(invoiceItem.getRate(), BigDecimal.ZERO)) == 0;
-    }
-
-    /**
-     * Check whether the repair invoice item overlaps the proposed item
-     *
-     * @param repairInvoiceItem      the repair invoice item associated to the repaired item for which we pass the subscriptionId
-     * @param repairedSubscriptionId the subscriptionId for which this repair points to
-     * @param invoiceItem            an invoice item to compare to
-     * @return
-     */
-    boolean isRepareeIncludedInRepair(final InvoiceItem repairInvoiceItem, final UUID repairedSubscriptionId, final InvoiceItem invoiceItem) {
-        return invoiceItem.getSubscriptionId().equals(repairedSubscriptionId) &&
-               repairInvoiceItem.getStartDate().compareTo(invoiceItem.getStartDate()) <= 0 &&
-               (invoiceItem.getEndDate() != null &&
-                repairInvoiceItem.getEndDate().compareTo(invoiceItem.getEndDate()) >= 0);
-    }
-
-    // We check to see if there are any adjustments that point to the item we are trying to repair
-    // If we did any CREDIT_ADJ or REFUND_ADJ, then we unfortunately we can't know what is the intent
-    // was as it applies to the full Invoice, so we ignore it. That might result in an extra positive CBA
-    // that would have to be corrected manually. This is the best we can do, and administrators should always
-    // use ITEM_ADJUSTMENT rather than CREDIT_ADJ or REFUND_ADJ when possible.
-    //
-    BigDecimal getAdjustedPositiveAmount(final List<InvoiceItem> existingItems, final UUID linkedItemId) {
-        BigDecimal totalAdjustedOnItem = BigDecimal.ZERO;
-        final Collection<InvoiceItem> invoiceItems = Collections2.filter(existingItems, new Predicate<InvoiceItem>() {
-            @Override
-            public boolean apply(final InvoiceItem item) {
-                return item.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ &&
-                       item.getLinkedItemId() != null && item.getLinkedItemId().equals(linkedItemId);
-            }
-        });
-
-        for (final InvoiceItem invoiceItem : invoiceItems) {
-            totalAdjustedOnItem = totalAdjustedOnItem.add(invoiceItem.getAmount());
-        }
-        return totalAdjustedOnItem.negate();
+        return finalItems.size() != 0 ? invoice : null;
     }
 
     private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
@@ -351,104 +134,6 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         return maxDate;
     }
 
-    /*
-     * Removes all matching items from both submitted collections
-     */
-    void removeMatchingInvoiceItems(final List<InvoiceItem> existingInvoiceItems,
-                                    final List<InvoiceItem> proposedItems) {
-        // We can't just use sets here as order matters (we want to keep duplicated in existingInvoiceItems)
-        final Iterator<InvoiceItem> proposedItemIterator = proposedItems.iterator();
-        while (proposedItemIterator.hasNext()) {
-            final InvoiceItem proposedItem = proposedItemIterator.next();
-
-            final Iterator<InvoiceItem> existingItemIterator = existingInvoiceItems.iterator();
-            while (existingItemIterator.hasNext()) {
-                final InvoiceItem existingItem = existingItemIterator.next();
-                if (existingItem.matches(proposedItem)) {
-                    existingItemIterator.remove();
-                    proposedItemIterator.remove();
-                    break;
-                }
-            }
-        }
-    }
-
-    /**
-     * Remove from the existing item list all repaired items-- both repaired and repair
-     * If this is a partial repair, we also need to find the reparee from the proposed list
-     * and remove it.
-     *
-     * @param existingItems input list of existing items
-     * @param proposedItems input list of proposed item
-     */
-    void removeRepairedAndRepairInvoiceItems(final List<InvoiceItem> existingItems, final List<InvoiceItem> proposedItems) {
-
-        final List<UUID> itemsToRemove = new ArrayList<UUID>();
-        List<InvoiceItem> itemsToAdd = Lists.newLinkedList();
-
-        for (final InvoiceItem item : existingItems) {
-            if (item.getInvoiceItemType() == InvoiceItemType.REPAIR_ADJ) {
-
-                // Assign for terminology purpose
-                final InvoiceItem repairItem = item;
-                final InvoiceItem repairedItem = getRepairedInvoiceItem(repairItem.getLinkedItemId(), existingItems);
-                // Always look for reparees; if this is a full repair there may not be any reparee to remove, but
-                // if this is a partial repair with an additional invoice item adjustment, this is seen as a full repair
-                // and yet there is a reparee to remove
-
-                final List<InvoiceItem> removedReparees = removeProposedRepareesForPartialrepair(repairedItem, repairItem, proposedItems);
-                itemsToAdd.addAll((computeNonRepairedItems(repairedItem, repairItem, removedReparees)));
-                itemsToRemove.add(repairItem.getId());
-                itemsToRemove.add(repairItem.getLinkedItemId());
-            }
-        }
-        final Iterator<InvoiceItem> iterator = existingItems.iterator();
-        while (iterator.hasNext()) {
-            final InvoiceItem item = iterator.next();
-            if (itemsToRemove.contains(item.getId())) {
-                iterator.remove();
-            }
-        }
-        existingItems.addAll(itemsToAdd);
-    }
-
-
-    /**
-     * Removes the reparee from proposed list of items if it exists.
-     *
-     * @param repairedItem  the repaired item
-     * @param proposedItems the list of existing items
-     */
-    protected List<InvoiceItem> removeProposedRepareesForPartialrepair(final InvoiceItem repairedItem, final InvoiceItem repairItem, final List<InvoiceItem> proposedItems) {
-
-        List<InvoiceItem> removedReparees = Collections.emptyList();
-        final Iterator<InvoiceItem> it = proposedItems.iterator();
-        while (it.hasNext()) {
-            final InvoiceItem cur = it.next();
-            final UUID repairedSubscriptionId = repairedItem.getSubscriptionId();
-            // We remove the item if we already billed for it, that is:
-            // - First we check if the current item is a reparee for that repaired
-            // - Second we check whether that reparee is outside of the repair period and therefore has already been accounted for. If not we keep it.
-            if (isRepareeItemForRepairedItem(repairedItem, cur) && !isRepareeIncludedInRepair(repairItem, repairedSubscriptionId, cur)) {
-                if (removedReparees.size() == 0) {
-                    removedReparees = Lists.newLinkedList();
-                }
-                removedReparees.add(cur);
-                it.remove();
-            }
-        }
-        return removedReparees;
-    }
-
-    private InvoiceItem getRepairedInvoiceItem(final UUID repairedInvoiceItemId, final List<InvoiceItem> existingItems) {
-        for (InvoiceItem cur : existingItems) {
-            if (cur.getId().equals(repairedInvoiceItemId)) {
-                return cur;
-            }
-        }
-        throw new IllegalStateException("Cannot find repaired invoice item " + repairedInvoiceItemId);
-    }
-
     private List<InvoiceItem> generateInvoiceItems(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
                                                    final LocalDate targetDate, final Currency currency) throws InvoiceApiException {
         final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
@@ -542,71 +227,6 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         return items;
     }
 
-    /**
-     *
-     * It compares the full period of the repairedItem with the list of repairees and repair
-     *
-     * @param repairedItem     the repair item
-     * @param repairItem       (one of) the repair item pointing to the repairedItem
-     * @param removedReparees  the reparees from propsoed list that were found matching that repairedItem
-     * @return
-     */
-    List<InvoiceItem> computeNonRepairedItems(final InvoiceItem repairedItem, final InvoiceItem repairItem, final List<InvoiceItem> removedReparees) {
-
-        final List<InvoiceItem> result = new LinkedList<InvoiceItem>();
-        if (removedReparees.size() == 0 || repairedItem.getInvoiceItemType() != InvoiceItemType.RECURRING) {
-            return result;
-        }
-
-        final List<InvoiceItem> repairAndReparees = new ArrayList<InvoiceItem>(removedReparees);
-        repairAndReparees.add(repairItem);
-
-        Collections.sort(repairAndReparees, new Comparator<InvoiceItem>() {
-            @Override
-            public int compare(final InvoiceItem o1, final InvoiceItem o2) {
-                return o1.getStartDate().compareTo(o2.getStartDate());
-            }
-        });
-
-        int nbTotalRepairedDays = Days.daysBetween(repairedItem.getStartDate(), repairedItem.getEndDate()).getDays();
-
-        LocalDate prevEnd = null;
-        final LocalDate startDate = repairedItem.getStartDate();
-        for (InvoiceItem cur : repairAndReparees) {
-            if (prevEnd == null) {
-                if (cur.getStartDate().compareTo(startDate) > 0) {
-                    result.add(createRecurringInvoiceItemForRepair(repairedItem.getStartDate(), cur.getStartDate(), repairedItem, nbTotalRepairedDays));
-                }
-            } else {
-                if (prevEnd.compareTo(cur.getStartDate()) < 0) {
-                    result.add(createRecurringInvoiceItemForRepair(prevEnd, cur.getStartDate(), repairedItem, nbTotalRepairedDays));
-                }
-            }
-            prevEnd = cur.getEndDate();
-        }
-
-        if (prevEnd.compareTo(repairedItem.getEndDate()) < 0) {
-            result.add(createRecurringInvoiceItemForRepair(prevEnd, repairedItem.getEndDate(), repairedItem, nbTotalRepairedDays));
-        }
-        return result;
-    }
-
-    private InvoiceItem createRecurringInvoiceItemForRepair(final LocalDate startDate, final LocalDate endDate, final InvoiceItem repairedItem, final int nbTotalRepairedDays) {
-        final BigDecimal amount = InvoiceDateUtils.calculateProrationBetweenDates(startDate, endDate, nbTotalRepairedDays).multiply(repairedItem.getRate()).setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE);
-        return new RecurringInvoiceItem(repairedItem.getInvoiceId(),
-                                        repairedItem.getAccountId(),
-                                        repairedItem.getBundleId(),
-                                        repairedItem.getSubscriptionId(),
-                                        repairedItem.getPlanName(),
-                                        repairedItem.getPhaseName(),
-                                        startDate,
-                                        endDate,
-                                        amount,
-                                        repairedItem.getRate(),
-                                        repairedItem.getCurrency());
-
-    }
-
     private BillingMode instantiateBillingMode(final BillingModeType billingMode) {
         switch (billingMode) {
             case IN_ADVANCE:
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
new file mode 100644
index 0000000..49a8c2d
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+/**
+ * Tree of invoice items for a given account.
+ * <p/>
+ * <p>It contains a map of <tt>SubscriptionItemTree</tt> and the logic is executed independently for all items
+ * associated to a given subscription. That also means that invoice item adjustment which cross subscriptions
+ * can't be correctly handled when they compete with other forms of adjustments.
+ * <p/>
+ * <p>The class is not thread safe, there is no such use case today, and there is a lifecyle to respect:
+ * <ul>
+ * <li>Add existing invoice items
+ * <li>Build the tree,
+ * <li>Merge the proposed list
+ * <li>Retrieves final list
+ * <ul/>
+ */
+public class AccountItemTree {
+
+    private final UUID accountId;
+    private final Map<UUID, SubscriptionItemTree> subscriptionItemTree;
+    private final List<InvoiceItem> allExistingItems;
+    private List<InvoiceItem> pendingItemAdj;
+
+    private boolean isBuilt;
+
+    public AccountItemTree(final UUID accountId) {
+        this.accountId = accountId;
+        this.subscriptionItemTree = new HashMap<UUID, SubscriptionItemTree>();
+        this.isBuilt = false;
+        this.allExistingItems = new LinkedList<InvoiceItem>();
+        this.pendingItemAdj = new LinkedList<InvoiceItem>();
+    }
+
+    /**
+     * build the subscription trees after they have been populated with existing items on disk
+     */
+    public void build() {
+        Preconditions.checkState(!isBuilt);
+
+        if (pendingItemAdj.size() > 0) {
+            for (InvoiceItem item : pendingItemAdj) {
+                addExistingItem(item, true);
+            }
+            pendingItemAdj.clear();
+        }
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            tree.build();
+        }
+        isBuilt = true;
+    }
+
+    /**
+     * Populate tree from existing items on disk
+     *
+     * @param existingItem an item read on disk
+     */
+    public void addExistingItem(final InvoiceItem existingItem) {
+        addExistingItem(existingItem, false);
+    }
+
+    private void addExistingItem(final InvoiceItem existingItem, boolean failOnMissingSubscription) {
+
+        Preconditions.checkState(!isBuilt);
+        switch (existingItem.getInvoiceItemType()) {
+            case EXTERNAL_CHARGE:
+            case CBA_ADJ:
+            case CREDIT_ADJ:
+            case REFUND_ADJ:
+                return;
+
+            case RECURRING:
+            case REPAIR_ADJ:
+            case FIXED:
+            case ITEM_ADJ:
+                break;
+
+            default:
+                Preconditions.checkState(false, "Unknown invoice item type " + existingItem.getInvoiceItemType());
+
+        }
+
+        allExistingItems.add(existingItem);
+
+        final UUID subscriptionId = getSubscriptionId(existingItem, allExistingItems);
+        Preconditions.checkState(subscriptionId != null || !failOnMissingSubscription);
+
+        if (subscriptionId == null && existingItem.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ) {
+            pendingItemAdj.add(existingItem);
+            return;
+        }
+
+        if (!subscriptionItemTree.containsKey(subscriptionId)) {
+            subscriptionItemTree.put(subscriptionId, new SubscriptionItemTree(subscriptionId));
+        }
+        final SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+        tree.addItem(existingItem);
+    }
+
+    /**
+     * Rebuild the new tree by merging current on-disk existing view with new proposed list.
+     *
+     * @param proposedItems list of proposed item that should be merged with current existing view
+     */
+    public void mergeWithProposedItems(final List<InvoiceItem> proposedItems) {
+
+        build();
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            tree.flatten(true);
+        }
+
+        for (InvoiceItem item : proposedItems) {
+            final UUID subscriptionId = getSubscriptionId(item, null);
+            SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+            if (tree == null) {
+                tree = new SubscriptionItemTree(subscriptionId);
+                subscriptionItemTree.put(subscriptionId, tree);
+            }
+            tree.mergeProposedItem(item);
+        }
+
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            tree.buildForMerge();
+        }
+    }
+
+    /**
+     * @return the resulting list of items that should be written to disk
+     */
+    public List<InvoiceItem> getResultingItemList() {
+        final List<InvoiceItem> result = new ArrayList<InvoiceItem>();
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            final List<InvoiceItem> simplifiedView = tree.getView();
+            if (simplifiedView.size() > 0) {
+                result.addAll(simplifiedView);
+            }
+        }
+        return result;
+    }
+
+    public UUID getAccountId() {
+        return accountId;
+    }
+
+    private UUID getSubscriptionId(final InvoiceItem item, final List<InvoiceItem> allItems) {
+        if (item.getInvoiceItemType() == InvoiceItemType.RECURRING ||
+            item.getInvoiceItemType() == InvoiceItemType.FIXED) {
+            return item.getSubscriptionId();
+        } else {
+            final InvoiceItem linkedItem = Iterables.tryFind(allItems, new Predicate<InvoiceItem>() {
+                @Override
+                public boolean apply(final InvoiceItem input) {
+                    return item.getLinkedItemId().equals(input.getId());
+                }
+            }).orNull();
+            return linkedItem != null ? linkedItem.getSubscriptionId() : null;
+        }
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java b/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java
new file mode 100644
index 0000000..e820459
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.generator.InvoiceDateUtils;
+import com.ning.billing.invoice.model.InvoicingConfiguration;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+/**
+ * An generic invoice item that contains all pertinent fields regarding of its InvoiceItemType.
+ * <p>
+ * It contains an action that determines what to do when building the tree (whether in normal or merge mode). It also
+ * keeps track of current adjusted and repair amount so subsequent repair can be limited to what is left.
+ */
+public class Item {
+
+    private static final int ROUNDING_MODE = InvoicingConfiguration.getRoundingMode();
+    private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();
+
+    private final UUID id;
+    private final UUID accountId;
+    private final UUID bundleId;
+    private final UUID subscriptionId;
+    private final UUID invoiceId;
+    private final String planName;
+    private final String phaseName;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+    private final BigDecimal amount;
+    private final BigDecimal rate;
+    private final Currency currency;
+    private final DateTime createdDate;
+    private final UUID linkedId;
+
+    private BigDecimal currentRepairedAmount;
+    private BigDecimal adjustedAmount;
+
+    private final ItemAction action;
+
+    public enum ItemAction {
+        ADD,
+        CANCEL
+    }
+
+    public Item(final Item item, final ItemAction action) {
+        this.id = item.id;
+        this.accountId = item.accountId;
+        this.bundleId = item.bundleId;
+        this.subscriptionId = item.subscriptionId;
+        this.invoiceId = item.invoiceId;
+        this.planName = item.planName;
+        this.phaseName = item.phaseName;
+        this.startDate = item.startDate;
+        this.endDate = item.endDate;
+        this.amount = item.amount;
+        this.rate = item.rate;
+        this.currency = item.currency;
+        // In merge mode, the reverse item needs to correctly point to itself (repair of original item)
+        this.linkedId = action == ItemAction.ADD ? item.linkedId : this.id;
+        this.createdDate = item.createdDate;
+        this.currentRepairedAmount = item.currentRepairedAmount;
+        this.adjustedAmount = item.adjustedAmount;
+
+        this.action = action;
+    }
+
+    public Item(final InvoiceItem item, final ItemAction action) {
+        this.id = item.getId();
+        this.accountId = item.getAccountId();
+        this.bundleId = item.getBundleId();
+        this.subscriptionId = item.getSubscriptionId();
+        this.invoiceId = item.getInvoiceId();
+        this.planName = item.getPlanName();
+        this.phaseName = item.getPhaseName();
+        this.startDate = item.getStartDate();
+        this.endDate = item.getEndDate();
+        this.amount = item.getAmount().abs();
+        this.rate = item.getRate();
+        this.currency = item.getCurrency();
+        this.linkedId = item.getLinkedItemId();
+        this.createdDate = item.getCreatedDate();
+        this.action = action;
+
+        this.currentRepairedAmount = BigDecimal.ZERO;
+        this.adjustedAmount = BigDecimal.ZERO;
+    }
+
+    public InvoiceItem toInvoiceItem() {
+        return toProratedInvoiceItem(startDate, endDate);
+    }
+
+    public InvoiceItem toProratedInvoiceItem(final LocalDate newStartDate, final LocalDate newEndDate) {
+
+        int nbTotalDays = Days.daysBetween(startDate, endDate).getDays();
+        final boolean prorated = !(newStartDate.compareTo(startDate) == 0 && newEndDate.compareTo(endDate) == 0);
+
+        // Pro-ration is built by using the startDate, endDate and amount of this item instead of using the rate and a potential full period.
+        final BigDecimal positiveAmount = prorated ?
+                                          InvoiceDateUtils.calculateProrationBetweenDates(newStartDate, newEndDate, nbTotalDays)
+                                                          .multiply(amount).setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE) :
+                                          amount;
+
+        if (action == ItemAction.ADD) {
+            return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, positiveAmount, rate, currency);
+        } else {
+            // We first compute the maximum amount after adjustment and that sets the amount limit of how much can be repaired.
+            final BigDecimal maxAvailableAmountAfterAdj = amount.subtract(adjustedAmount);
+            final BigDecimal maxAvailableAmountForRepair = maxAvailableAmountAfterAdj.subtract(currentRepairedAmount);
+            final BigDecimal positiveAmountForRepair = positiveAmount.compareTo(maxAvailableAmountForRepair) <= 0 ? positiveAmount : maxAvailableAmountForRepair;
+            return positiveAmountForRepair.compareTo(BigDecimal.ZERO) > 0 ? new RepairAdjInvoiceItem(invoiceId, accountId, newStartDate, newEndDate, positiveAmountForRepair.negate(), currency, linkedId) : null;
+        }
+    }
+
+    public void incrementAdjustedAmount(final BigDecimal increment) {
+        Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+        adjustedAmount = adjustedAmount.add(increment);
+    }
+
+    public void incrementCurrentRepairedAmount(final BigDecimal increment) {
+        Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+        currentRepairedAmount = currentRepairedAmount.add(increment);
+    }
+
+    public ItemAction getAction() {
+        return action;
+    }
+
+    public UUID getLinkedId() {
+        return linkedId;
+    }
+
+    public LocalDate getEndDate() {
+        return endDate;
+    }
+
+    public LocalDate getStartDate() {
+        return startDate;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public UUID getId() {
+        return id;
+    }
+
+    public Currency getCurrency() {
+        return currency;
+    }
+
+    /**
+     * Compare two items to check whether there are the same kind; that is whether or not they build for the same product/plan.
+     *
+     * @param other item to compare with
+     * @return
+     */
+    public boolean isSameKind(final Item other) {
+
+        final InvoiceItem otherItem = other.toInvoiceItem();
+
+        return !id.equals(otherItem.getId()) &&
+               // Finally, for the tricky part... In case of complete repairs, the new invoiceItem will always meet all of the
+               // following conditions: same type, subscription, start date. Depending on the catalog configuration, the end
+               // date check could also match (e.g. repair from annual to monthly). For that scenario, we need to default
+               // to catalog checks (the rate check is a lame check for versioned catalogs).
+               Objects.firstNonNull(planName, "").equals(Objects.firstNonNull(otherItem.getPlanName(), "")) &&
+               Objects.firstNonNull(phaseName, "").equals(Objects.firstNonNull(otherItem.getPhaseName(), "")) &&
+               Objects.firstNonNull(rate, BigDecimal.ZERO).compareTo(Objects.firstNonNull(otherItem.getRate(), BigDecimal.ZERO)) == 0;
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
new file mode 100644
index 0000000..2ead948
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.invoice.tree.Item.ItemAction;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+/**
+ * Keeps track of all the items existing on a specified interval.
+ */
+public class ItemsInterval {
+
+    private final NodeInterval interval;
+    private LinkedList<Item> items;
+
+    public ItemsInterval(final NodeInterval interval) {
+        this(interval, null);
+    }
+
+    public ItemsInterval(final NodeInterval interval, final Item initialItem) {
+        this.interval = interval;
+        this.items = Lists.newLinkedList();
+        if (initialItem != null) {
+            items.add(initialItem);
+        }
+    }
+
+    public boolean containsItem(final UUID targetId) {
+        return Iterables.tryFind(items, new Predicate<Item>() {
+            @Override
+            public boolean apply(final Item input) {
+                return input.getId().equals(targetId);
+            }
+        }).orNull() != null;
+    }
+
+    public void setAdjustment(final BigDecimal amount, final UUID targetId) {
+        final Item item = Iterables.tryFind(items, new Predicate<Item>() {
+            @Override
+            public boolean apply(final Item input) {
+                return input.getId().equals(targetId);
+            }
+        }).get();
+        item.incrementAdjustedAmount(amount);
+    }
+
+    public List<Item> getItems() {
+        return items;
+    }
+
+    public void buildForMissingInterval(final LocalDate startDate, final LocalDate endDate, final List<Item> output, final boolean addRepair) {
+        final Item item = createNewItem(startDate, endDate, addRepair);
+        if (item != null) {
+            output.add(item);
+        }
+    }
+
+    /**
+     * Determines what is left based on the mergeMode and the action for each item.
+     *
+     * @param output
+     * @param mergeMode
+     */
+    public void buildFromItems(final List<Item> output, final boolean mergeMode) {
+        final Item item  = getResultingItem(mergeMode);
+        if (item != null) {
+            output.add(item);
+        }
+    }
+
+    private Item getResultingItem(final boolean mergeMode) {
+        return mergeMode ? getResulting_CANCEL_Item() : getResulting_ADD_Item();
+    }
+
+    private Item getResulting_CANCEL_Item() {
+        Preconditions.checkState(items.size() == 0 || items.size() == 1);
+        return Iterables.tryFind(items, new Predicate<Item>() {
+            @Override
+            public boolean apply(final Item input) {
+                return input.getAction() == ItemAction.CANCEL;
+            }
+        }).orNull();
+    }
+
+
+    private Item getResulting_ADD_Item() {
+
+        final Set<UUID> repairedIds = new HashSet<UUID>();
+        final ListIterator<Item> it = items.listIterator(items.size());
+
+        while (it.hasPrevious()) {
+            final Item cur = it.previous();
+            switch (cur.getAction()) {
+                case ADD:
+                    // If we found a CANCEL item pointing to that item then don't return it as it was repair (full repair scenario)
+                    if (!repairedIds.contains(cur.getId())) {
+                        return cur;
+                    }
+                    break;
+
+                case CANCEL:
+                    // In all cases populate the set with the id of target item being repaired
+                    if (cur.getLinkedId() != null) {
+                        repairedIds.add(cur.getLinkedId());
+                    }
+                    break;
+            }
+        }
+        return null;
+    }
+
+
+    // Just ensure that ADD items precedes CANCEL items
+    public void insertSortedItem(final Item item) {
+        items.add(item);
+        Collections.sort(items, new Comparator<Item>() {
+            @Override
+            public int compare(final Item o1, final Item o2) {
+                if (o1.getAction() == ItemAction.ADD && o2.getAction() == ItemAction.CANCEL) {
+                    return -1;
+                } else if (o1.getAction() == ItemAction.CANCEL && o2.getAction() == ItemAction.ADD) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        });
+    }
+
+    public void cancelItems(final Item item) {
+        Preconditions.checkState(item.getAction() == ItemAction.ADD);
+        Preconditions.checkState(items.size() == 1);
+        Preconditions.checkState(items.get(0).getAction() == ItemAction.CANCEL);
+        items.clear();
+    }
+
+    /**
+     * Creates a new item.
+     * <p/>
+     * <ul>
+     * <li>In normal mode, we only consider ADD items. This happens when for instance an existing item was partially repaired
+     * and there is a need to create a new item which represents the part left -- that was not repaired.
+     * <li>In mergeMode, we allow to create new items that are the missing repaired items (CANCEL).
+     * </ul>
+     *
+     * @param startDate start date of the new item to create
+     * @param endDate   end date of the new item to create
+     * @param mergeMode mode to consider.
+     * @return
+     */
+    private Item createNewItem(LocalDate startDate, LocalDate endDate, final boolean mergeMode) {
+
+        final Item item  = getResultingItem(mergeMode);
+        if (item == null) {
+            return null;
+        }
+
+        final Item result = new Item(item.toProratedInvoiceItem(startDate, endDate), item.getAction());
+        if (item.getAction() == ItemAction.CANCEL && result != null) {
+            item.incrementCurrentRepairedAmount(result.getAmount());
+        }
+        return result;
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
new file mode 100644
index 0000000..3762783
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.util.jackson.ObjectMapper;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.google.common.base.Preconditions;
+
+public class ItemsNodeInterval extends NodeInterval {
+
+    private ItemsInterval items;
+
+    public ItemsNodeInterval() {
+        this.items = new ItemsInterval(this);
+    }
+
+    public ItemsNodeInterval(final NodeInterval parent, final Item item) {
+        super(parent, item.getStartDate(), item.getEndDate());
+        this.items = new ItemsInterval(this, item);
+    }
+
+    @JsonIgnore
+    public ItemsInterval getItemsInterval() {
+        return items;
+    }
+
+    public List<Item> getItems() {
+        return items.getItems();
+    }
+
+    /**
+     * <p/>
+     * There is no limit in the depth of the tree,
+     * and the build strategy is to first consider the lowest child for a given period
+     * and go up the tree adding missing interval if needed. For e.g, one of the possible scenario:
+     * <pre>
+     * D1                                                  D2
+     * |---------------------------------------------------|   Plan P1
+     *       D1'             D2'
+     *       |---------------|/////////////////////////////|   Plan P2, REPAIR
+     *
+     *  In that case we will generate:
+     *  [D1,D1') on Plan P1; [D1', D2') on Plan P2, and [D2', D2) repair item
+     *
+     * <pre/>
+     *
+     * In the merge mode, the strategy is different, the tree is fairly shallow
+     * and the goal is to generate the repair items; @see addProposedItem
+     *
+     * @param output result list of built items
+     */
+    public void buildForExistingItems(final List<Item> output) {
+        build(new BuildNodeCallback() {
+            @Override
+            public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+                items.buildForMissingInterval(startDate, endDate, output, false);
+            }
+
+            @Override
+            public void onLastNode(final NodeInterval curNode) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+                items.buildFromItems(output, false);
+            }
+        });
+    }
+
+    /**
+     * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
+     * <p/>
+     * That means that if we were to not merge any new proposed items, we would end up with only those reversed existing
+     * items, and they would all end up repaired-- which is what we want.
+     * <p/>
+     * However, if there are new proposed items, then we look to see if they are children one our existing reverse items
+     * so that we can generate the repair pieces missing. For e.g, below is one scenario among so many:
+     * <p/>
+     * <pre>
+     * D1                                                  D2
+     * |---------------------------------------------------| (existing reversed (CANCEL) item
+     *       D1'             D2'
+     *       |---------------| (proposed same plan)
+     * </pre>
+     * In that case we want to generated a repair for [D1, D1') and [D2',D2)
+     * <p/>
+     * Note that this tree is never very deep, only 3 levels max, with exiting at the first level
+     * and proposed that are the for the exact same plan but for different dates below.
+     *
+     * @param output result list of built items
+     */
+    public void mergeExistingAndProposed(final List<Item> output) {
+        build(new BuildNodeCallback() {
+            @Override
+            public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+                items.buildForMissingInterval(startDate, endDate, output, true);
+            }
+
+            @Override
+            public void onLastNode(final NodeInterval curNode) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+                items.buildFromItems(output, true);
+            }
+        });
+    }
+
+    /**
+     * Add existing item into the tree.
+     *
+     * @param newNode an existing item
+     */
+    public boolean addExistingItem(final ItemsNodeInterval newNode) {
+
+        return addNode(newNode, new AddNodeCallback() {
+            @Override
+            public boolean onExistingNode(final NodeInterval existingNode) {
+                if (!existingNode.isRoot() && newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0) {
+                    final Item item = newNode.getItems().get(0);
+                    final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
+                    existingOrNewNodeItems.insertSortedItem(item);
+                }
+                // There is no new node added but instead we just populated the list of items for the already existing node.
+                return false;
+            }
+
+            @Override
+            public boolean shouldInsertNode(final NodeInterval insertionNode) {
+                // Always want to insert node in the tree when we find the right place.
+                return true;
+            }
+        });
+    }
+
+    /**
+     * Add proposed item into the (flattened and reversed) tree.
+     *
+     * @param newNode a new proposed item
+     * @return true if the item was merged and will trigger a repair or false if the proposed item should be kept as such
+     *         and no repair generated.
+     */
+    public boolean addProposedItem(final ItemsNodeInterval newNode) {
+
+        return addNode(newNode, new AddNodeCallback() {
+            @Override
+            public boolean onExistingNode(final NodeInterval existingNode) {
+                if (!shouldInsertNode(existingNode)) {
+                    return false;
+                }
+
+                Preconditions.checkState(newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0);
+                final Item item = newNode.getItems().get(0);
+                final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
+                existingOrNewNodeItems.cancelItems(item);
+                // In the merge logic, whether we really insert the node or find an existing node on which to insert items should be seen
+                // as an insertion (so as to avoid keeping that proposed item, see how return value of addProposedItem is used)
+                return true;
+            }
+
+            @Override
+            public boolean shouldInsertNode(final NodeInterval insertionNode) {
+                // The root level is solely for the reversed existing items. If there is a new node that does not fit below the level
+                // of reversed existing items, we want to return false and keep it outside of the tree. It should be 'kept as such'.
+                if (insertionNode.isRoot()) {
+                    return false;
+                }
+
+                final ItemsInterval insertionNodeItems = ((ItemsNodeInterval) insertionNode).getItemsInterval();
+                Preconditions.checkState(insertionNodeItems.getItems().size() == 1, "Expected existing node to have only one item");
+                final Item insertionNodeItem = insertionNodeItems.getItems().get(0);
+                final Item newNodeItem = newNode.getItems().get(0);
+
+                // If we receive a new proposed that is the same kind as the reversed existing we want to insert it to generate
+                // a piece of repair
+                if (insertionNodeItem.isSameKind(newNodeItem)) {
+                    return true;
+                    // If not, then keep the proposed outside of the tree.
+                } else {
+                    return false;
+                }
+            }
+        });
+    }
+
+    /**
+     * Add the adjustment amount on the item specified by the targetId.
+     *
+     * @param adjustementDate date of the adjustment
+     * @param amount          amount of the adjustment
+     * @param targetId        item that has been adjusted
+     */
+    public void addAdjustment(final LocalDate adjustementDate, final BigDecimal amount, final UUID targetId) {
+        // TODO we should really be using findNode(adjustementDate, new SearchCallback() instead but wrong dates in test
+        // creates test panic.
+        final NodeInterval node = findNode(new SearchCallback() {
+            @Override
+            public boolean isMatch(final NodeInterval curNode) {
+                return ((ItemsNodeInterval) curNode).getItemsInterval().containsItem(targetId);
+            }
+        });
+        Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
+        ((ItemsNodeInterval) node).setAdjustment(amount.negate(), targetId);
+    }
+
+    public void jsonSerializeTree(final ObjectMapper mapper, final OutputStream output) throws IOException {
+
+        final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+        generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+        walkTree(new WalkCallback() {
+
+            private int curDepth = 0;
+
+            @Override
+            public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+                final ItemsNodeInterval node = (ItemsNodeInterval) curNode;
+                if (node.isRoot()) {
+                    return;
+                }
+
+                try {
+                    if (curDepth < depth) {
+                        generator.writeStartArray();
+                        curDepth = depth;
+                    } else if (curDepth > depth) {
+                        generator.writeEndArray();
+                        curDepth = depth;
+                    }
+                    generator.writeObject(node);
+                } catch (IOException e) {
+                    throw new RuntimeException("Failed to deserialize tree", e);
+                }
+            }
+        });
+        generator.close();
+    }
+
+    protected void setAdjustment(final BigDecimal amount, final UUID linkedId) {
+        items.setAdjustment(amount, linkedId);
+    }
+
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
new file mode 100644
index 0000000..b1d1483
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.util.List;
+
+import org.joda.time.LocalDate;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+public class NodeInterval {
+
+    protected NodeInterval parent;
+    protected NodeInterval leftChild;
+    protected NodeInterval rightSibling;
+
+    protected LocalDate start;
+    protected LocalDate end;
+
+    public NodeInterval() {
+        this(null, null, null);
+    }
+
+    public NodeInterval(final NodeInterval parent, final LocalDate startDate, final LocalDate endDate) {
+        this.start = startDate;
+        this.end = endDate;
+        this.parent = parent;
+        this.leftChild = null;
+        this.rightSibling = null;
+    }
+
+    /**
+     * Build the tree by calling the callback on the last node in the tree or remaining part with no children.
+     *
+     * @param callback the callback which perform the build logic.
+     */
+    public void build(final BuildNodeCallback callback) {
+
+        Preconditions.checkNotNull(callback);
+
+        if (leftChild == null) {
+            callback.onLastNode(this);
+            return;
+        }
+
+        LocalDate curDate = start;
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            if (curChild.getStart().compareTo(curDate) > 0) {
+                callback.onMissingInterval(this, curDate, curChild.getStart());
+            }
+            curChild.build(callback);
+            curDate = curChild.getEnd();
+            curChild = curChild.getRightSibling();
+        }
+
+        // Finally if there is a hole at the end, we build the missing piece from ourself
+        if (curDate.compareTo(end) < 0) {
+            callback.onMissingInterval(this, curDate, end);
+        }
+    }
+
+    /**
+     * Add a new node in the tree.
+     *
+     * @param newNode  the node to be added
+     * @param callback the callback that will allow to specify insertion and return behavior.
+     * @return true if node was inserted. Note that this is driven by the callback, this method is generic
+     *         and specific behavior can be tuned through specific callbacks.
+     */
+    public boolean addNode(final NodeInterval newNode, final AddNodeCallback callback) {
+
+        Preconditions.checkNotNull(newNode);
+        Preconditions.checkNotNull(callback);
+
+        if (!isRoot() && newNode.getStart().compareTo(start) == 0 && newNode.getEnd().compareTo(end) == 0) {
+            return callback.onExistingNode(this);
+        }
+
+        computeRootInterval(newNode);
+
+        newNode.parent = this;
+        if (leftChild == null) {
+            if (callback.shouldInsertNode(this)) {
+                leftChild = newNode;
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        NodeInterval prevChild = null;
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            if (curChild.isItemContained(newNode)) {
+                return curChild.addNode(newNode, callback);
+            }
+
+            if (curChild.isItemOverlap(newNode)) {
+                if (callback.shouldInsertNode(this)) {
+                    rebalance(newNode);
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+
+            if (newNode.getStart().compareTo(curChild.getStart()) < 0) {
+                if (callback.shouldInsertNode(this)) {
+                    newNode.rightSibling = curChild;
+                    if (prevChild == null) {
+                        leftChild = newNode;
+                    } else {
+                        prevChild.rightSibling = newNode;
+                    }
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+            prevChild = curChild;
+            curChild = curChild.rightSibling;
+        }
+
+        if (callback.shouldInsertNode(this)) {
+            prevChild.rightSibling = newNode;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Return the first node satisfying the date and match callback.
+     *
+     * @param targetDate target date for possible match nodes whose interval comprises that date
+     * @param callback   custom logic to decide if a given node is a match
+     * @return the found node or null if there is nothing.
+     */
+    public NodeInterval findNode(final LocalDate targetDate, final SearchCallback callback) {
+
+        Preconditions.checkNotNull(callback);
+        Preconditions.checkNotNull(targetDate);
+
+        if (targetDate.compareTo(getStart()) < 0 || targetDate.compareTo(getEnd()) > 0) {
+            return null;
+        }
+
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            if (curChild.getStart().compareTo(targetDate) <= 0 && curChild.getEnd().compareTo(targetDate) >= 0) {
+                if (callback.isMatch(curChild)) {
+                    return curChild;
+                }
+                NodeInterval result = curChild.findNode(targetDate, callback);
+                if (result != null) {
+                    return result;
+                }
+            }
+            curChild = curChild.getRightSibling();
+        }
+        return null;
+    }
+
+    /**
+     * Return the first node satisfying the date and match callback.
+     *
+     * @param callback custom logic to decide if a given node is a match
+     * @return the found node or null if there is nothing.
+     */
+    public NodeInterval findNode(final SearchCallback callback) {
+
+        Preconditions.checkNotNull(callback);
+        if (callback.isMatch(this)) {
+            return this;
+        }
+
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            final NodeInterval result = curChild.findNode(callback);
+            if (result != null) {
+                return result;
+            }
+            curChild = curChild.getRightSibling();
+        }
+        return null;
+    }
+
+    /**
+     * Walk the tree (depth first search) and invoke callback for each node.
+     *
+     * @param callback
+     */
+    public void walkTree(WalkCallback callback) {
+        Preconditions.checkNotNull(callback);
+        walkTreeWithDepth(callback, 0);
+    }
+
+    private void walkTreeWithDepth(WalkCallback callback, int depth) {
+
+        Preconditions.checkNotNull(callback);
+        callback.onCurrentNode(depth, this, parent);
+
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            curChild.walkTreeWithDepth(callback, (depth + 1));
+            curChild = curChild.getRightSibling();
+        }
+    }
+
+
+    public boolean isItemContained(final NodeInterval newNode) {
+        return (newNode.getStart().compareTo(start) >= 0 &&
+                newNode.getStart().compareTo(end) <= 0 &&
+                newNode.getEnd().compareTo(start) >= 0 &&
+                newNode.getEnd().compareTo(end) <= 0);
+    }
+
+    public boolean isItemOverlap(final NodeInterval newNode) {
+        return ((newNode.getStart().compareTo(start) < 0 &&
+                 newNode.getEnd().compareTo(end) >= 0) ||
+                (newNode.getStart().compareTo(start) <= 0 &&
+                 newNode.getEnd().compareTo(end) > 0));
+    }
+
+    @JsonIgnore
+    public boolean isRoot() {
+        return parent == null;
+    }
+
+    public LocalDate getStart() {
+        return start;
+    }
+
+    public LocalDate getEnd() {
+        return end;
+    }
+
+    @JsonIgnore
+    public NodeInterval getParent() {
+        return parent;
+    }
+
+    @JsonIgnore
+    public NodeInterval getLeftChild() {
+        return leftChild;
+    }
+
+    @JsonIgnore
+    public NodeInterval getRightSibling() {
+        return rightSibling;
+    }
+
+    @JsonIgnore
+    public int getNbChildren() {
+        int result = 0;
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            result++;
+            curChild = curChild.rightSibling;
+        }
+        return result;
+    }
+
+    /**
+     * Since items may be added out of order, there is no guarantee that we don't suddenly have a new node
+     * whose interval emcompasses cuurent node(s). In which case we need to rebalance the tree.
+     *
+     * @param newNode node that triggered a rebalance operation
+     */
+    private void rebalance(final NodeInterval newNode) {
+
+        NodeInterval prevRebalanced = null;
+        NodeInterval curChild = leftChild;
+        List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
+        do {
+            if (curChild.isItemOverlap(newNode)) {
+                toBeRebalanced.add(curChild);
+            } else {
+                if (toBeRebalanced.size() > 0) {
+                    break;
+                }
+                prevRebalanced = curChild;
+            }
+            curChild = curChild.rightSibling;
+        } while (curChild != null);
+
+        newNode.parent = this;
+        final NodeInterval lastNodeToRebalance = toBeRebalanced.get(toBeRebalanced.size() - 1);
+        newNode.rightSibling = lastNodeToRebalance.rightSibling;
+        lastNodeToRebalance.rightSibling = null;
+        if (prevRebalanced == null) {
+            leftChild = newNode;
+        } else {
+            prevRebalanced.rightSibling = newNode;
+        }
+
+        NodeInterval prev = null;
+        for (NodeInterval cur : toBeRebalanced) {
+            cur.parent = newNode;
+            if (prev == null) {
+                newNode.leftChild = cur;
+            } else {
+                prev.rightSibling = cur;
+            }
+            prev = cur;
+        }
+    }
+
+    private void computeRootInterval(final NodeInterval newNode) {
+        if (!isRoot()) {
+            return;
+        }
+        this.start = (start == null || start.compareTo(newNode.getStart()) > 0) ? newNode.getStart() : start;
+        this.end = (end == null || end.compareTo(newNode.getEnd()) < 0) ? newNode.getEnd() : end;
+    }
+
+    /**
+     * Provides callback for walking the tree.
+     */
+    public interface WalkCallback {
+        public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent);
+    }
+
+    /**
+     * Provides custom logic for the search.
+     */
+    public interface SearchCallback {
+        /**
+         * Custom logic to decide which node to return.
+         *
+         * @param curNode found node
+         * @return evaluates whether this is the node that should be returned
+         */
+        boolean isMatch(NodeInterval curNode);
+    }
+
+    /**
+     * Provides the custom logic for when building resulting state from the tree.
+     */
+    public interface BuildNodeCallback {
+
+        /**
+         * Called when we hit a missing interval where there is no child.
+         *
+         * @param curNode   current node
+         * @param startDate startDate of the new interval to build
+         * @param endDate   endDate of the new interval to build
+         */
+        public void onMissingInterval(NodeInterval curNode, LocalDate startDate, LocalDate endDate);
+
+        /**
+         * Called when we hit a node with no children
+         *
+         * @param curNode current node
+         */
+        public void onLastNode(NodeInterval curNode);
+    }
+
+    /**
+     * Provides the custom logic for when adding nodes in the tree.
+     */
+    public interface AddNodeCallback {
+
+        /**
+         * Called when trying to insert a new node in the tree but there is already
+         * such a node for that same interval.
+         *
+         * @param existingNode
+         * @return this is the return value for the addNode method
+         */
+        public boolean onExistingNode(final NodeInterval existingNode);
+
+        /**
+         * Called prior to insert the new node in the tree
+         *
+         * @param insertionNode the parent node where this new node would be inserted
+         * @return true if addNode should proceed with the insertion and false otherwise
+         */
+        public boolean shouldInsertNode(final NodeInterval insertionNode);
+    }
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
new file mode 100644
index 0000000..8aa8312
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.tree.Item.ItemAction;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+
+/**
+ * Tree of invoice items for a given subscription.
+ */
+public class SubscriptionItemTree {
+
+    private boolean isBuilt;
+
+    private final UUID subscriptionId;
+    private ItemsNodeInterval root;
+
+    private List<Item> items;
+
+    private List<InvoiceItem> existingFixedItems;
+    private List<InvoiceItem> remainingFixedItems;
+    private List<InvoiceItem> pendingItemAdj;
+
+    private static final Comparator<InvoiceItem> INVOICE_ITEM_COMPARATOR = new Comparator<InvoiceItem>() {
+        @Override
+        public int compare(final InvoiceItem o1, final InvoiceItem o2) {
+            int startDateComp = o1.getStartDate().compareTo(o2.getStartDate());
+            if (startDateComp != 0) {
+                return startDateComp;
+            }
+            int itemTypeComp =  (o1.getInvoiceItemType().ordinal()<o2.getInvoiceItemType().ordinal() ? -1 :
+                                 (o1.getInvoiceItemType().ordinal()==o2.getInvoiceItemType().ordinal() ? 0 : 1));
+            if (itemTypeComp != 0) {
+                return itemTypeComp;
+            }
+            Preconditions.checkState(false, "Unexpected list of items for subscription " + o1.getSubscriptionId());
+            // Never reached...
+            return 0;
+        }
+    };
+
+    public SubscriptionItemTree(final UUID subscriptionId) {
+        this.subscriptionId = subscriptionId;
+        this.root = new ItemsNodeInterval();
+        this.items = new LinkedList<Item>();
+        this.existingFixedItems = new LinkedList<InvoiceItem>();
+        this.remainingFixedItems = new LinkedList<InvoiceItem>();
+        this.pendingItemAdj = new LinkedList<InvoiceItem>();
+        this.isBuilt = false;
+    }
+
+    /**
+     * Build the tree to return the list of existing items.
+     */
+    public void build() {
+        Preconditions.checkState(!isBuilt);
+        for (InvoiceItem item : pendingItemAdj) {
+            root.addAdjustment(item.getStartDate(), item.getAmount(), item.getLinkedItemId());
+        }
+        pendingItemAdj.clear();
+        root.buildForExistingItems(items);
+        isBuilt = true;
+    }
+
+    /**
+     * Flattens the tree so its depth only has one level below root -- becomes a list.
+     * <p>
+     * If the tree was not built, it is first built. The list of items is cleared and the state is now reset to unbuilt.
+     *
+     * @param reverse whether to reverse the existing items (recurring items now show up as CANCEL instead of ADD)
+     */
+    public void flatten(boolean reverse) {
+        if (!isBuilt) {
+            build();
+        }
+        root = new ItemsNodeInterval();
+        for (Item item : items) {
+            Preconditions.checkState(item.getAction() == ItemAction.ADD);
+            root.addExistingItem(new ItemsNodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
+        }
+        items.clear();
+        isBuilt = false;
+    }
+
+    public void buildForMerge() {
+        Preconditions.checkState(!isBuilt);
+        root.mergeExistingAndProposed(items);
+        isBuilt = true;
+    }
+
+    /**
+     * Add an existing item in the tree.
+     *
+     * @param invoiceItem new existing invoice item on disk.
+     */
+    public void addItem(final InvoiceItem invoiceItem) {
+
+        Preconditions.checkState(!isBuilt);
+        switch (invoiceItem.getInvoiceItemType()) {
+            case RECURRING:
+                root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+                break;
+
+            case REPAIR_ADJ:
+                root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.CANCEL)));
+                break;
+
+            case FIXED:
+                existingFixedItems.add(invoiceItem);
+                break;
+
+            case ITEM_ADJ:
+                pendingItemAdj.add(invoiceItem);
+                break;
+
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Merge a new proposed ietm in the tree.
+     *
+     * @param invoiceItem new proposed item that should be merged in the existing tree
+     */
+    public void mergeProposedItem(final InvoiceItem invoiceItem) {
+
+        Preconditions.checkState(!isBuilt);
+        switch (invoiceItem.getInvoiceItemType()) {
+            case RECURRING:
+                final boolean result = root.addProposedItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+                if (!result) {
+                    items.add(new Item(invoiceItem, ItemAction.ADD));
+                }
+                break;
+
+            case FIXED:
+                final InvoiceItem existingItem = Iterables.tryFind(existingFixedItems, new Predicate<InvoiceItem>() {
+                    @Override
+                    public boolean apply(final InvoiceItem input) {
+                        return input.matches(invoiceItem);
+                    }
+                }).orNull();
+                if (existingItem == null) {
+                    remainingFixedItems.add(invoiceItem);
+                }
+                break;
+
+            default:
+                Preconditions.checkState(false, "Unexpected proposed item " + invoiceItem);
+        }
+
+    }
+
+    /**
+     * Can be called prior or after merge with proposed items.
+     * <ul>
+     * <li>When called prior, the merge this gives a flat view of the existing items on disk
+     * <li>When called after the merge with proposed items, this gives the list of items that should now be written to disk -- new fixed, recurring and repair.
+     * </ul>
+     * @return a flat view of the items in the tree.
+     */
+    public List<InvoiceItem> getView() {
+
+        final List<InvoiceItem> tmp = new LinkedList<InvoiceItem>();
+        tmp.addAll(remainingFixedItems);
+        tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
+            @Override
+            public InvoiceItem apply(final Item input) {
+                return input.toInvoiceItem();
+            }
+        }), new Predicate<InvoiceItem>() {
+            @Override
+            public boolean apply(@Nullable final InvoiceItem input) {
+                return input != null;
+            }
+        }));
+
+        final List<InvoiceItem> result = Ordering.<InvoiceItem>from(INVOICE_ITEM_COMPARATOR).sortedCopy(tmp);
+        checkItemsListState(result);
+        return result;
+    }
+
+    // Verify there is no double billing, and no double repair (credits)
+    private void checkItemsListState(final List<InvoiceItem> orderedList) {
+
+        LocalDate prevRecurringEndDate = null;
+        LocalDate prevRepairEndDate = null;
+        for (InvoiceItem cur : orderedList) {
+            switch (cur.getInvoiceItemType()) {
+                case FIXED:
+                    break;
+
+                case RECURRING:
+                    if (prevRecurringEndDate != null) {
+                        Preconditions.checkState(prevRecurringEndDate.compareTo(cur.getStartDate()) <= 0);
+                    }
+                    prevRecurringEndDate = cur.getEndDate();
+                    break;
+
+                case REPAIR_ADJ:
+                    if (prevRepairEndDate != null) {
+                        Preconditions.checkState(prevRepairEndDate.compareTo(cur.getStartDate()) <= 0);
+                    }
+                    prevRepairEndDate = cur.getEndDate();
+                    break;
+
+                default:
+                    Preconditions.checkState(false, "Unexpected item type " + cur.getInvoiceItemType());
+            }
+        }
+    }
+
+    public UUID getSubscriptionId() {
+        return subscriptionId;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof SubscriptionItemTree)) {
+            return false;
+        }
+
+        final SubscriptionItemTree that = (SubscriptionItemTree) o;
+
+        if (root != null ? !root.equals(that.root) : that.root != null) {
+            return false;
+        }
+        if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = subscriptionId != null ? subscriptionId.hashCode() : 0;
+        result = 31 * result + (root != null ? root.hashCode() : 0);
+        return result;
+    }
+
+    @VisibleForTesting
+    ItemsNodeInterval getRoot() {
+        return root;
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
index 0488412..747b7ca 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
@@ -34,6 +34,7 @@ import org.testng.annotations.Test;
 
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
+import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.catalog.DefaultPrice;
 import com.ning.billing.catalog.MockInternationalPrice;
 import com.ning.billing.catalog.MockPlan;
@@ -45,6 +46,7 @@ import com.ning.billing.catalog.api.PhaseType;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
 import com.ning.billing.clock.ClockMock;
+import com.ning.billing.entity.EntityPersistenceException;
 import com.ning.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
 import com.ning.billing.invoice.MockBillingEventSet;
 import com.ning.billing.invoice.api.Invoice;
@@ -60,13 +62,11 @@ import com.ning.billing.invoice.model.DefaultInvoicePayment;
 import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
 import com.ning.billing.invoice.model.RecurringInvoiceItem;
 import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
-import com.ning.billing.subscription.api.SubscriptionBase;
-import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.entity.EntityPersistenceException;
 import com.ning.billing.junction.BillingEvent;
 import com.ning.billing.junction.BillingEventSet;
 import com.ning.billing.junction.BillingModeType;
+import com.ning.billing.subscription.api.SubscriptionBase;
+import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
 
 import com.google.common.collect.ImmutableMap;
 
@@ -1155,13 +1155,17 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         assertEquals(invoice.getBalance().compareTo(ZERO), 0);
     }
 
-    private SubscriptionBase getZombieSubscription() {
+    private SubscriptionBase getZombieSubscription(UUID subscriptionId) {
         final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class);
         Mockito.when(subscription.getId()).thenReturn(UUID.randomUUID());
         Mockito.when(subscription.getBundleId()).thenReturn(UUID.randomUUID());
         return subscription;
     }
 
+    private SubscriptionBase getZombieSubscription() {
+        return getZombieSubscription(UUID.randomUUID());
+    }
+
     @Test(groups = "slow")
     public void testInvoiceForFreeTrialWithRecurringDiscount() throws InvoiceApiException, CatalogApiException {
         final Currency currency = Currency.USD;
@@ -1281,7 +1285,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         ((ClockMock) clock).setDay(startDate);
 
         final LocalDate recuringStartDate = clock.getUTCNow().plusDays(30).toLocalDate();
-        final LocalDate recuringEndDate = clock.getUTCNow().plusDays(30).toLocalDate();
+        final LocalDate recuringEndDate = recuringStartDate.plusMonths(1);
         final LocalDate targetDate = recuringStartDate.plusDays(1);
 
         // FIRST CREATE INITIAL INVOICE WITH ONE RECURRING ITEM
@@ -1316,7 +1320,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
 
         // NOW COMPUTE A DIFFERENT ITEM TO TRIGGER REPAIR
         final BillingEventSet events = new MockBillingEventSet();
-        final SubscriptionBase subscription = getZombieSubscription();
+        final SubscriptionBase subscription = getZombieSubscription(subscriptionId);
 
         final Plan plan = Mockito.mock(Plan.class);
         Mockito.when(plan.getName()).thenReturn("plan");
@@ -1324,18 +1328,15 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         final PlanPhase phase1 = Mockito.mock(PlanPhase.class);
         Mockito.when(phase1.getName()).thenReturn("plan-phase1");
 
-        final PlanPhase phase2 = Mockito.mock(PlanPhase.class);
-        Mockito.when(phase2.getName()).thenReturn("plan-phase2");
-
         final BillingEvent event1 = invoiceUtil.createMockBillingEvent(null, subscription, recuringStartDate.toDateTimeAtStartOfDay(), plan, phase1, null,
                                                                        TEN, Currency.USD,
-                                                                       BillingPeriod.MONTHLY, 1, BillingModeType.IN_ADVANCE,
+                                                                       BillingPeriod.MONTHLY, 31, BillingModeType.IN_ADVANCE,
                                                                        "new-event", 1L, SubscriptionBaseTransitionType.CREATE);
         events.add(event1);
         final Invoice newInvoice = generator.generateInvoice(UUID.randomUUID(), events, invoices, targetDate, Currency.USD);
         invoiceUtil.createInvoice(newInvoice, true, context);
 
-        // VERIFY THAT WE STILL HAVE ONLY 2 ITEMS, MENAING THERE WERE NO REPAIR AND NO CBA GENERATED
+        // VERIFY THAT WE STILL HAVE ONLY 2 ITEMS, MEANING THERE WERE NO REPAIR AND NO CBA GENERATED
         final Invoice firstInvoice = new DefaultInvoice(invoiceDao.getById(invoiceId, context));
         assertNotNull(firstInvoice);
         assertEquals(firstInvoice.getInvoiceItems().size(), 2);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/TestBillingIntervalDetail.java b/invoice/src/test/java/com/ning/billing/invoice/generator/TestBillingIntervalDetail.java
index 65c692f..ce8f84e 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/generator/TestBillingIntervalDetail.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/TestBillingIntervalDetail.java
@@ -25,6 +25,69 @@ import com.ning.billing.invoice.InvoiceTestSuiteNoDB;
 
 public class TestBillingIntervalDetail extends InvoiceTestSuiteNoDB {
 
+    /*
+     *
+     *         Start         BCD    END_MONTH
+     * |---------|------------|-------|
+     *
+     */
+    @Test(groups = "fast")
+    public void testCalculateFirstBillingCycleDate1() throws Exception {
+        final LocalDate from = new LocalDate("2012-01-16");
+        final int bcd = 17;
+        final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL);
+        billingIntervalDetail.calculateFirstBillingCycleDate();
+        Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-17"));
+    }
+
+    /*
+     *
+     *         Start             END_MONTH    BCD
+     * |---------|-------------------| - - - -|
+     *
+     */
+    @Test(groups = "fast")
+    public void testCalculateFirstBillingCycleDate2() throws Exception {
+        final LocalDate from = new LocalDate("2012-02-16");
+        final int bcd = 30;
+        final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL);
+        billingIntervalDetail.calculateFirstBillingCycleDate();
+        Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-29"));
+    }
+
+    /*
+     * Here the interesting part is that BCD is prior start and
+     *  i) we use MONTHLY billing period
+     * ii) on the next month, there is no such date (2012-02-30 does not exist)
+     *
+     *                                      Start
+     *                              BCD     END_MONTH
+     * |----------------------------|--------|
+     *
+     */
+    @Test(groups = "fast")
+    public void testCalculateFirstBillingCycleDate4() throws Exception {
+        final LocalDate from = new LocalDate("2012-01-31");
+        final int bcd = 30;
+        final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.MONTHLY);
+        billingIntervalDetail.calculateFirstBillingCycleDate();
+        Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-29"));
+    }
+
+    /*
+     *
+     *         BCD                 Start      END_MONTH
+     * |---------|-------------------|-----------|
+     *
+     */
+    @Test(groups = "fast")
+    public void testCalculateFirstBillingCycleDate3() throws Exception {
+        final LocalDate from = new LocalDate("2012-02-16");
+        final int bcd = 14;
+        final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL);
+        billingIntervalDetail.calculateFirstBillingCycleDate();
+        Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2013-02-14"));
+    }
 
     @Test(groups = "fast")
     public void testNextBCDShouldNotBeInThePast() throws Exception {
@@ -102,5 +165,4 @@ public class TestBillingIntervalDetail extends InvoiceTestSuiteNoDB {
         Assert.assertEquals(effectiveEndDate, new LocalDate("2012-05-31"));
     }
 
-
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceHelper.java
index 17c2f2c..e2f53c6 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceHelper.java
@@ -112,7 +112,6 @@ public class TestInvoiceHelper {
     public static final BigDecimal TWENTY_FOUR = new BigDecimal("24.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal TWENTY_FIVE = new BigDecimal("25.0").setScale(NUMBER_OF_DECIMALS);
 
-    public static final BigDecimal TWENTY_SEVEN = new BigDecimal("27.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal TWENTY_EIGHT = new BigDecimal("28.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal TWENTY_NINE = new BigDecimal("29.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal THIRTY = new BigDecimal("30.0").setScale(NUMBER_OF_DECIMALS);
@@ -120,6 +119,8 @@ public class TestInvoiceHelper {
 
     public static final BigDecimal FORTY = new BigDecimal("40.0").setScale(NUMBER_OF_DECIMALS);
 
+    public static final BigDecimal SEVENTY_FIVE = new BigDecimal("75.0").setScale(NUMBER_OF_DECIMALS);
+
     public static final BigDecimal EIGHTY_NINE = new BigDecimal("89.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal NINETY = new BigDecimal("90.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal NINETY_ONE = new BigDecimal("91.0").setScale(NUMBER_OF_DECIMALS);
@@ -127,6 +128,9 @@ public class TestInvoiceHelper {
 
     public static final BigDecimal ONE_HUNDRED = new BigDecimal("100.0").setScale(NUMBER_OF_DECIMALS);
 
+    public static final BigDecimal THREE_HUNDRED_AND_FOURTY_NINE = new BigDecimal("349.0").setScale(NUMBER_OF_DECIMALS);
+    public static final BigDecimal THREE_HUNDRED_AND_FIFTY_FOUR = new BigDecimal("354.0").setScale(NUMBER_OF_DECIMALS);
+
     public static final BigDecimal THREE_HUNDRED_AND_SIXTY_FIVE = new BigDecimal("365.0").setScale(NUMBER_OF_DECIMALS);
     public static final BigDecimal THREE_HUNDRED_AND_SIXTY_SIX = new BigDecimal("366.0").setScale(NUMBER_OF_DECIMALS);
 
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestProRation.java b/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestProRation.java
index dde0beb..1fb7106 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestProRation.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestProRation.java
@@ -20,6 +20,7 @@ import static com.ning.billing.invoice.TestInvoiceHelper.*;
 
 import java.math.BigDecimal;
 
+import org.joda.time.Days;
 import org.joda.time.LocalDate;
 import org.testng.annotations.Test;
 
@@ -39,16 +40,18 @@ public class TestProRation extends ProRationInAdvanceTestBase {
         final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 31);
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 24);
 
-        final BigDecimal expectedValue = ONE.add(FIFTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
+        // THREE_HUNDRED_AND_FOURTY_NINE is number of days between startDate and expected first billing cycle date (2012, 1, 15);
+        final BigDecimal expectedValue = THREE_HUNDRED_AND_FOURTY_NINE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(startDate, targetDate, 15, expectedValue);
     }
 
     @Test(groups = "fast")
     public void testSinglePlan_PrecedingProRation_CrossingYearBoundary() throws InvalidDateSequenceException {
         final LocalDate startDate = invoiceUtil.buildDate(2010, 12, 15);
-        final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 13);
+        final LocalDate targetDate = invoiceUtil.buildDate(2012, 1, 13);
 
-        final BigDecimal expectedValue = ONE.add(TWENTY.divide(THREE_HUNDRED_AND_SIXTY_FIVE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
+        // THREE_HUNDRED_AND_FOURTY_NINE is number of days between startDate and expected first billing cycle date (2011, 12, 4);
+        final BigDecimal expectedValue = ONE.add(THREE_HUNDRED_AND_FIFTY_FOUR.divide(THREE_HUNDRED_AND_SIXTY_FIVE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
         testCalculateNumberOfBillingCycles(startDate, targetDate, 4, expectedValue);
     }
 
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestProRation.java b/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestProRation.java
index ce3608e..ada9ec9 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestProRation.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestProRation.java
@@ -20,6 +20,7 @@ import static com.ning.billing.invoice.TestInvoiceHelper.*;
 
 import java.math.BigDecimal;
 
+import org.joda.time.Days;
 import org.joda.time.LocalDate;
 import org.testng.annotations.Test;
 
@@ -44,35 +45,23 @@ public class TestProRation extends ProRationInAdvanceTestBase {
         expectedValue = FOURTEEN.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 10, expectedValue);
 
-        expectedValue = FOURTEEN.divide(NINETY, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
+        // 75 is number of days between phaseChangeDate and next billing cycle date (2011, 5, 10)
+        // 89 is total number of days between the next and previous billing period  (2011, 2, 10) -> (2011, 5, 10)
+        expectedValue = SEVENTY_FIVE.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 10, expectedValue);
     }
 
     @Test(groups = "fast")
-    public void testSinglePlan_WithPhaseChange_BeforeBillCycleDay() throws InvalidDateSequenceException {
-        final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
-        final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
-        final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
-
-        BigDecimal expectedValue;
-        expectedValue = FOURTEEN.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
-        testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, expectedValue);
-
-        expectedValue = FOURTEEN.divide(NINETY, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
-        testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, expectedValue);
-    }
-
-    @Test(groups = "fast")
     public void testSinglePlan_WithPhaseChange_OnBillCycleDay() throws InvalidDateSequenceException {
         final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
         final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
-        final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 3);
+        final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 3);
 
         BigDecimal expectedValue;
         expectedValue = FOURTEEN.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, expectedValue);
 
-        expectedValue = FOURTEEN.divide(NINETY, NUMBER_OF_DECIMALS, ROUNDING_METHOD).add(ONE);
+        expectedValue = ONE.add(SEVENTY_FIVE.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
         testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, expectedValue);
     }
 
@@ -80,13 +69,13 @@ public class TestProRation extends ProRationInAdvanceTestBase {
     public void testSinglePlan_WithPhaseChange_AfterBillCycleDay() throws InvalidDateSequenceException {
         final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
         final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
-        final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 4);
+        final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 4);
 
         BigDecimal expectedValue;
         expectedValue = FOURTEEN.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, expectedValue);
 
-        expectedValue = FOURTEEN.divide(NINETY, NUMBER_OF_DECIMALS, ROUNDING_METHOD).add(ONE);
+        expectedValue = SEVENTY_FIVE.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD).add(ONE);
         testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, expectedValue);
     }
 
@@ -235,12 +224,13 @@ public class TestProRation extends ProRationInAdvanceTestBase {
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 21);
 
         BigDecimal expectedValue;
-        expectedValue = SEVEN.divide(NINETY_TWO, NUMBER_OF_DECIMALS, ROUNDING_METHOD);
-        expectedValue = expectedValue.add(ONE);
-        expectedValue = expectedValue.add(THREE.divide(NINETY_TWO, NUMBER_OF_DECIMALS, ROUNDING_METHOD));
+        // startDate, 2011, 4, 7 -> 66 days out of 2011, 1, 7, 2011, 4, 7 -> 90
+        expectedValue = new BigDecimal("66.00").divide(NINETY, 2 * NUMBER_OF_DECIMALS, ROUNDING_METHOD);
+        // 2011, 1, 7, planChangeDate-> 33 days out of 2011, 4, 7, 2011, 7, 7 -> 89
+        expectedValue = expectedValue.add(new BigDecimal("33.00").divide(NINETY_ONE, 2 * NUMBER_OF_DECIMALS, ROUNDING_METHOD)).setScale(NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 7, expectedValue);
 
-        expectedValue = FIVE.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD).add(ONE);
+        expectedValue = FIVE.divide(EIGHTY_NINE, NUMBER_OF_DECIMALS, ROUNDING_METHOD).add(ONE).setScale(NUMBER_OF_DECIMALS, ROUNDING_METHOD);
         testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 15, expectedValue);
     }
 
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
new file mode 100644
index 0000000..9512591
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import com.ning.billing.invoice.tree.NodeInterval.AddNodeCallback;
+import com.ning.billing.invoice.tree.NodeInterval.BuildNodeCallback;
+import com.ning.billing.invoice.tree.NodeInterval.SearchCallback;
+import com.ning.billing.invoice.tree.NodeInterval.WalkCallback;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
+
+    private AddNodeCallback CALLBACK = new DummyAddNodeCallback();
+
+    public class DummyNodeInterval extends NodeInterval {
+
+        private final UUID id;
+
+        public DummyNodeInterval() {
+            this.id = UUID.randomUUID();
+        }
+
+        public DummyNodeInterval(final NodeInterval parent, final LocalDate startDate, final LocalDate endDate) {
+            super(parent, startDate, endDate);
+            this.id = UUID.randomUUID();
+        }
+
+        public UUID getId() {
+            return id;
+        }
+    }
+
+    public class DummyAddNodeCallback implements AddNodeCallback {
+
+        @Override
+        public boolean onExistingNode(final NodeInterval existingNode) {
+            return false;
+        }
+
+        @Override
+        public boolean shouldInsertNode(final NodeInterval insertionNode) {
+            return true;
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testAddExistingItemSimple() {
+        final DummyNodeInterval root = new DummyNodeInterval();
+
+        final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+        root.addNode(top, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+        final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+        final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+        root.addNode(firstChildLevel1, CALLBACK);
+        root.addNode(secondChildLevel1, CALLBACK);
+        root.addNode(thirdChildLevel1, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+        final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+        final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+        root.addNode(firstChildLevel2, CALLBACK);
+        root.addNode(secondChildLevel2, CALLBACK);
+        root.addNode(thirdChildLevel2, CALLBACK);
+
+        checkNode(top, 3, root, firstChildLevel1, null);
+        checkNode(firstChildLevel1, 2, top, firstChildLevel2, secondChildLevel1);
+        checkNode(secondChildLevel1, 0, top, null, thirdChildLevel1);
+        checkNode(thirdChildLevel1, 1, top, thirdChildLevel2, null);
+
+        checkNode(firstChildLevel2, 0, firstChildLevel1, null, secondChildLevel2);
+        checkNode(secondChildLevel2, 0, firstChildLevel1, null, null);
+        checkNode(thirdChildLevel2, 0, thirdChildLevel1, null, null);
+    }
+
+    @Test(groups = "fast")
+    public void testAddExistingItemWithRebalance() {
+        final DummyNodeInterval root = new DummyNodeInterval();
+
+        final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+        root.addNode(top, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+        final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+        final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+        root.addNode(firstChildLevel2, CALLBACK);
+        root.addNode(secondChildLevel2, CALLBACK);
+        root.addNode(thirdChildLevel2, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+        final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+        final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+        root.addNode(firstChildLevel1, CALLBACK);
+        root.addNode(secondChildLevel1, CALLBACK);
+        root.addNode(thirdChildLevel1, CALLBACK);
+
+        checkNode(top, 3, root, firstChildLevel1, null);
+        checkNode(firstChildLevel1, 2, top, firstChildLevel2, secondChildLevel1);
+        checkNode(secondChildLevel1, 0, top, null, thirdChildLevel1);
+        checkNode(thirdChildLevel1, 1, top, thirdChildLevel2, null);
+
+        checkNode(firstChildLevel2, 0, firstChildLevel1, null, secondChildLevel2);
+        checkNode(secondChildLevel2, 0, firstChildLevel1, null, null);
+        checkNode(thirdChildLevel2, 0, thirdChildLevel1, null, null);
+    }
+
+    @Test(groups = "fast")
+    public void testBuild() {
+        final DummyNodeInterval root = new DummyNodeInterval();
+
+        final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+        root.addNode(top, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+        final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+        final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+        root.addNode(firstChildLevel1, CALLBACK);
+        root.addNode(secondChildLevel1, CALLBACK);
+        root.addNode(thirdChildLevel1, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+        final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+        final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+        root.addNode(firstChildLevel2, CALLBACK);
+        root.addNode(secondChildLevel2, CALLBACK);
+        root.addNode(thirdChildLevel2, CALLBACK);
+
+        final List<NodeInterval> output = new LinkedList<NodeInterval>();
+
+        // Just build the missing pieces.
+        root.build(new BuildNodeCallback() {
+            @Override
+            public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+                output.add(createNodeInterval(startDate, endDate));
+            }
+
+            @Override
+            public void onLastNode(final NodeInterval curNode) {
+                // Nothing
+            }
+        });
+
+        final List<NodeInterval> expected = new LinkedList<NodeInterval>();
+        expected.add(createNodeInterval("2014-01-05", "2014-01-07"));
+        expected.add(createNodeInterval("2014-01-07", "2014-01-08"));
+        expected.add(createNodeInterval("2014-01-15", "2014-01-16"));
+        expected.add(createNodeInterval("2014-01-17", "2014-02-01"));
+
+        assertEquals(output.size(), expected.size());
+        checkInterval(output.get(0), expected.get(0));
+        checkInterval(output.get(1), expected.get(1));
+    }
+
+    @Test(groups = "fast")
+    public void testSearch() {
+        final DummyNodeInterval root = new DummyNodeInterval();
+
+        final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+        root.addNode(top, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+        final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+        final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+        root.addNode(firstChildLevel1, CALLBACK);
+        root.addNode(secondChildLevel1, CALLBACK);
+        root.addNode(thirdChildLevel1, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+        final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+        final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+        root.addNode(firstChildLevel2, CALLBACK);
+        root.addNode(secondChildLevel2, CALLBACK);
+        root.addNode(thirdChildLevel2, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel3 = createNodeInterval("2014-01-01", "2014-01-02");
+        final DummyNodeInterval secondChildLevel3 = createNodeInterval("2014-01-03", "2014-01-04");
+        root.addNode(firstChildLevel3, CALLBACK);
+        root.addNode(secondChildLevel3, CALLBACK);
+
+        final NodeInterval search1 = root.findNode(new LocalDate("2014-01-04"), new SearchCallback() {
+            @Override
+            public boolean isMatch(final NodeInterval curNode) {
+                return ((DummyNodeInterval) curNode).getId().equals(secondChildLevel3.getId());
+            }
+        });
+        checkInterval(search1, secondChildLevel3);
+
+        final NodeInterval search2 = root.findNode(new SearchCallback() {
+            @Override
+            public boolean isMatch(final NodeInterval curNode) {
+                return ((DummyNodeInterval) curNode).getId().equals(thirdChildLevel2.getId());
+            }
+        });
+        checkInterval(search2, thirdChildLevel2);
+
+        final NodeInterval nullSearch = root.findNode(new SearchCallback() {
+            @Override
+            public boolean isMatch(final NodeInterval curNode) {
+                return ((DummyNodeInterval) curNode).getId().equals("foo");
+            }
+        });
+        assertNull(nullSearch);
+    }
+
+    @Test(groups = "fast")
+    public void testWalkTree() {
+        final DummyNodeInterval root = new DummyNodeInterval();
+
+        final DummyNodeInterval firstChildLevel0 = createNodeInterval("2014-01-01", "2014-02-01");
+        root.addNode(firstChildLevel0, CALLBACK);
+
+        final DummyNodeInterval secondChildLevel0 = createNodeInterval("2014-02-01", "2014-03-01");
+        root.addNode(secondChildLevel0, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+        final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+        final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+        root.addNode(firstChildLevel1, CALLBACK);
+        root.addNode(secondChildLevel1, CALLBACK);
+        root.addNode(thirdChildLevel1, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+        final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-05");
+        final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+        root.addNode(firstChildLevel2, CALLBACK);
+        root.addNode(secondChildLevel2, CALLBACK);
+        root.addNode(thirdChildLevel2, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel3 = createNodeInterval("2014-01-01", "2014-01-02");
+        final DummyNodeInterval secondChildLevel3 = createNodeInterval("2014-01-03", "2014-01-04");
+        root.addNode(firstChildLevel3, CALLBACK);
+        root.addNode(secondChildLevel3, CALLBACK);
+
+        final List<NodeInterval> expected = new LinkedList<NodeInterval>();
+        expected.add(root);
+        expected.add(firstChildLevel0);
+        expected.add(firstChildLevel1);
+        expected.add(firstChildLevel2);
+        expected.add(firstChildLevel3);
+        expected.add(secondChildLevel2);
+        expected.add(secondChildLevel3);
+        expected.add(secondChildLevel1);
+        expected.add(thirdChildLevel1);
+        expected.add(thirdChildLevel2);
+        expected.add(secondChildLevel0);
+
+        final List<NodeInterval> result = new LinkedList<NodeInterval>();
+        root.walkTree(new WalkCallback() {
+            @Override
+            public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+                result.add(curNode);
+            }
+        });
+
+        assertEquals(result.size(), expected.size());
+        for (int i = 0; i < result.size(); i++) {
+            if (i == 0) {
+                assertTrue(result.get(0).isRoot());
+                checkInterval(result.get(0), createNodeInterval("2014-01-01", "2014-03-01"));
+            } else {
+                checkInterval(result.get(i), expected.get(i));
+            }
+        }
+    }
+
+    private void checkInterval(final NodeInterval real, final NodeInterval expected) {
+        assertEquals(real.getStart(), expected.getStart());
+        assertEquals(real.getEnd(), expected.getEnd());
+    }
+
+    private void checkNode(final NodeInterval node, final int expectedChildren, final DummyNodeInterval expectedParent, final DummyNodeInterval expectedLeftChild, final DummyNodeInterval expectedRightSibling) {
+        assertEquals(node.getNbChildren(), expectedChildren);
+        assertEquals(node.getParent(), expectedParent);
+        assertEquals(node.getRightSibling(), expectedRightSibling);
+        assertEquals(node.getLeftChild(), expectedLeftChild);
+        assertEquals(node.getLeftChild(), expectedLeftChild);
+    }
+
+    private DummyNodeInterval createNodeInterval(final LocalDate startDate, final LocalDate endDate) {
+        return new DummyNodeInterval(null, startDate, endDate);
+    }
+
+    private DummyNodeInterval createNodeInterval(final String startDate, final String endDate) {
+        return createNodeInterval(new LocalDate(startDate), new LocalDate(endDate));
+    }
+
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
new file mode 100644
index 0000000..258ada8
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -0,0 +1,828 @@
+/*
+ * Copyright 2010-2014 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.tree;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
+import com.ning.billing.invoice.model.ItemAdjInvoiceItem;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.util.jackson.ObjectMapper;
+
+import com.google.common.collect.Lists;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
+
+    private final UUID invoiceId = UUID.randomUUID();
+    private final UUID accountId = UUID.randomUUID();
+    private final UUID subscriptionId = UUID.randomUUID();
+    private final UUID bundleId = UUID.randomUUID();
+    private final String planName = "my-plan";
+    private final String phaseName = "my-phase";
+    private final Currency currency = Currency.USD;
+
+    @Test(groups = "fast")
+    public void testSimpleRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate repairDate = new LocalDate(2014, 1, 23);
+
+        final BigDecimal rate1 = new BigDecimal("12.00");
+        final BigDecimal amount1 = rate1;
+
+        final BigDecimal rate2 = new BigDecimal("14.85");
+        final BigDecimal amount2 = rate2;
+
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem newItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", repairDate, endDate, amount2, rate2, currency);
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate, endDate, amount1.negate(), currency, initial.getId());
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, repairDate, new BigDecimal("8.52"), rate1, currency);
+        expectedResult.add(expected1);
+        final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", repairDate, endDate, amount2, rate2, currency);
+        expectedResult.add(expected2);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(initial);
+        tree.addItem(newItem);
+        tree.addItem(repair);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+        tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(repair);
+        tree.addItem(newItem);
+        tree.addItem(initial);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+        tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(repair);
+        tree.addItem(initial);
+        tree.addItem(newItem);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMultipleRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate repairDate1 = new LocalDate(2014, 1, 23);
+
+        final LocalDate repairDate2 = new LocalDate(2014, 1, 26);
+
+        final BigDecimal rate1 = new BigDecimal("12.00");
+        final BigDecimal amount1 = rate1;
+
+        final BigDecimal rate2 = new BigDecimal("14.85");
+        final BigDecimal amount2 = rate2;
+
+        final BigDecimal rate3 = new BigDecimal("19.23");
+        final BigDecimal amount3 = rate3;
+
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate1, endDate, amount2, rate2, currency);
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate1, endDate, amount1.negate(), currency, initial.getId());
+
+        final InvoiceItem newItem2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate2, endDate, amount3, rate3, currency);
+        final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate2, endDate, amount2.negate(), currency, initial.getId());
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, repairDate1, new BigDecimal("8.52"), rate1, currency);
+        expectedResult.add(expected1);
+        final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate1, repairDate2, new BigDecimal("4.95"), rate2, currency);
+        expectedResult.add(expected2);
+        final InvoiceItem expected3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate2, endDate, amount3, rate3, currency);
+        expectedResult.add(expected3);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(initial);
+        tree.addItem(newItem1);
+        tree.addItem(repair1);
+        tree.addItem(newItem2);
+        tree.addItem(repair2);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+        tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(repair2);
+        tree.addItem(newItem1);
+        tree.addItem(newItem2);
+        tree.addItem(repair1);
+        tree.addItem(initial);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+        tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(repair1);
+        tree.addItem(newItem1);
+        tree.addItem(initial);
+        tree.addItem(repair2);
+        tree.addItem(newItem2);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMultipleBlockedBillings() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate blockStart1 = new LocalDate(2014, 1, 8);
+        final LocalDate unblockStart1 = new LocalDate(2014, 1, 10);
+
+        final LocalDate blockStart2 = new LocalDate(2014, 1, 17);
+        final LocalDate unblockStart2 = new LocalDate(2014, 1, 23);
+
+        final BigDecimal rate1 = new BigDecimal("12.00");
+        final BigDecimal amount1 = rate1;
+
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem block1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockStart1, unblockStart1, amount1.negate(), currency, initial.getId());
+        final InvoiceItem block2 = new RepairAdjInvoiceItem(invoiceId, accountId, blockStart2, unblockStart2, amount1.negate(), currency, initial.getId());
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockStart1, new BigDecimal("2.71"), rate1, currency);
+        expectedResult.add(expected1);
+        final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockStart1, blockStart2, new BigDecimal("2.71"), rate1, currency);
+        expectedResult.add(expected2);
+        final InvoiceItem expected3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockStart2, endDate, new BigDecimal("3.48"), rate1, currency);
+        expectedResult.add(expected3);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(initial);
+        tree.addItem(block1);
+        tree.addItem(block2);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testBlockAcrossPeriod() {
+
+        final LocalDate startDate1 = new LocalDate(2014, 1, 1);
+        final LocalDate blockDate = new LocalDate(2014, 1, 25);
+        final LocalDate startDate2 = new LocalDate(2014, 2, 1);
+        final LocalDate unblockDate = new LocalDate(2014, 2, 7);
+        final LocalDate endDate = new LocalDate(2014, 3, 1);
+
+        final BigDecimal rate1 = new BigDecimal("12.00");
+        final BigDecimal amount1 = rate1;
+
+        final InvoiceItem first = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, startDate2, amount1, rate1, currency);
+        final InvoiceItem second = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate, amount1, rate1, currency);
+        final InvoiceItem block1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate, startDate2, amount1.negate(), currency, first.getId());
+        final InvoiceItem block2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, unblockDate, amount1.negate(), currency, first.getId());
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, blockDate, new BigDecimal("9.29"), rate1, currency);
+        final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate, endDate, new BigDecimal("9.43"), rate1, currency);
+        expectedResult.add(expected1);
+        expectedResult.add(expected2);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(first);
+        tree.addItem(second);
+        tree.addItem(block1);
+        tree.addItem(block2);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testAnnualFullRepairFollowedByMonthly() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate firstMonthlyEndDate = new LocalDate(2014, 2, 1);
+        final LocalDate secondMonthlyEndDate = new LocalDate(2014, 3, 1);
+        final LocalDate endDate = new LocalDate(2015, 2, 1);
+
+        final BigDecimal rate1 = new BigDecimal("120.00");
+        final BigDecimal amount1 = rate1;
+
+        final BigDecimal rate2 = new BigDecimal("10.00");
+        final BigDecimal amount2 = rate2;
+
+        final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, amount1.negate(), currency, annual.getId());
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", startDate, firstMonthlyEndDate, amount2, rate2, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", firstMonthlyEndDate, secondMonthlyEndDate, amount2, rate2, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        expectedResult.add(monthly2);
+
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(annual);
+        tree.addItem(repair);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+        tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(monthly1);
+        tree.addItem(repair);
+        tree.addItem(annual);
+        tree.addItem(monthly2);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+        tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(annual);
+        tree.addItem(repair);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMonthlyToAnnualWithLeadingProRation() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endMonthly1 = new LocalDate(2014, 2, 1);
+        final LocalDate endMonthly2 = new LocalDate(2014, 3, 1);
+        final LocalDate switchToAnnualDate = new LocalDate(2014, 2, 23);
+        final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final BigDecimal yearlyRate = new BigDecimal("100.00");
+        final BigDecimal yearlyAmount = yearlyRate;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, endMonthly2, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, switchToAnnualDate, endMonthly2, monthlyAmount.negate(), currency, monthly2.getId());
+        final InvoiceItem leadingAnnualProration = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, switchToAnnualDate, endMonthly2, yearlyAmount, yearlyRate, currency);
+        final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly2, endDate, yearlyAmount, yearlyRate, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        final InvoiceItem monthly2Prorated = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, switchToAnnualDate, new BigDecimal("9.43"), monthlyRate, currency);
+        expectedResult.add(monthly2Prorated);
+        expectedResult.add(leadingAnnualProration);
+        expectedResult.add(annual);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(repair);
+        tree.addItem(leadingAnnualProration);
+        tree.addItem(annual);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMonthlyToAnnualWithNoProRation() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endMonthly1 = new LocalDate(2014, 2, 1);
+        final LocalDate endMonthly2 = new LocalDate(2014, 3, 1);
+        final LocalDate switchToAnnualDate = new LocalDate(2014, 2, 23);
+        final LocalDate endDate = new LocalDate(2015, 2, 23);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final BigDecimal yearlyRate = new BigDecimal("100.00");
+        final BigDecimal yearlyAmount = yearlyRate;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, endMonthly2, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, switchToAnnualDate, endMonthly2, monthlyAmount.negate(), currency, monthly2.getId());
+        final InvoiceItem annual = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, switchToAnnualDate, endDate, yearlyAmount, yearlyRate, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        final InvoiceItem monthly2Prorated = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, switchToAnnualDate, new BigDecimal("9.43"), monthlyRate, currency);
+        expectedResult.add(monthly2Prorated);
+        expectedResult.add(annual);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(repair);
+        tree.addItem(annual);
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeWithNoExisting() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(proposed1);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeTwoSimilarItems() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        tree.addItem(monthly1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeTwoDifferentItems() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount1 = monthlyRate1;
+
+        final BigDecimal monthlyRate2 = new BigDecimal("15.00");
+        final BigDecimal monthlyAmount2 = monthlyRate2;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+        tree.addItem(monthly1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount2, monthlyRate2, currency);
+
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, monthlyAmount1.negate(), currency, monthly1.getId());
+        expectedResult.add(proposed1);
+        expectedResult.add(repair);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeCancellationWithInitialRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate blockDate = new LocalDate(2014, 1, 25);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount1 = monthlyRate1;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+        tree.addItem(monthly1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, blockDate, endDate, monthlyAmount1, monthlyRate1, currency);
+
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, blockDate, new BigDecimal("-9.29"), currency, monthly1.getId());
+        expectedResult.add(repair);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeCancellationWithFinalRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate cancelDate = new LocalDate(2014, 1, 25);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount1 = monthlyRate1;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+        tree.addItem(monthly1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, monthlyAmount1, monthlyRate1, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-2.71"), currency, monthly1.getId());
+        expectedResult.add(repair);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeCancellationWithMiddleRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate blockDate = new LocalDate(2014, 1, 13);
+        final LocalDate unblockDate = new LocalDate(2014, 1, 25);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount1 = monthlyRate1;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+        tree.addItem(monthly1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockDate, monthlyAmount1, monthlyRate1, currency);
+        final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate, endDate, monthlyAmount1, monthlyRate1, currency);
+
+        tree.mergeProposedItem(proposed1);
+        tree.mergeProposedItem(proposed2);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate, unblockDate, new BigDecimal("-4.65"), currency, monthly1.getId());
+        expectedResult.add(repair);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+
+    @Test(groups = "fast")
+    public void testMergeCancellationWithTwoMiddleRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate blockDate1 = new LocalDate(2014, 1, 7);
+        final LocalDate unblockDate1 = new LocalDate(2014, 1, 13);
+        final LocalDate blockDate2 = new LocalDate(2014, 1, 17);
+        final LocalDate unblockDate2 = new LocalDate(2014, 1, 25);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        tree.addItem(monthly);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockDate1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate1, blockDate2, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem proposed3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate2, endDate, monthlyAmount, monthlyRate, currency);
+
+        tree.mergeProposedItem(proposed1);
+        tree.mergeProposedItem(proposed2);
+        tree.mergeProposedItem(proposed3);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate1, unblockDate1, new BigDecimal("-2.32"), currency, monthly.getId());
+        final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, blockDate2, unblockDate2, new BigDecimal("-3.10"), currency, monthly.getId());
+        expectedResult.add(repair1);
+        expectedResult.add(repair2);
+        verifyResult(tree.getView(), expectedResult);
+
+
+        // Dot it again but with proposed items out of order
+        final SubscriptionItemTree treeAgain = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthlyAgain = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        treeAgain.addItem(monthlyAgain);
+        treeAgain.flatten(true);
+
+        final InvoiceItem proposed2Again = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate1, blockDate2, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem proposed1Again = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockDate1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem proposed3Again = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockDate2, endDate, monthlyAmount, monthlyRate, currency);
+
+        treeAgain.mergeProposedItem(proposed1Again);
+        treeAgain.mergeProposedItem(proposed2Again);
+        treeAgain.mergeProposedItem(proposed3Again);
+        treeAgain.buildForMerge();
+
+        verifyResult(treeAgain.getView(), expectedResult);
+
+
+    }
+
+    @Test(groups = "fast")
+    public void testMergeUpgradeWithFinalRepair() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate upgradeDate = new LocalDate(2014, 1, 25);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate1 = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount1 = monthlyRate1;
+
+        final BigDecimal monthlyRate2 = new BigDecimal("20.00");
+        final BigDecimal monthlyAmount2 = monthlyRate1;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount1, monthlyRate1, currency);
+        tree.addItem(monthly1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, upgradeDate, monthlyAmount1, monthlyRate1, currency);
+        final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "foo", "foo", upgradeDate, endDate, monthlyAmount2, monthlyRate2, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.mergeProposedItem(proposed2);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, upgradeDate, endDate, new BigDecimal("-2.71"), currency, monthly1.getId());
+        expectedResult.add(proposed2);
+        expectedResult.add(repair);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeWithSecondRepair() {
+
+        final LocalDate startDate = new LocalDate(2012, 5, 1);
+        final LocalDate endDate = new LocalDate(2012, 6, 1);
+        final LocalDate change1 = new LocalDate(2012, 5, 7);
+        final LocalDate change2 = new LocalDate(2012, 5, 8);
+
+        final BigDecimal rate1 = new BigDecimal("599.95");
+        final BigDecimal amount1 = rate1;
+
+        final BigDecimal rate2 = new BigDecimal("9.95");
+        final BigDecimal proratedAmount2 = new BigDecimal("8.02");
+
+        final BigDecimal rate3 = new BigDecimal("29.95");
+        final BigDecimal proratedAmount3 = new BigDecimal("23.19");
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "foo", "foo", change1, endDate, proratedAmount2, rate2, currency);
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, change1, endDate, new BigDecimal("-483.86"), currency, initial.getId());
+
+        tree.addItem(initial);
+        tree.addItem(newItem1);
+        tree.addItem(repair1);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, change1, amount1, rate1, currency);
+        final InvoiceItem proposed2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "foo", "foo", change1, change2, proratedAmount3, rate2, currency);
+        final InvoiceItem proposed3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "bar", "bar", change2, endDate, proratedAmount3, rate3, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.mergeProposedItem(proposed2);
+        tree.mergeProposedItem(proposed3);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, change2, endDate, new BigDecimal("-7.70"), currency, initial.getId());
+        expectedResult.add(proposed3);
+        expectedResult.add(repair2);
+        verifyResult(tree.getView(), expectedResult);
+
+    }
+
+    @Test(groups = "fast")
+    public void testWithExistingFixedItem() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+        final BigDecimal fixedAmount = new BigDecimal("5.00");
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem fixed = new FixedPriceInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, fixedAmount, currency);
+        tree.addItem(monthly);
+        tree.addItem(fixed);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.mergeProposedItem(fixed);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testWithNewFixedItem() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+        final BigDecimal fixedAmount = new BigDecimal("5.00");
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem monthly = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        tree.addItem(monthly);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem fixed = new FixedPriceInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, fixedAmount, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.mergeProposedItem(fixed);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(fixed);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testRepairWithSmallItemAdjustment() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate itemAdjDate = new LocalDate(2014, 1, 2);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate cancelDate = new LocalDate(2014, 1, 23);
+
+        final BigDecimal rate1 = new BigDecimal("12.00");
+        final BigDecimal amount1 = rate1;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem itemAdj = new ItemAdjInvoiceItem(initial, itemAdjDate, new BigDecimal("-2.00"), currency);
+        tree.addItem(initial);
+        tree.addItem(itemAdj);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, amount1, rate1, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-3.48"), currency, initial.getId());
+        expectedResult.add(repair1);
+
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testRepairWithLargeItemAdjustment() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate itemAdjDate = new LocalDate(2014, 1, 2);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate cancelDate = new LocalDate(2014, 1, 23);
+
+        final BigDecimal rate1 = new BigDecimal("12.00");
+        final BigDecimal amount1 = rate1;
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+        final InvoiceItem itemAdj = new ItemAdjInvoiceItem(initial, itemAdjDate, new BigDecimal("-10.00"), currency);
+        tree.addItem(initial);
+        tree.addItem(itemAdj);
+        tree.flatten(true);
+
+        final InvoiceItem proposed1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, amount1, rate1, currency);
+        tree.mergeProposedItem(proposed1);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-2.00"), currency, initial.getId());
+        expectedResult.add(repair1);
+
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeMonthlyToAnnualWithNoProRation() {
+
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endMonthly1 = new LocalDate(2014, 2, 1);
+        final LocalDate endMonthly2 = new LocalDate(2014, 3, 1);
+        final LocalDate switchToAnnualDate = new LocalDate(2014, 2, 23);
+        final LocalDate endDate = new LocalDate(2015, 2, 23);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final BigDecimal yearlyRate = new BigDecimal("100.00");
+        final BigDecimal yearlyAmount = yearlyRate;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, endMonthly2, monthlyAmount, monthlyRate, currency);
+
+        // First test with items in order
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.flatten(true);
+
+        final InvoiceItem proposed = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, switchToAnnualDate, endDate, yearlyAmount, yearlyRate, currency);
+        final InvoiceItem proposedMonthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endMonthly1, monthlyAmount, monthlyRate, currency);
+        tree.mergeProposedItem(proposedMonthly1);
+        final InvoiceItem proRatedmonthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, endMonthly1, switchToAnnualDate, monthlyAmount, monthlyRate, currency);
+        tree.mergeProposedItem(proRatedmonthly2);
+        tree.mergeProposedItem(proposed);
+        tree.buildForMerge();
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, switchToAnnualDate, endMonthly2, new BigDecimal("-2.57"), currency, monthly2.getId());
+        expectedResult.add(proposed);
+        expectedResult.add(repair);
+
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void verifyJson() {
+
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final UUID id1 = UUID.fromString("e8ba6ce7-9bd4-417d-af53-70951ecaa99f");
+        final InvoiceItem yearly1 = new RecurringInvoiceItem(id1, new DateTime(), invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, new LocalDate("2014-01-01"), new LocalDate("2015-01-01"), BigDecimal.TEN, BigDecimal.TEN, currency);
+        tree.addItem(yearly1);
+
+        final UUID id2 = UUID.fromString("48db1317-9a6e-4666-bcc5-fc7d3d0defc8");
+        final InvoiceItem newItem = new RecurringInvoiceItem(id2, new DateTime(), invoiceId, accountId, bundleId, subscriptionId, "other-plan", "other-plan", new LocalDate("2014-08-01"), new LocalDate("2015-01-01"), BigDecimal.ONE, BigDecimal.ONE, currency);
+        tree.addItem(newItem);
+
+        final UUID id3 = UUID.fromString("02ec57f5-2723-478b-86ba-ebeaedacb9db");
+        final InvoiceItem repair = new RepairAdjInvoiceItem(id3, new DateTime(), invoiceId, accountId, new LocalDate("2014-08-01"), new LocalDate("2015-01-01"), BigDecimal.TEN.negate(), currency, yearly1.getId());
+        tree.addItem(repair);
+
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            tree.getRoot().jsonSerializeTree(new ObjectMapper(), outputStream);
+
+            final String json = outputStream.toString("UTF-8");
+            final String expectedJson = "[{\"start\":\"2014-01-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"startDate\":\"2014-01-01\",\"endDate\":\"2015-01-01\",\"amount\":10,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"}]},[{\"start\":\"2014-08-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"48db1317-9a6e-4666-bcc5-fc7d3d0defc8\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":1,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"},{\"id\":\"02ec57f5-2723-478b-86ba-ebeaedacb9db\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":10,\"currency\":\"USD\",\"linkedId\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"action\":\"CANCEL\"}]}]]";
+
+            assertEquals(json, expectedJson);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+
+    }
+
+    private void verifyResult(final List<InvoiceItem> result, final List<InvoiceItem> expectedResult) {
+        assertEquals(result.size(), expectedResult.size());
+        for (int i = 0; i < expectedResult.size(); i++) {
+            assertTrue(result.get(i).matches(expectedResult.get(i)));
+        }
+    }
+
+}