killbill-aplcache

Code review integration - Introduce tree walker - Fix item

2/26/2014 9:47:07 PM

Details

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 8aaa7e7..36e7b9a 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
@@ -32,11 +32,11 @@ import com.google.common.collect.Iterables;
 
 /**
  * Tree of invoice items for a given account.
- *
+ * <p/>
  * <p>It contains a map of <tt>SubscriptionItemTree</tt> and the logic is executed independently for all items
  * associated to a given subscription. That also means that invoice item adjustment which cross subscriptions
  * can't be correctly handled when they compete with other forms of adjustments.
- *
+ * <p/>
  * <p>The class is not thread safe, there is no such use case today, and there is a lifecyle to respect:
  * <ul>
  * <li>Add existing invoice items
@@ -50,6 +50,7 @@ public class AccountItemTree {
     private final UUID accountId;
     private final Map<UUID, SubscriptionItemTree> subscriptionItemTree;
     private final List<InvoiceItem> allExistingItems;
+    private List<InvoiceItem> pendingItemAdj;
 
     private boolean isBuilt;
 
@@ -58,6 +59,7 @@ public class AccountItemTree {
         this.subscriptionItemTree = new HashMap<UUID, SubscriptionItemTree>();
         this.isBuilt = false;
         this.allExistingItems = new LinkedList<InvoiceItem>();
+        this.pendingItemAdj = new LinkedList<InvoiceItem>();
     }
 
     /**
@@ -65,30 +67,76 @@ public class AccountItemTree {
      */
     public void build() {
         Preconditions.checkState(!isBuilt);
+
+        if (allExistingItems.size() > 0) {
+            for (InvoiceItem item : allExistingItems) {
+                addExistingItem(item, true);
+            }
+            allExistingItems.clear();
+        }
         for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
             tree.build();
         }
         isBuilt = true;
     }
+
     /**
      * Populate tree from existing items on disk
      *
      * @param existingItem an item read on disk
      */
     public void addExistingItem(final InvoiceItem existingItem) {
+        addExistingItem(existingItem, false);
+    }
+
+    /*
+    EXTERNAL_CHARGE,
+    // Fixed (one-time) charge
+    FIXED,
+    // Recurring charge
+    RECURRING,
+    // Internal adjustment, used for repair
+    REPAIR_ADJ,
+    // Internal adjustment, used as rollover credits
+    CBA_ADJ,
+    // Credit adjustment, either at the account level (on its own invoice) or against an existing invoice
+    // (invoice level adjustment)
+    CREDIT_ADJ,
+    // Invoice item adjustment (by itself or triggered by a refund)
+    ITEM_ADJ,
+    // Refund adjustment (against a posted payment), used when adjusting invoices
+    REFUND_ADJ
+     */
+    private void addExistingItem(final InvoiceItem existingItem, boolean failOnMissingSubscription) {
 
         Preconditions.checkState(!isBuilt);
-        if (existingItem.getInvoiceItemType() != InvoiceItemType.RECURRING &&
-            existingItem.getInvoiceItemType() != InvoiceItemType.REPAIR_ADJ &&
-            existingItem.getInvoiceItemType() != InvoiceItemType.FIXED &&
-            existingItem.getInvoiceItemType() != InvoiceItemType.ITEM_ADJ) {
-            return;
+        switch (existingItem.getInvoiceItemType()) {
+            case EXTERNAL_CHARGE:
+            case CBA_ADJ:
+            case CREDIT_ADJ:
+            case REFUND_ADJ:
+                return;
+
+            case RECURRING:
+            case REPAIR_ADJ:
+            case FIXED:
+            case ITEM_ADJ:
+                break;
+
+            default:
+                Preconditions.checkState(false, "Unknown invoice item type " + existingItem.getInvoiceItemType());
+
         }
 
         allExistingItems.add(existingItem);
 
-        final UUID subscriptionId  = getSubscriptionId(existingItem, allExistingItems);
-        Preconditions.checkNotNull(subscriptionId);
+        final UUID subscriptionId = getSubscriptionId(existingItem, allExistingItems);
+        Preconditions.checkState(subscriptionId != null || !failOnMissingSubscription);
+
+        if (subscriptionId == null && existingItem.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ) {
+            pendingItemAdj.add(existingItem);
+            return;
+        }
 
         if (!subscriptionItemTree.containsKey(subscriptionId)) {
             subscriptionItemTree.put(subscriptionId, new SubscriptionItemTree(subscriptionId));
@@ -104,13 +152,13 @@ public class AccountItemTree {
      */
     public void mergeWithProposedItems(final List<InvoiceItem> proposedItems) {
 
+        build();
         for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
             tree.flatten(true);
         }
-        isBuilt = true;
 
         for (InvoiceItem item : proposedItems) {
-            final UUID subscriptionId  = getSubscriptionId(item, null);
+            final UUID subscriptionId = getSubscriptionId(item, null);
             SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
             if (tree == null) {
                 tree = new SubscriptionItemTree(subscriptionId);
@@ -125,7 +173,6 @@ public class AccountItemTree {
     }
 
     /**
-     *
      * @return the resulting list of items that should be written to disk
      */
     public List<InvoiceItem> getResultingItemList() {
@@ -148,13 +195,13 @@ public class AccountItemTree {
             item.getInvoiceItemType() == InvoiceItemType.FIXED) {
             return item.getSubscriptionId();
         } else {
-            final InvoiceItem linkedItem  = Iterables.tryFind(allItems, new Predicate<InvoiceItem>() {
+            final InvoiceItem linkedItem = Iterables.tryFind(allItems, new Predicate<InvoiceItem>() {
                 @Override
                 public boolean apply(final InvoiceItem input) {
                     return item.getLinkedItemId().equals(input.getId());
                 }
-            }).get();
-            return linkedItem.getSubscriptionId();
+            }).orNull();
+            return linkedItem != null ? linkedItem.getSubscriptionId() : null;
         }
     }
 }
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 fa7bf04..2ead948 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
@@ -92,6 +92,28 @@ public class ItemsInterval {
      * @param mergeMode
      */
     public void buildFromItems(final List<Item> output, final boolean mergeMode) {
+        final Item item  = getResultingItem(mergeMode);
+        if (item != null) {
+            output.add(item);
+        }
+    }
+
+    private Item getResultingItem(final boolean mergeMode) {
+        return mergeMode ? getResulting_CANCEL_Item() : getResulting_ADD_Item();
+    }
+
+    private Item getResulting_CANCEL_Item() {
+        Preconditions.checkState(items.size() == 0 || items.size() == 1);
+        return Iterables.tryFind(items, new Predicate<Item>() {
+            @Override
+            public boolean apply(final Item input) {
+                return input.getAction() == ItemAction.CANCEL;
+            }
+        }).orNull();
+    }
+
+
+    private Item getResulting_ADD_Item() {
 
         final Set<UUID> repairedIds = new HashSet<UUID>();
         final ListIterator<Item> it = items.listIterator(items.size());
@@ -100,20 +122,13 @@ public class ItemsInterval {
             final Item cur = it.previous();
             switch (cur.getAction()) {
                 case ADD:
-                    // Don't consider ADD items in mergeMode as they are only there to specify the bounderies of the repair elements.
-                    if (!mergeMode) {
-                        // If we found a CANCEL item pointing to that item then don't return it as it was repair (full repair scenario)
-                        if (!repairedIds.contains(cur.getId())) {
-                            output.add(cur);
-                        }
+                    // If we found a CANCEL item pointing to that item then don't return it as it was repair (full repair scenario)
+                    if (!repairedIds.contains(cur.getId())) {
+                        return cur;
                     }
                     break;
 
                 case CANCEL:
-                    // In merge logic we want to CANCEL (repair) items)
-                    if (mergeMode) {
-                        output.add(cur);
-                    }
                     // In all cases populate the set with the id of target item being repaired
                     if (cur.getLinkedId() != null) {
                         repairedIds.add(cur.getLinkedId());
@@ -121,8 +136,10 @@ public class ItemsInterval {
                     break;
             }
         }
+        return null;
     }
 
+
     // Just ensure that ADD items precedes CANCEL items
     public void insertSortedItem(final Item item) {
         items.add(item);
@@ -163,17 +180,11 @@ public class ItemsInterval {
      */
     private Item createNewItem(LocalDate startDate, LocalDate endDate, final boolean mergeMode) {
 
-        final List<Item> itemToConsider = new LinkedList<Item>();
-        buildFromItems(itemToConsider, mergeMode);
-        if (itemToConsider.size() == 0) {
+        final Item item  = getResultingItem(mergeMode);
+        if (item == null) {
             return null;
         }
 
-        Preconditions.checkState(itemToConsider.size() == 1);
-        final Item item = itemToConsider.size() == 1 ? itemToConsider.get(0) : null;
-        Preconditions.checkState((!mergeMode && item.getAction() == ItemAction.ADD) ||
-                                 (mergeMode && item.getAction() == ItemAction.CANCEL));
-
         final Item result = new Item(item.toProratedInvoiceItem(startDate, endDate), item.getAction());
         if (item.getAction() == ItemAction.CANCEL && result != null) {
             item.incrementCurrentRepairedAmount(result.getAmount());
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 d18724f..4c575f5 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
@@ -71,7 +71,7 @@ public class NodeInterval {
      * @param output    result list of items
      * @param mergeMode mode used to produce output list
      */
-    public void build(final List<Item> output, boolean mergeMode) {
+    public void build(final List<Item> output, final boolean mergeMode) {
 
         // There is no sub-interval, just add our own items.
         if (leftChild == null) {
@@ -79,21 +79,52 @@ public class NodeInterval {
             return;
         }
 
-        LocalDate curDate = start;
+        final NodeInterval lastChild = walkChildren(new ChildCallback() {
+
+            // Start for the beginning of the interval and as we move through children keep the mark the end date for
+            // each child period.
+            LocalDate curDate = start;
+
+            @Override
+            public boolean performActionOnChild(final NodeInterval prevChild, final NodeInterval curChild) {
+                // If there is a hole, that is no child, we build the missing piece from ourself
+                if (curChild.getStart().compareTo(curDate) > 0) {
+                    items.buildForMissingInterval(curDate, curChild.getStart(), output, mergeMode);
+                }
+                // Recursively build for the child
+                curChild.build(output, mergeMode);
+                curDate = curChild.getEnd();
+                return false;
+            }
+        });
+        // Finally if there is a hole at the end, we build the missing piece from ourself
+        if (lastChild.getEnd().compareTo(end) < 0) {
+            items.buildForMissingInterval(lastChild.getEnd(), end, output, mergeMode);
+        }
+    }
+
+    private NodeInterval walkChildren(final ChildCallback callback) {
+
+        NodeInterval prevChild = null;
         NodeInterval curChild = leftChild;
         while (curChild != null) {
-            if (curChild.getStart().compareTo(curDate) > 0) {
-                items.buildForMissingInterval(curDate, curChild.getStart(), output, mergeMode);
+            boolean shouldBreak = callback.performActionOnChild(prevChild, curChild);
+            if (shouldBreak) {
+                return curChild;
             }
-            curChild.build(output, mergeMode);
-            curDate = curChild.getEnd();
+            prevChild = curChild;
             curChild = curChild.getRightSibling();
         }
-        if (curDate.compareTo(end) < 0) {
-            items.buildForMissingInterval(curDate, end, output, mergeMode);
-        }
+        return prevChild == null ? curChild : prevChild;
+    }
+
+    public interface ChildCallback {
+        public boolean performActionOnChild(NodeInterval prevChild, NodeInterval child);
     }
 
+
+
+
     /**
      * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
      * That means that if we were to not merge any new proposed items, we would end up with only those reversed existing
@@ -280,7 +311,7 @@ public class NodeInterval {
 
         NodeInterval prevChild = null;
         NodeInterval curChild = leftChild;
-        do {
+        while (curChild != null) {
             if (curChild.isItemContained(item)) {
                 curChild.addExistingItem(newNode);
                 return;
@@ -302,7 +333,7 @@ public class NodeInterval {
             }
             prevChild = curChild;
             curChild = curChild.rightSibling;
-        } while (curChild != null);
+        }
 
         prevChild.rightSibling = newNode;
     }
@@ -371,7 +402,7 @@ public class NodeInterval {
         return findNodeRecursively2(this, date, targetItemId);
     }
 
-    // TODO That method should be use instaed of findNodeRecursively2 to search the node more effectively using the time
+    // TODO That method should be use instead of findNodeRecursively2 to search the node more effectively using the time
     // but unfortunately that fails because of our test that use the wrong date when doing adjustments.
     private NodeInterval findNodeRecursively(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
         if (date.compareTo(curNode.getStart()) < 0 || date.compareTo(curNode.getEnd()) > 0) {