killbill-aplcache

Details

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 54b288a..b5af327 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
@@ -60,7 +60,6 @@ import com.ning.billing.util.config.InvoiceConfig;
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.Lists;
 import com.google.inject.Inject;
 
 /**
@@ -136,7 +135,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
                         !events.getSubscriptionIdsWithAutoInvoiceOff()
                                .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
 
-                        if (item.getInvoiceItemType() == InvoiceItemType.FIXED || item.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ) {
+                        if ( item.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ) {
                             existingItems.add(item);
                         } else {
                             tree.addItem(item, allSeenItems);
@@ -152,7 +151,6 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
             existingItems.addAll(recurringExistingItems);
         }
 
-
         final LocalDate adjustedTargetDate = adjustTargetDate(existingInvoices, targetDate);
 
         final Invoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), adjustedTargetDate, targetCurrency);
@@ -167,6 +165,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         // 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);
 
@@ -194,8 +193,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
      */
     void addRepairItems(final List<InvoiceItem> existingItems, final List<InvoiceItem> proposedItems) {
         for (final InvoiceItem existingItem : existingItems) {
-            if (existingItem.getInvoiceItemType() == InvoiceItemType.RECURRING ||
-                existingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
+            if (existingItem.getInvoiceItemType() == InvoiceItemType.RECURRING) {
                 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) {
@@ -308,21 +306,6 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
                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
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
index b7f3acc..b219108 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
@@ -18,13 +18,10 @@ package com.ning.billing.invoice.tree;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import javax.annotation.Nullable;
-
 import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoiceItemType;
 
@@ -42,7 +39,10 @@ public class AccountItemTree {
     }
 
     public void addItem(final InvoiceItem item, final List<InvoiceItem> allItems) {
-        if (item.getInvoiceItemType() != InvoiceItemType.RECURRING && item.getInvoiceItemType() != InvoiceItemType.REPAIR_ADJ) {
+        if (item.getInvoiceItemType() != InvoiceItemType.RECURRING &&
+            item.getInvoiceItemType() != InvoiceItemType.REPAIR_ADJ &&
+            item.getInvoiceItemType() != InvoiceItemType.FIXED &&
+            item.getInvoiceItemType() != InvoiceItemType.ITEM_ADJ) {
             return;
         }
         final UUID subscriptionId  = getSubscriptionId(item, allItems);
@@ -60,11 +60,32 @@ public class AccountItemTree {
         }
     }
 
+    public void mergeWithProposedItems(final List<InvoiceItem> proposedItems) {
+
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            tree.flatten(true);
+        }
+
+        for (InvoiceItem item : proposedItems) {
+            final UUID subscriptionId  = getSubscriptionId(item, null);
+            SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+            if (tree == null) {
+                tree = new SubscriptionItemTree(subscriptionId);
+                subscriptionItemTree.put(subscriptionId, tree);
+            }
+            tree.mergeProposedItem(item);
+        }
+
+        for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+            tree.buildForMerge();
+        }
+    }
+
     public List<InvoiceItem> getCurrentExistingItemsView() {
 
         final List<InvoiceItem> result = new ArrayList<InvoiceItem>();
         for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
-            final List<InvoiceItem> simplifiedView = tree.getSimplifiedView();
+            final List<InvoiceItem> simplifiedView = tree.getView();
             if (simplifiedView.size() > 0) {
                 result.addAll(simplifiedView);
             }
@@ -73,7 +94,8 @@ public class AccountItemTree {
     }
 
     private UUID getSubscriptionId(final InvoiceItem item, final List<InvoiceItem> allItems) {
-        if (item.getInvoiceItemType() == InvoiceItemType.RECURRING) {
+        if (item.getInvoiceItemType() == InvoiceItemType.RECURRING ||
+            item.getInvoiceItemType() == InvoiceItemType.FIXED) {
             return item.getSubscriptionId();
         } else {
             final InvoiceItem linkedItem  = Iterables.tryFind(allItems, new Predicate<InvoiceItem>() {
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..4d2801d
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/Item.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.generator.InvoiceDateUtils;
+import com.ning.billing.invoice.model.InvoicingConfiguration;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+public class Item {
+
+    private static final int ROUNDING_MODE = InvoicingConfiguration.getRoundingMode();
+    private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();
+
+    private final UUID id;
+    private final UUID accountId;
+    private final UUID bundleId;
+    private final UUID subscriptionId;
+    private final UUID invoiceId;
+    private final String planName;
+    private final String phaseName;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+    private final BigDecimal amount;
+    private final BigDecimal rate;
+    private final Currency currency;
+    private final DateTime createdDate;
+    private final UUID linkedId;
+
+    private BigDecimal currentRepairedAmount;
+    private BigDecimal adjustedAmount;
+
+    private final ItemAction action;
+
+    public enum ItemAction {
+        ADD,
+        CANCEL
+    }
+
+    public Item(final Item item, final ItemAction action) {
+        this.id = item.id;
+        this.accountId = item.accountId;
+        this.bundleId = item.bundleId;
+        this.subscriptionId = item.subscriptionId;
+        this.invoiceId = item.invoiceId;
+        this.planName = item.planName;
+        this.phaseName = item.phaseName;
+        this.startDate = item.startDate;
+        this.endDate = item.endDate;
+        this.amount = item.amount;
+        this.rate = item.rate;
+        this.currency = item.currency;
+        this.linkedId = item.linkedId;
+        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);
+
+        final BigDecimal positiveAmount = prorated ?
+                                          InvoiceDateUtils.calculateProrationBetweenDates(newStartDate, newEndDate, nbTotalDays)
+                                                          .multiply(rate).setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE) :
+                                          amount;
+
+        if (action == ItemAction.ADD) {
+            return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, positiveAmount, rate, currency);
+        } else {
+            final BigDecimal maxAvailableAmountAfterAdj = amount.subtract(adjustedAmount);
+            final BigDecimal maxAvailableAmountForRepair = maxAvailableAmountAfterAdj.subtract(currentRepairedAmount);
+            final BigDecimal positiveAmountForRepair = positiveAmount.compareTo(maxAvailableAmountForRepair) <= 0 ? positiveAmount : maxAvailableAmountForRepair;
+            return new RepairAdjInvoiceItem(invoiceId, accountId, newStartDate, newEndDate, positiveAmountForRepair.negate(), currency, linkedId);
+        }
+    }
+
+    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;
+    }
+
+    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
index ba5e0d2..dc258f5 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
@@ -27,114 +27,128 @@ import java.util.ListIterator;
 import java.util.Set;
 import java.util.UUID;
 
-import org.joda.time.Days;
 import org.joda.time.LocalDate;
 
 import com.ning.billing.invoice.api.InvoiceItem;
-import com.ning.billing.invoice.api.InvoiceItemType;
-import com.ning.billing.invoice.generator.InvoiceDateUtils;
-import com.ning.billing.invoice.model.InvoicingConfiguration;
-import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.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;
 
 public class ItemsInterval {
 
-    private static final int ROUNDING_MODE = InvoicingConfiguration.getRoundingMode();
-    private static final int NUMBER_OF_DECIMALS = InvoicingConfiguration.getNumberOfDecimals();
+    private final NodeInterval interval;
+    private LinkedList<Item> items;
 
-
-    private LinkedList<InvoiceItem> items;
-
-    public ItemsInterval() {
-        this(null);
+    public ItemsInterval(final NodeInterval interval) {
+        this(interval, null);
     }
 
-    public ItemsInterval(final InvoiceItem initialItem) {
+    public ItemsInterval(final NodeInterval interval, final Item initialItem) {
+        this.interval = interval;
         this.items = Lists.newLinkedList();
         if (initialItem != null) {
             items.add(initialItem);
         }
     }
 
-    public List<InvoiceItem> getItems() {
+    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 buildForNonRepairedItems(final LocalDate startDate, final LocalDate endDate, final List<InvoiceItem> output) {
-        final InvoiceItem item = createRecuringItem(startDate, endDate);
+    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);
         }
     }
 
-
-    public void buildFromItems(final List<InvoiceItem> output) {
-
-
+    public void buildFromItems(final List<Item> output, final boolean addRepair) {
         final Set<UUID> repairedIds = new HashSet<UUID>();
-        ListIterator<InvoiceItem> it = items.listIterator(items.size());
+        final ListIterator<Item> it = items.listIterator(items.size());
+
         while (it.hasPrevious()) {
-            final InvoiceItem cur = it.previous();
-            switch (cur.getInvoiceItemType()) {
-                case FIXED:
-                case RECURRING:
-                    // The only time we could see that true is a case of full repair, when the repair
-                    // points to an item that will end up in the same ItemsInterval
+            final Item cur = it.previous();
+            switch (cur.getAction()) {
+                case ADD:
                     if (!repairedIds.contains(cur.getId())) {
                         output.add(cur);
                     }
                     break;
-
-                case REPAIR_ADJ:
-                    repairedIds.add(cur.getLinkedItemId());
-                    break;
-
-                case ITEM_ADJ:
-                    // If item has been adjusted, we assume the item should not be re-invoiced so we leave it in the list.
+                case CANCEL:
+                    if (cur.getLinkedId() != null) {
+                        repairedIds.add(cur.getLinkedId());
+                    }
+                    if (addRepair) {
+                        output.add(cur);
+                    }
                     break;
-
-                // Ignored
-                case EXTERNAL_CHARGE:
-                case CBA_ADJ:
-                case CREDIT_ADJ:
-                case REFUND_ADJ:
-                default:
             }
         }
     }
 
-    public void insertSortedItem(final InvoiceItem item) {
+    // Just ensure that ADD items precedes CANCEL items
+    public void insertSortedItem(final Item item) {
         items.add(item);
-        Collections.sort(items, new Comparator<InvoiceItem>() {
+        Collections.sort(items, new Comparator<Item>() {
             @Override
-            public int compare(final InvoiceItem o1, final InvoiceItem o2) {
-
-                final int type1 = o1.getInvoiceItemType().ordinal();
-                final int type2 = o2.getInvoiceItemType().ordinal();
-                return (type1 < type2) ? -1 : ((type1 == type2) ? 0 : 1);
+            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;
+                }
             }
         });
     }
 
-    private InvoiceItem createRecuringItem(LocalDate startDate, LocalDate endDate) {
+    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();
+    }
+
+    private Item createNewItem(LocalDate startDate, LocalDate endDate, final boolean addRepair) {
 
-        final List<InvoiceItem> itemToConsider = new LinkedList<InvoiceItem>();
-        buildFromItems(itemToConsider);
+        final List<Item> itemToConsider = new LinkedList<Item>();
+        buildFromItems(itemToConsider, addRepair);
 
-        Iterator<InvoiceItem> it = itemToConsider.iterator();
+        Iterator<Item> it = itemToConsider.iterator();
         while (it.hasNext()) {
-            final InvoiceItem cur = it.next();
-            if (cur.getInvoiceItemType() == InvoiceItemType.RECURRING &&
-                cur.getStartDate().compareTo(startDate) <= 0 &&
-                cur.getEndDate().compareTo(endDate) >= 0) {
-                int nbTotalRepairedDays = Days.daysBetween(cur.getStartDate(), cur.getEndDate()).getDays();
-                final BigDecimal amount = InvoiceDateUtils.calculateProrationBetweenDates(startDate, endDate, nbTotalRepairedDays).multiply(cur.getRate()).setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE);
-                return new RecurringInvoiceItem(cur.getInvoiceId(), cur.getAccountId(), cur.getBundleId(), cur.getSubscriptionId(),
-                                                cur.getPlanName(), cur.getPhaseName(), startDate, endDate, amount, cur.getRate(), cur.getCurrency());
+            final Item cur = it.next();
+            if (cur.getAction() == ItemAction.CANCEL && !addRepair) {
+                continue;
+            }
+            final Item result = new Item(cur.toProratedInvoiceItem(startDate, endDate), cur.getAction());
+            if (cur.getAction() == ItemAction.CANCEL) {
+                cur.incrementCurrentRepairedAmount(result.getAmount());
             }
+            return result;
         }
         return null;
     }
-
 }
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
index b714f76..1e0d65a 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
@@ -16,12 +16,16 @@
 
 package com.ning.billing.invoice.tree;
 
+import java.math.BigDecimal;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.UUID;
 
 import org.joda.time.LocalDate;
 
 import com.ning.billing.invoice.api.InvoiceItem;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
 public class NodeInterval {
@@ -35,23 +39,23 @@ public class NodeInterval {
     private NodeInterval rightSibling;
 
     public NodeInterval() {
-        this.items = new ItemsInterval();
+        this.items = new ItemsInterval(this);
     }
 
-    public NodeInterval(final NodeInterval parent, final InvoiceItem item) {
+    public NodeInterval(final NodeInterval parent, final Item item) {
         this.start = item.getStartDate();
         this.end = item.getEndDate();
-        this.items = new ItemsInterval(item);
+        this.items = new ItemsInterval(this, item);
         this.parent = parent;
         this.leftChild = null;
         this.rightSibling = null;
     }
 
-    public void build(final List<InvoiceItem> output) {
+    public void build(final List<Item> output, boolean addRepair) {
 
         // There is no sub-interval, just add our own items.
         if (leftChild == null) {
-            items.buildFromItems(output);
+            items.buildFromItems(output, addRepair);
             return;
         }
 
@@ -59,45 +63,132 @@ public class NodeInterval {
         NodeInterval curChild = leftChild;
         while (curChild != null) {
             if (curChild.getStart().compareTo(curDate) > 0) {
-               items.buildForNonRepairedItems(curDate, curChild.getStart(), output);
+                items.buildForMissingInterval(curDate, curChild.getStart(), output, addRepair);
             }
-            curChild.build(output);
+            curChild.build(output, addRepair);
             curDate = curChild.getEnd();
             curChild = curChild.getRightSibling();
         }
         if (curDate.compareTo(end) < 0) {
-            items.buildForNonRepairedItems(curDate, end, output);
+            items.buildForMissingInterval(curDate, end, output, addRepair);
         }
     }
 
-    public boolean isItemContained(final InvoiceItem item) {
+    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 InvoiceItem item) {
+    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 void addItem(final InvoiceItem item) {
+    public void addAdjustment(final LocalDate adjustementDate, final BigDecimal amount, final UUID linkedId) {
+        NodeInterval node = findNode(adjustementDate, linkedId);
+        if (node == null) {
+            throw new TreeNodeException("Cannot add adjustement for item = " + linkedId + ", date = " + adjustementDate);
+        }
+        node.setAdjustment(amount.negate(), linkedId);
+    }
+
+    private void setAdjustment(final BigDecimal amount, final UUID linkedId) {
+        items.setAdjustment(amount, linkedId);
+    }
+
+    private NodeInterval findNode(final LocalDate date, final UUID targetItemId) {
+        if (!isRoot()) {
+            throw new TreeNodeException("findNode can only be called from root");
+        }
+        return findNodeRecursively(this, date, targetItemId);
+    }
+
+    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;
+    }
+
+    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) {
+            leftChild = newNode;
+            return true;
+        }
+
+        NodeInterval prevChild = null;
+        NodeInterval curChild = leftChild;
+        do {
+            Preconditions.checkState(!curChild.isItemOverlap(newNodeItem), "Item " + newNodeItem + " overlaps " + curChild);
+
+            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)) {
+                    curChild.mergeProposedItem(newNode);
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+
+            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);
+
+        prevChild.rightSibling = newNode;
+        return true;
+    }
+
+    public void addNodeInterval(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;
         }
-
-        final NodeInterval newNode = new NodeInterval(this, item);
         computeRootInterval(newNode);
         addNode(newNode);
     }
 
+    // STEPH TODO are parents correctly maintained and/or do we need them?
     private void addNode(final NodeInterval newNode) {
-        final InvoiceItem item = newNode.getItems().get(0);
+        final Item item = newNode.getItems().get(0);
         if (leftChild == null) {
             leftChild = newNode;
             return;
@@ -107,7 +198,7 @@ public class NodeInterval {
         NodeInterval curChild = leftChild;
         do {
             if (curChild.isItemContained(item)) {
-                curChild.addItem(item);
+                curChild.addNodeInterval(newNode);
                 return;
             }
 
@@ -134,13 +225,13 @@ public class NodeInterval {
 
     private void rebalance(final NodeInterval newNode) {
 
-        final InvoiceItem invoiceItem = newNode.getItems().get(0);
+        final Item item = newNode.getItems().get(0);
 
         NodeInterval prevRebalanced = null;
         NodeInterval curChild = leftChild;
         List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
         do {
-            if (curChild.isItemOverlap(invoiceItem)) {
+            if (curChild.isItemOverlap(item)) {
                 toBeRebalanced.add(curChild);
             } else {
                 if (toBeRebalanced.size() > 0) {
@@ -158,7 +249,6 @@ public class NodeInterval {
             prevRebalanced.rightSibling = newNode;
         }
 
-
         NodeInterval prev = null;
         for (NodeInterval cur : toBeRebalanced) {
             if (prev == null) {
@@ -202,51 +292,11 @@ public class NodeInterval {
         return rightSibling;
     }
 
-    public List<InvoiceItem> getItems() {
+    public List<Item> getItems() {
         return items.getItems();
     }
 
-    @Override
-    public boolean equals(final Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!(o instanceof NodeInterval)) {
-            return false;
-        }
-
-        final NodeInterval that = (NodeInterval) o;
-
-        if (end != null ? !end.equals(that.end) : that.end != null) {
-            return false;
-        }
-        if (items != null ? !items.equals(that.items) : that.items != null) {
-            return false;
-        }
-        if (leftChild != null ? !leftChild.equals(that.leftChild) : that.leftChild != null) {
-            return false;
-        }
-        if (parent != null ? !parent.equals(that.parent) : that.parent != null) {
-            return false;
-        }
-        if (rightSibling != null ? !rightSibling.equals(that.rightSibling) : that.rightSibling != null) {
-            return false;
-        }
-        if (start != null ? !start.equals(that.start) : that.start != null) {
-            return false;
-        }
-
-        return true;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = start != null ? start.hashCode() : 0;
-        result = 31 * result + (end != null ? end.hashCode() : 0);
-        result = 31 * result + (items != null ? items.hashCode() : 0);
-        result = 31 * result + (parent != null ? parent.hashCode() : 0);
-        result = 31 * result + (leftChild != null ? leftChild.hashCode() : 0);
-        result = 31 * result + (rightSibling != null ? rightSibling.hashCode() : 0);
-        return result;
+    public boolean containsItem(final UUID targetId) {
+        return items.containsItem(targetId);
     }
 }
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
index a5686d6..e137c4d 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
@@ -20,38 +20,138 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoiceItemType;
+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;
 
 public class SubscriptionItemTree {
 
+    private boolean isBuilt;
+
     private final UUID subscriptionId;
-    private final NodeInterval root;
+    private NodeInterval root;
 
-    private List<InvoiceItem> fixedOrRecuringItems;
+    private List<Item> items;
+
+    private List<InvoiceItem> existingFixedItems;
+    private List<InvoiceItem> remainingFixedItems;
+    private List<InvoiceItem> pendingItemAdj;
 
     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;
+    }
+
+    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;
     }
 
-    public void addItem(final InvoiceItem item) {
-        if (item.getInvoiceItemType() != InvoiceItemType.RECURRING && item.getInvoiceItemType() != InvoiceItemType.REPAIR_ADJ) {
-            return;
+    public void flatten(boolean reverse) {
+        if (!isBuilt) {
+            build();
+        }
+        root = new NodeInterval();
+        for (Item item : items) {
+            final InvoiceItem invoiceItem = item.toInvoiceItem();
+            Preconditions.checkState(item.getAction() == ItemAction.ADD);
+            root.addNodeInterval(new NodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
         }
-        root.addItem(item);
+        items.clear();
+        isBuilt = false;
     }
 
+    public void buildForMerge() {
+        Preconditions.checkState(!isBuilt);
+        root.build(items, true);
+        isBuilt = true;
+    }
 
-    public void build() {
-        if (fixedOrRecuringItems == null) {
-            fixedOrRecuringItems = new LinkedList<InvoiceItem>();
-            root.build(fixedOrRecuringItems);
+    public void addItem(final InvoiceItem invoiceItem) {
+
+        Preconditions.checkState(!isBuilt);
+        switch (invoiceItem.getInvoiceItemType()) {
+            case RECURRING:
+                root.addNodeInterval(new NodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+                break;
+
+            case REPAIR_ADJ:
+                root.addNodeInterval(new NodeInterval(root, new Item(invoiceItem, ItemAction.CANCEL)));
+                break;
+
+            case FIXED:
+                existingFixedItems.add(invoiceItem);
+                break;
+
+            case ITEM_ADJ:
+                pendingItemAdj.add(invoiceItem);
+                break;
+
+            default:
+                break;
         }
     }
 
-    public List<InvoiceItem> getSimplifiedView() {
-        return fixedOrRecuringItems;
+
+    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);
+        }
+
+    }
+
+    public List<InvoiceItem> getView() {
+
+        // STEPH TODO check that nodeInterval don't overlap or throw. => double billing...
+        final List<InvoiceItem> result = new LinkedList<InvoiceItem>();
+        result.addAll(remainingFixedItems);
+        result.addAll(Collections2.transform(items, new Function<Item, InvoiceItem>() {
+            @Override
+            public InvoiceItem apply(final Item input) {
+                return input.toInvoiceItem();
+            }
+        }));
+        return result;
     }
 
     public UUID getSubscriptionId() {
@@ -85,4 +185,5 @@ public class SubscriptionItemTree {
         result = 31 * result + (root != null ? root.hashCode() : 0);
         return result;
     }
+
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/TreeNodeException.java b/invoice/src/main/java/com/ning/billing/invoice/tree/TreeNodeException.java
new file mode 100644
index 0000000..4263c63
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/TreeNodeException.java
@@ -0,0 +1,23 @@
+package com.ning.billing.invoice.tree;
+
+public class TreeNodeException extends RuntimeException {
+
+    public TreeNodeException() {
+    }
+
+    public TreeNodeException(final String message) {
+        super(message);
+    }
+
+    public TreeNodeException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+    public TreeNodeException(final Throwable cause) {
+        super(cause);
+    }
+
+    public TreeNodeException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java b/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java
index cb02de4..15e099b 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/generator/TestDefaultInvoiceGeneratorRepairUnit.java
@@ -280,102 +280,4 @@ public class TestDefaultInvoiceGeneratorRepairUnit extends InvoiceTestSuiteNoDB 
         assertFalse(defaultInvoiceGenerator.isRepareeItemForRepairedItem(annual, annual));
         assertFalse(defaultInvoiceGenerator.isRepareeItemForRepairedItem(annual, monthly));
     }
-
-    /*********************************************  isRepareeIncludedInRepair logic ********************************/
-
-    // Check for an item whose start and endDate exactly fit in repair item
-    @Test(groups = "fast")
-    public void testIsRepareeIncludedInRepair1() throws Exception {
-        final LocalDate startDate = new LocalDate(2012, 5, 1);
-        final LocalDate startRepair = new LocalDate(2012, 8, 1);
-        final LocalDate endDate = new LocalDate(2013, 5, 1);
-
-        // Repaired item
-        final InvoiceItem repairedItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-
-        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoiceId, accountId, startRepair, endDate, BigDecimal.ONE, currency, repairedItem.getId());
-
-        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepair, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-        assertTrue(defaultInvoiceGenerator.isRepareeIncludedInRepair(repairItem, repairedItem.getSubscriptionId(), invoiceItem));
-    }
-
-    // Check for an item whose start is greater than repair startDate
-    @Test(groups = "fast")
-    public void testIsRepareeIncludedInRepair2() throws Exception {
-        final LocalDate startDate = new LocalDate(2012, 5, 1);
-        final LocalDate startRepair = new LocalDate(2012, 8, 1);
-        final LocalDate endDate = new LocalDate(2013, 5, 1);
-
-        // Repaired item
-        final InvoiceItem repairedItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-
-        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoiceId, accountId, startRepair, endDate, BigDecimal.ONE, currency, repairedItem.getId());
-
-        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepair.plusDays(1), endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-        assertTrue(defaultInvoiceGenerator.isRepareeIncludedInRepair(repairItem, repairedItem.getSubscriptionId(), invoiceItem));
-    }
-
-    // Check for an item whose endDate is lower than repair endDate
-    @Test(groups = "fast")
-    public void testIsRepareeIncludedInRepair3() throws Exception {
-        final LocalDate startDate = new LocalDate(2012, 5, 1);
-        final LocalDate startRepair = new LocalDate(2012, 8, 1);
-        final LocalDate endDate = new LocalDate(2013, 5, 1);
-
-        // Repaired item
-        final InvoiceItem repairedItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-
-        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoiceId, accountId, startRepair, endDate, BigDecimal.ONE, currency, repairedItem.getId());
-
-        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepair, endDate.minusDays(1), BigDecimal.TEN, BigDecimal.TEN, currency);
-        assertTrue(defaultInvoiceGenerator.isRepareeIncludedInRepair(repairItem, repairedItem.getSubscriptionId(), invoiceItem));
-    }
-
-    // Check for an item whose endDate is lower than repair endDate
-    @Test(groups = "fast")
-    public void testIsRepareeIncludedInRepair4() throws Exception {
-        final LocalDate startDate = new LocalDate(2012, 5, 1);
-        final LocalDate startRepair = new LocalDate(2012, 8, 1);
-        final LocalDate endDate = new LocalDate(2013, 5, 1);
-
-        // Repaired item
-        final InvoiceItem repairedItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-
-        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoiceId, accountId, startRepair, endDate, BigDecimal.ONE, currency, repairedItem.getId());
-
-        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepair, endDate.minusDays(1), BigDecimal.TEN, BigDecimal.TEN, currency);
-        assertTrue(defaultInvoiceGenerator.isRepareeIncludedInRepair(repairItem, repairedItem.getSubscriptionId(), invoiceItem));
-    }
-
-    // Check for an item whose endDate is greater than repair endDate
-    @Test(groups = "fast")
-    public void testIsRepareeIncludedInRepair5() throws Exception {
-        final LocalDate startDate = new LocalDate(2012, 5, 1);
-        final LocalDate startRepair = new LocalDate(2012, 8, 1);
-        final LocalDate endDate = new LocalDate(2013, 5, 1);
-
-        // Repaired item
-        final InvoiceItem repairedItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-
-        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoiceId, accountId, startRepair, endDate, BigDecimal.ONE, currency, repairedItem.getId());
-
-        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepair, endDate.plusDays(1), BigDecimal.TEN, BigDecimal.TEN, currency);
-        assertFalse(defaultInvoiceGenerator.isRepareeIncludedInRepair(repairItem, repairedItem.getSubscriptionId(), invoiceItem));
-    }
-
-    @Test(groups = "fast")
-    public void testIsRepareeIncludedInRepairWrongSubscription() throws Exception {
-        final LocalDate startDate = new LocalDate(2012, 5, 1);
-        final LocalDate startRepair = new LocalDate(2012, 8, 1);
-        final LocalDate endDate = new LocalDate(2013, 5, 1);
-
-        // Repaired item
-        final InvoiceItem repairedItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-
-        final InvoiceItem repairItem = new RepairAdjInvoiceItem(invoiceId, accountId, startRepair, endDate, BigDecimal.ONE, currency, repairedItem.getId());
-
-        final UUID otherSubscriptionId = UUID.fromString("a9cbee45-5796-4dc5-be1f-7c020518460d");
-        final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, otherSubscriptionId, planName, phaseName, startRepair, endDate, BigDecimal.TEN, BigDecimal.TEN, currency);
-        assertFalse(defaultInvoiceGenerator.isRepareeIncludedInRepair(repairItem, repairedItem.getSubscriptionId(), invoiceItem));
-    }
 }
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
index eb13737..6eddcc8 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -25,11 +25,13 @@ 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 sun.reflect.annotation.ExceptionProxy;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
@@ -74,21 +76,21 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(newItem);
         tree.addItem(repair);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
 
         tree = new SubscriptionItemTree(subscriptionId);
         tree.addItem(repair);
         tree.addItem(newItem);
         tree.addItem(initial);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
 
         tree = new SubscriptionItemTree(subscriptionId);
         tree.addItem(repair);
         tree.addItem(initial);
         tree.addItem(newItem);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
     }
 
     @Test(groups = "fast")
@@ -133,7 +135,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(newItem2);
         tree.addItem(repair2);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
 
         tree = new SubscriptionItemTree(subscriptionId);
         tree.addItem(repair2);
@@ -142,7 +144,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(repair1);
         tree.addItem(initial);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
 
         tree = new SubscriptionItemTree(subscriptionId);
         tree.addItem(repair1);
@@ -151,7 +153,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(repair2);
         tree.addItem(newItem2);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
     }
 
     @Test(groups = "fast")
@@ -187,35 +189,10 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(block1);
         tree.addItem(block2);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
     }
 
 
-    @Test(groups = "fast")
-    public void testInvoiceItemAdj() {
-
-        final LocalDate startDate = new LocalDate(2014, 1, 1);
-        final LocalDate itemAdjDate = new LocalDate(2014, 1, 7);
-        final LocalDate endDate = new LocalDate(2014, 2, 1);
-
-
-        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 adj = new ItemAdjInvoiceItem(initial, itemAdjDate, amount1, currency);
-
-        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
-        expectedResult.add(initial);
-
-        // First test with items in order
-        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
-        tree.addItem(initial);
-        tree.addItem(adj);
-        tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
-    }
-
 
     @Test(groups = "fast")
     public void testBlockAcrossPeriod() {
@@ -248,7 +225,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(block1);
         tree.addItem(block2);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
     }
 
     @Test(groups = "fast")
@@ -280,7 +257,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(monthly1);
         tree.addItem(monthly2);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
 
 
         tree = new SubscriptionItemTree(subscriptionId);
@@ -289,7 +266,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(annual);
         tree.addItem(monthly2);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        verifyResult(tree.getView(), expectedResult);
 
         tree = new SubscriptionItemTree(subscriptionId);
         tree.addItem(monthly1);
@@ -297,9 +274,392 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         tree.addItem(annual);
         tree.addItem(repair);
         tree.build();
-        verifyResult(tree.getSimplifiedView(), expectedResult);
+        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 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 testMergeWithInitialRepair() {
+
+        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);
+        expectedResult.add(proposed1);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+
+    @Test(groups = "fast")
+    public void testMergeWithFinalRepair() {
+
+        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(proposed1);
+        expectedResult.add(repair);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeWithMiddleRepair() {
+
+        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(proposed1);
+        expectedResult.add(repair);
+        expectedResult.add(proposed2);
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    @Test(groups = "fast")
+    public void testMergeWithTwoMiddleRepair() {
+
+        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(proposed1);
+        expectedResult.add(repair1);
+        expectedResult.add(proposed2);
+        expectedResult.add(repair2);
+        expectedResult.add(proposed3);
+        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 expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, amount1, rate1, currency);
+        expectedResult.add(expected1);
+        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 expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, cancelDate, amount1, rate1, currency);
+        expectedResult.add(expected1);
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, cancelDate, endDate, new BigDecimal("-2.00"), currency, initial.getId());
+        expectedResult.add(repair1);
+
+        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++) {
@@ -307,4 +667,5 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         }
     }
 
+
 }