killbill-memoizeit

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index ec6768b..eb85775 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -19,11 +19,14 @@
 package org.killbill.billing.beatrix.integration;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.HashMap;
 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.killbill.billing.account.api.Account;
@@ -37,8 +40,13 @@ import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
@@ -52,6 +60,10 @@ import static org.testng.Assert.assertNotNull;
 
 public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
 
+    @Inject
+    protected InvoiceDao invoiceDao;
+
+
     @AfterMethod(groups = "slow")
     public void afterMethod() throws Exception {
         super.afterMethod();
@@ -607,4 +619,124 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
         checkNoMoreInvoiceToGenerate(account);
     }
+
+    @Test(groups = "slow")
+    public void testWithSuperflousRepairedItems() throws Exception {
+
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(today);
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final String pricelistName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 1);
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+        invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+        //
+        // Check we get the first invoice at the phase event
+        //
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // Move the clock to 2012-05-02
+        clock.addDays(31);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 2);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+        invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        final Invoice lastInvoice = invoices.get(1);
+        invoiceChecker.checkInvoice(lastInvoice.getId(), callContext, toBeChecked);
+
+
+        //
+        // Let's add a bunch of items by hand to pretend we have lots of cancelling items that should be cleaned (data issue after a potential invoice bug)
+        //
+        // We test both full and partial repair.
+        final InvoiceModelDao shellInvoice = new InvoiceModelDao(UUID.randomUUID(), lastInvoice.getCreatedDate(), lastInvoice.getAccountId(), null,
+                                                                 lastInvoice.getInvoiceDate(), lastInvoice.getTargetDate(), lastInvoice.getCurrency(), false);
+
+        final InvoiceItemModelDao recurring1 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                       bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+                                                                       null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+        final InvoiceItemModelDao repair1 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                    bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                    null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("-249.95"), new BigDecimal("-249.95"), account.getCurrency(), recurring1.getId());
+
+        final InvoiceItemModelDao recurring2 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                       bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+                                                                       null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+
+        final InvoiceItemModelDao repair21 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                     bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                     null, new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 13), new BigDecimal("-100.95"), new BigDecimal("-100.95"), account.getCurrency(), recurring2.getId());
+
+        final InvoiceItemModelDao repair22 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                     bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                     null, new LocalDate(2012, 5, 13), new LocalDate(2012, 5, 22), new BigDecimal("-100"), new BigDecimal("-100"), account.getCurrency(), recurring2.getId());
+
+        final InvoiceItemModelDao repair23 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                     bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                     null, new LocalDate(2012, 5, 22), new LocalDate(2012, 6, 1), new BigDecimal("-49"), new BigDecimal("-49"), account.getCurrency(), recurring2.getId());
+
+
+
+        final InvoiceItemModelDao recurring3 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                       bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+                                                                       null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+
+        final InvoiceItemModelDao repair3 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                    bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                    null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("-249.95"), new BigDecimal("-249.95"), account.getCurrency(), recurring3.getId());
+
+        List<InvoiceItemModelDao> newItems = new ArrayList<InvoiceItemModelDao>();
+        newItems.add(recurring1);
+        newItems.add(repair1);
+        newItems.add(recurring2);
+        newItems.add(repair21);
+        newItems.add(repair22);
+        newItems.add(repair23);
+        newItems.add(recurring3);
+        newItems.add(repair3);
+        invoiceDao.createInvoice(shellInvoice, newItems, false, new FutureAccountNotifications(null, new HashMap<UUID, List<SubscriptionNotification>>()), internalCallContext);
+
+
+        // Move ahead one month, verify nothing from previous data was generated
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertEquals(invoices.size(), 3);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+
+
+    }
+
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
index ee2226a..ab28d81 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
@@ -242,4 +242,17 @@ public class DefaultPlanPhase extends ValidatingConfig<StandaloneCatalog> implem
         //result = 31 * result + (usages != null ? Arrays.hashCode(usages) : 0);
         return result;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultPlanPhase{");
+        sb.append("type=").append(type);
+        sb.append(", duration=").append(duration);
+        sb.append(", fixed=").append(fixed);
+        sb.append(", recurring=").append(recurring);
+        sb.append(", usages=").append(Arrays.toString(usages));
+        sb.append(", plan=").append(plan);
+        sb.append('}');
+        return sb.toString();
+    }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
index 48e946a..e3f9af0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -21,6 +21,7 @@ package org.killbill.billing.invoice.tree;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -282,18 +283,19 @@ public class ItemsNodeInterval extends NodeInterval {
     //   it has no more leaves and no more items.
     // Case B - This is a bit more involved: We look for full repair that happened in pieces; this will translate to an ADD element of a NodeInterval,
     // whose children completely map the interval (isPartitionedByChildren) and where each child will have a CANCEL item pointing to the ADD.
-    // When we detect such nodes, we delete both the ADD in the parent interval and the CANCEL in the children
+    // When we detect such nodes, we delete both the ADD in the parent interval and the CANCEL in the children (and cleanup the interval if it does not have items)
     //
     private void pruneTree() {
         walkTree(new WalkCallback() {
             @Override
             public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
 
-                if(curNode.isRoot()) {
+                if (curNode.isRoot()) {
                     return;
                 }
 
                 final ItemsInterval curNodeItems = ((ItemsNodeInterval) curNode).getItemsInterval();
+
                 // Case A:
                 final boolean isEmpty = curNodeItems.mergeCancellingPairs();
                 if (isEmpty && curNode.getLeftChild() == null) {
@@ -305,14 +307,22 @@ public class ItemsNodeInterval extends NodeInterval {
                 }
 
                 // Case B -- look for such case, and if found (foundFullRepairByParts) we fix them below.
-                final Iterator<Item> it =  curNodeItems.get_ADD_items().iterator();
+                List<Item> curNodeItemsToBeRemoved = null;
+                final Iterator<Item> it = curNodeItems.get_ADD_items().iterator();
+                // For each item on this curNode interval we check if there is a matching set of CANCEL items on the children (resulting in completely cancelling that item).
                 while (it.hasNext()) {
 
                     final Item curAddItem = it.next();
 
+                    //
+                    // We already know the children partition fully the 'curNode' interval, we just need to see if for each piece
+                    // we find a matching CANCEL item pointing to this 'curAddItem'
+                    //
                     NodeInterval curChild = curNode.getLeftChild();
-                    Map<ItemsInterval, Item> toBeRemoved = new HashMap<ItemsInterval, Item>();
-                    boolean foundFullRepairByParts = true;
+                    Map<ItemsInterval, Item> childrenCancellingToBeRemoved = new HashMap<ItemsInterval, Item>();
+
+                    // Note that because of previous iterations, curChild could now be null so we need to initialize the foundFullRepairByParts based on that new state.
+                    boolean foundFullRepairByParts = curChild != null;
                     while (curChild != null) {
                         final ItemsInterval curChildItems = ((ItemsNodeInterval) curChild).getItemsInterval();
                         Item cancellingItem = curChildItems.getCancelledItemIfExists(curAddItem.getId());
@@ -320,20 +330,27 @@ public class ItemsNodeInterval extends NodeInterval {
                             foundFullRepairByParts = false;
                             break;
                         }
-                        toBeRemoved.put(curChildItems, cancellingItem);
+                        childrenCancellingToBeRemoved.put(curChildItems, cancellingItem);
                         curChild = curChild.getRightSibling();
                     }
 
                     if (foundFullRepairByParts) {
-                        for (ItemsInterval curItemsInterval : toBeRemoved.keySet()) {
-                            curItemsInterval.remove(toBeRemoved.get(curItemsInterval));
+                        for (ItemsInterval curItemsInterval : childrenCancellingToBeRemoved.keySet()) {
+                            curItemsInterval.remove(childrenCancellingToBeRemoved.get(curItemsInterval));
                             if (curItemsInterval.size() == 0) {
                                 curNode.removeChild(curItemsInterval.getNodeInterval());
                             }
                         }
-                        curNodeItems.remove(curAddItem);
+                        if (curNodeItemsToBeRemoved == null) {
+                            curNodeItemsToBeRemoved = new ArrayList<Item>();
+                        }
+                        curNodeItemsToBeRemoved.add(curAddItem);
                     }
                 }
+                // Finally Execute the removal of the curNodeItems outside of the upper while loop so as to not trigger ConcurrentModificationException (see #641)
+                for (Item curNodeItemsRemoval : curNodeItemsToBeRemoved) {
+                    curNodeItems.remove(curNodeItemsRemoval);
+                }
             }
         });
     }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 102f9c7..edc9cf5 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -394,6 +394,189 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
         verifyResult(tree.getView(), expectedResult);
     }
 
+    // Will test the case A from ItemsNodeInterval#prune logic (when we delete a node while walking the tree)
+    @Test(groups = "fast")
+    public void testFullRepairPruneLogic1() {
+
+        final LocalDate startDate1 = new LocalDate(2015, 1, 1);
+        final LocalDate endDate1 = new LocalDate(2015, 2, 1);
+
+
+        final LocalDate startDate2 = endDate1;
+        final LocalDate endDate2 = new LocalDate(2015, 3, 1);
+
+        final LocalDate startDate3 = endDate2;
+        final LocalDate endDate3 = new LocalDate(2015, 4, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, endDate1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repairMonthly2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, endDate2, new BigDecimal("-12.00"), currency, monthly2.getId());
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate3, endDate3, monthlyAmount, monthlyRate, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        expectedResult.add(monthly3);
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(repairMonthly2);
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+
+    // Will test the case A from ItemsNodeInterval#prune logic (an item is left on the interval)
+    @Test(groups = "fast")
+    public void testFullRepairPruneLogic2() {
+
+        final LocalDate startDate1 = new LocalDate(2015, 1, 1);
+        final LocalDate endDate1 = new LocalDate(2015, 2, 1);
+
+
+        final LocalDate startDate2 = endDate1;
+        final LocalDate endDate2 = new LocalDate(2015, 3, 1);
+
+        final LocalDate startDate3 = endDate2;
+        final LocalDate endDate3 = new LocalDate(2015, 4, 1);
+
+        final BigDecimal monthlyRateInit = new BigDecimal("12.00");
+        final BigDecimal monthlyAmountInit = monthlyRateInit;
+
+        final BigDecimal monthlyRateFinal = new BigDecimal("15.00");
+        final BigDecimal monthlyAmountFinal = monthlyRateFinal;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, endDate1, monthlyRateInit, monthlyAmountInit, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyRateInit, monthlyAmountInit, currency);
+        final InvoiceItem repairMonthly2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, endDate2, new BigDecimal("-12.00"), currency, monthly2.getId());
+
+        final InvoiceItem monthly2New = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyRateFinal, monthlyAmountFinal, currency);
+
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate3, endDate3, monthlyRateFinal, monthlyAmountFinal, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        expectedResult.add(monthly2New);
+        expectedResult.add(monthly3);
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(repairMonthly2);
+        tree.addItem(monthly2New);
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+    }
+
+
+    // Will test the case B from ItemsNodeInterval#prune logic
+    @Test(groups = "fast")
+    public void testFullRepairByPartsPruneLogic1() {
+
+        final LocalDate startDate = new LocalDate(2015, 2, 1);
+        final LocalDate intermediate1 = new LocalDate(2015, 2, 8);
+        final LocalDate intermediate2 = new LocalDate(2015, 2, 16);
+        final LocalDate intermediate3 = new LocalDate(2015, 2, 24);
+        final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair11 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly1.getId());
+        final InvoiceItem repair12 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly1.getId());
+        final InvoiceItem repair13 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly1.getId());
+        final InvoiceItem repair14 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly1.getId());
+
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair21 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair23 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair24 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly2.getId());
+
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly3);
+
+        // First test with items in order
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(repair11);
+        tree.addItem(repair12);
+        tree.addItem(repair13);
+        tree.addItem(repair14);
+
+        tree.addItem(monthly2);
+        tree.addItem(repair21);
+        tree.addItem(repair22);
+        tree.addItem(repair23);
+        tree.addItem(repair24);
+
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    // Will test the case A and B from ItemsNodeInterval#prune logic
+    @Test(groups = "fast")
+    public void testFullRepairByPartsPruneLogic2() {
+
+        final LocalDate startDate = new LocalDate(2015, 2, 1);
+        final LocalDate intermediate1 = new LocalDate(2015, 2, 8);
+        final LocalDate intermediate2 = new LocalDate(2015, 2, 16);
+        final LocalDate intermediate3 = new LocalDate(2015, 2, 24);
+        final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair11 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, new BigDecimal("-12.00"), currency, monthly1.getId());
+
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair21 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair23 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair24 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly2.getId());
+
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly3);
+
+        // First test with items in order
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(repair11);
+
+        tree.addItem(monthly2);
+        tree.addItem(repair21);
+        tree.addItem(repair22);
+        tree.addItem(repair23);
+        tree.addItem(repair24);
+
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+
+
     @Test(groups = "fast")
     public void testMergeWithNoExisting() {