killbill-aplcache

Merge remote-tracking branch 'origin/invoice2' into new-amount-decimals-handling-rfc Signed-off-by:

2/26/2014 8:10:17 AM

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 4d94e20..cc2a894 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);
 
@@ -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")),
@@ -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/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 d060568..ab23915 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.89")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 2), new LocalDate(2012, 5, 2), InvoiceItemType.CBA_ADJ, new BigDecimal("231.89")));
+                // 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.75")),
-                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.07")));
-        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.75")),
-                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.07")));
-        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.75")),
-                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.07")));
-        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.75")),
-                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.07")));
-        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.69")));
         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.69")));
         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/util/InvoiceChecker.java b/beatrix/src/test/java/com/ning/billing/beatrix/util/InvoiceChecker.java
index 7925efe..bc4f042 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/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java
index 57bd5dc..74a58bb 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;
@@ -49,48 +44,15 @@ import com.ning.billing.invoice.model.InAdvanceBillingMode;
 import com.ning.billing.invoice.model.InvalidDateSequenceException;
 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.ning.billing.util.currency.KillBillMoney;
 
-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);
@@ -118,14 +80,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);
                     }
                 }
             }
@@ -139,191 +102,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 {
@@ -349,104 +132,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>();
@@ -540,71 +225,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());
-        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..8aaa7e7
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
@@ -0,0 +1,160 @@
+/*
+ * 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>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>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 boolean isBuilt;
+
+    public AccountItemTree(final UUID accountId) {
+        this.accountId = accountId;
+        this.subscriptionItemTree = new HashMap<UUID, SubscriptionItemTree>();
+        this.isBuilt = false;
+        this.allExistingItems = new LinkedList<InvoiceItem>();
+    }
+
+    /**
+     * build the subscription trees after they have been populated with existing items on disk
+     */
+    public void build() {
+        Preconditions.checkState(!isBuilt);
+        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) {
+
+        Preconditions.checkState(!isBuilt);
+        if (existingItem.getInvoiceItemType() != InvoiceItemType.RECURRING &&
+            existingItem.getInvoiceItemType() != InvoiceItemType.REPAIR_ADJ &&
+            existingItem.getInvoiceItemType() != InvoiceItemType.FIXED &&
+            existingItem.getInvoiceItemType() != InvoiceItemType.ITEM_ADJ) {
+            return;
+        }
+
+        allExistingItems.add(existingItem);
+
+        final UUID subscriptionId  = getSubscriptionId(existingItem, allExistingItems);
+        Preconditions.checkNotNull(subscriptionId);
+
+        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) {
+
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            tree.flatten(true);
+        }
+        isBuilt = 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());
+                }
+            }).get();
+            return linkedItem.getSubscriptionId();
+        }
+    }
+}
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..40e97ee
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java
@@ -0,0 +1,193 @@
+/*
+ * 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.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.util.currency.KillBillMoney;
+
+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 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) : amount;
+
+        if (action == ItemAction.ADD) {
+            return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, KillBillMoney.of(positiveAmount, currency), 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, KillBillMoney.of(positiveAmountForRepair.negate(), currency), 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..fa7bf04
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
@@ -0,0 +1,183 @@
+/*
+ * 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 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:
+                    // Don't consider ADD items in mergeMode as they are only there to specify the bounderies of the repair elements.
+                    if (!mergeMode) {
+                        // 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())) {
+                            output.add(cur);
+                        }
+                    }
+                    break;
+
+                case CANCEL:
+                    // In merge logic we want to CANCEL (repair) items)
+                    if (mergeMode) {
+                        output.add(cur);
+                    }
+                    // In all cases populate the set with the id of target item being repaired
+                    if (cur.getLinkedId() != null) {
+                        repairedIds.add(cur.getLinkedId());
+                    }
+                    break;
+            }
+        }
+    }
+
+    // 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 List<Item> itemToConsider = new LinkedList<Item>();
+        buildFromItems(itemToConsider, mergeMode);
+        if (itemToConsider.size() == 0) {
+            return null;
+        }
+
+        Preconditions.checkState(itemToConsider.size() == 1);
+        final Item item = itemToConsider.size() == 1 ? itemToConsider.get(0) : null;
+        Preconditions.checkState((!mergeMode && item.getAction() == ItemAction.ADD) ||
+                                 (mergeMode && item.getAction() == ItemAction.CANCEL));
+
+        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/NodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
new file mode 100644
index 0000000..de7fd1b
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
@@ -0,0 +1,392 @@
+/*
+ * 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.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+public class NodeInterval {
+
+    private LocalDate start;
+    private LocalDate end;
+    private ItemsInterval items;
+
+    private NodeInterval parent;
+    private NodeInterval leftChild;
+    private NodeInterval rightSibling;
+
+    public NodeInterval() {
+        this.items = new ItemsInterval(this);
+    }
+
+    public NodeInterval(final NodeInterval parent, final Item item) {
+        this.start = item.getStartDate();
+        this.end = item.getEndDate();
+        this.items = new ItemsInterval(this, item);
+        this.parent = parent;
+        this.leftChild = null;
+        this.rightSibling = null;
+    }
+
+    /**
+     * Build the output list from the elements in the tree.
+     * <p/>
+     * In the simple mode, mergeMode = false, 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 mergeProposedItem
+     *
+     * @param output    result list of items
+     * @param mergeMode mode used to produce output list
+     */
+    public void build(final List<Item> output, boolean mergeMode) {
+
+        // There is no sub-interval, just add our own items.
+        if (leftChild == null) {
+            items.buildFromItems(output, mergeMode);
+            return;
+        }
+
+        LocalDate curDate = start;
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            if (curChild.getStart().compareTo(curDate) > 0) {
+                items.buildForMissingInterval(curDate, curChild.getStart(), output, mergeMode);
+            }
+            curChild.build(output, mergeMode);
+            curDate = curChild.getEnd();
+            curChild = curChild.getRightSibling();
+        }
+        if (curDate.compareTo(end) < 0) {
+            items.buildForMissingInterval(curDate, end, output, mergeMode);
+        }
+    }
+
+    /**
+     * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
+     * 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 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 mergeProposedItem(final NodeInterval newNode) {
+
+        Preconditions.checkState(newNode.getItems().size() == 1, "Expected new node to have only one item");
+        final Item newNodeItem = newNode.getItems().get(0);
+
+        if (!isRoot() && newNodeItem.getStartDate().compareTo(start) == 0 && newNodeItem.getEndDate().compareTo(end) == 0) {
+            items.cancelItems(newNodeItem);
+            return true;
+        }
+        computeRootInterval(newNode);
+
+        if (leftChild == null) {
+            // There is no existing items, only new proposed one, nothing to add in that merge tree
+            if (isRoot()) {
+                return false;
+            } else {
+                // Proposed item is the first child of an existing item with the same product info.
+                leftChild = newNode;
+                return true;
+
+            }
+        }
+
+        NodeInterval prevChild = null;
+        NodeInterval curChild = leftChild;
+        do {
+            if (curChild.isItemContained(newNodeItem)) {
+                final Item existingNodeItem = curChild.getItems().get(0);
+
+                Preconditions.checkState(curChild.getItems().size() == 1, "Expected existing node to have only one item");
+                if (existingNodeItem.isSameKind(newNodeItem)) {
+                    // Proposed item has same product info than parent and is contained so insert it at the right place in the tree
+                    curChild.mergeProposedItem(newNode);
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+
+            // STEPH test for that code path
+            if (newNodeItem.getStartDate().compareTo(curChild.getStart()) < 0) {
+                newNode.rightSibling = curChild;
+                if (prevChild == null) {
+                    leftChild = newNode;
+                } else {
+                    prevChild.rightSibling = newNode;
+                }
+                return true;
+            }
+
+            prevChild = curChild;
+            curChild = curChild.rightSibling;
+        } while (curChild != null);
+
+        if (isRoot()) {
+            // The new proposed item spans over a new interval, nothing to add in the merge tree
+            return false;
+        } else {
+            prevChild.rightSibling = newNode;
+            return true;
+        }
+    }
+
+    /**
+     * Add an existing item in the tree of items.
+     *
+     * @param newNode new existing item to be added
+     */
+    public void addExistingItem(final NodeInterval newNode) {
+        final Item item = newNode.getItems().get(0);
+        if (!isRoot() && item.getStartDate().compareTo(start) == 0 && item.getEndDate().compareTo(end) == 0) {
+            items.insertSortedItem(item);
+            return;
+        }
+        computeRootInterval(newNode);
+        addNode(newNode);
+    }
+
+    /**
+     * 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) {
+        NodeInterval node = findNode(adjustementDate, targetId);
+        Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
+        node.setAdjustment(amount.negate(), targetId);
+    }
+
+    public boolean isItemContained(final Item item) {
+        return (item.getStartDate().compareTo(start) >= 0 &&
+                item.getStartDate().compareTo(end) <= 0 &&
+                item.getEndDate().compareTo(start) >= 0 &&
+                item.getEndDate().compareTo(end) <= 0);
+    }
+
+    public boolean isItemOverlap(final Item item) {
+        return ((item.getStartDate().compareTo(start) < 0 &&
+                 item.getEndDate().compareTo(end) >= 0) ||
+                (item.getStartDate().compareTo(start) <= 0 &&
+                 item.getEndDate().compareTo(end) > 0));
+    }
+
+
+
+    public boolean isRoot() {
+        return parent == null;
+    }
+
+    public LocalDate getStart() {
+        return start;
+    }
+
+    public LocalDate getEnd() {
+        return end;
+    }
+
+    public NodeInterval getParent() {
+        return parent;
+    }
+
+    public NodeInterval getLeftChild() {
+        return leftChild;
+    }
+
+    public NodeInterval getRightSibling() {
+        return rightSibling;
+    }
+
+    public List<Item> getItems() {
+        return items.getItems();
+    }
+
+    public boolean containsItem(final UUID targetId) {
+        return items.containsItem(targetId);
+    }
+
+    // STEPH TODO are parents correctly maintained and/or do we need them?
+    private void addNode(final NodeInterval newNode) {
+        final Item item = newNode.getItems().get(0);
+        if (leftChild == null) {
+            leftChild = newNode;
+            return;
+        }
+
+        NodeInterval prevChild = null;
+        NodeInterval curChild = leftChild;
+        do {
+            if (curChild.isItemContained(item)) {
+                curChild.addExistingItem(newNode);
+                return;
+            }
+
+            if (curChild.isItemOverlap(item)) {
+                rebalance(newNode);
+                return;
+            }
+
+            if (item.getStartDate().compareTo(curChild.getStart()) < 0) {
+                newNode.rightSibling = curChild;
+                if (prevChild == null) {
+                    leftChild = newNode;
+                } else {
+                    prevChild.rightSibling = newNode;
+                }
+                return;
+            }
+            prevChild = curChild;
+            curChild = curChild.rightSibling;
+        } while (curChild != null);
+
+        prevChild.rightSibling = newNode;
+    }
+
+    /**
+     * Since items may be added out of order, there is no guarantee that we don't suddenly had 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) {
+
+        final Item item = newNode.getItems().get(0);
+
+        NodeInterval prevRebalanced = null;
+        NodeInterval curChild = leftChild;
+        List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
+        do {
+            if (curChild.isItemOverlap(item)) {
+                toBeRebalanced.add(curChild);
+            } else {
+                if (toBeRebalanced.size() > 0) {
+                    break;
+                }
+                prevRebalanced = curChild;
+            }
+            curChild = curChild.rightSibling;
+        } while (curChild != null);
+
+        newNode.rightSibling = toBeRebalanced.get(toBeRebalanced.size() - 1).rightSibling;
+        if (prevRebalanced == null) {
+            leftChild = newNode;
+        } else {
+            prevRebalanced.rightSibling = newNode;
+        }
+
+        NodeInterval prev = null;
+        for (NodeInterval cur : toBeRebalanced) {
+            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;
+    }
+
+    private void setAdjustment(final BigDecimal amount, final UUID linkedId) {
+        items.setAdjustment(amount, linkedId);
+    }
+
+    private NodeInterval findNode(final LocalDate date, final UUID targetItemId) {
+        Preconditions.checkState(isRoot(), "findNode can only be called from root");
+        return findNodeRecursively2(this, date, targetItemId);
+    }
+
+    // TODO That method should be use instaed of findNodeRecursively2 to search the node more effectively using the time
+    // but unfortunately that fails because of our test that use the wrong date when doing adjustments.
+    private NodeInterval findNodeRecursively(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
+        if (date.compareTo(curNode.getStart()) < 0 || date.compareTo(curNode.getEnd()) > 0) {
+            return null;
+        }
+        NodeInterval curChild = curNode.getLeftChild();
+        while (curChild != null) {
+            if (curChild.getStart().compareTo(date) <= 0 && curChild.getEnd().compareTo(date) >= 0) {
+                if (curChild.containsItem(targetItemId)) {
+                    return curChild;
+                } else {
+                    return findNodeRecursively(curChild, date, targetItemId);
+                }
+            }
+            curChild = curChild.getRightSibling();
+        }
+        return null;
+    }
+
+    private NodeInterval findNodeRecursively2(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
+
+        if (!curNode.isRoot() && curNode.containsItem(targetItemId)) {
+            return curNode;
+        }
+
+        NodeInterval curChild = curNode.getLeftChild();
+        while (curChild != null) {
+            final NodeInterval result = findNodeRecursively2(curChild, date, targetItemId);
+            if (result != null) {
+                return result;
+            }
+            curChild = curChild.getRightSibling();
+        }
+        return null;
+    }
+}
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..49eb390
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
@@ -0,0 +1,275 @@
+/*
+ * 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.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 NodeInterval 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 = Integer.compare(o1.getInvoiceItemType().ordinal(), o2.getInvoiceItemType().ordinal());
+            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 NodeInterval();
+        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.build(items, false);
+        isBuilt = true;
+    }
+
+    /**
+     * Flattens the tree so its depth only has one levl 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 NodeInterval();
+        for (Item item : items) {
+            Preconditions.checkState(item.getAction() == ItemAction.ADD);
+            root.addExistingItem(new NodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
+        }
+        items.clear();
+        isBuilt = false;
+    }
+
+    public void buildForMerge() {
+        Preconditions.checkState(!isBuilt);
+        root.build(items, true);
+        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 NodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+                break;
+
+            case REPAIR_ADJ:
+                root.addExistingItem(new NodeInterval(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.mergeProposedItem(new NodeInterval(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;
+    }
+
+}
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 540c8e3..2d77b3e 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.ning.billing.util.currency.KillBillMoney;
 
 import com.google.common.collect.ImmutableMap;
@@ -1156,13 +1156,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;
@@ -1282,7 +1286,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
@@ -1317,7 +1321,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");
@@ -1325,18 +1329,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/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
new file mode 100644
index 0000000..f157cdf
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -0,0 +1,771 @@
+/*
+ * 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.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+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.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);
+    }
+
+    @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);
+    }
+
+    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)));
+        }
+    }
+
+}