killbill-aplcache

NodeInterval and other invoice tree related classes rework;

2/27/2014 11:11:25 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 96bbda4..49a8c2d 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
@@ -89,24 +89,6 @@ public class AccountItemTree {
         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);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
index df31e26..fdf29f0 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
@@ -1,11 +1,18 @@
 package com.ning.billing.invoice.tree;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.LocalDate;
 
+import com.ning.billing.util.jackson.ObjectMapper;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Preconditions;
 
 public class ItemsNodeInterval extends NodeInterval {
@@ -21,12 +28,13 @@ public class ItemsNodeInterval extends NodeInterval {
         this.items = new ItemsInterval(this, item);
     }
 
-    public ItemsInterval getItems() {
+    @JsonIgnore
+    public ItemsInterval getItemsInterval() {
         return items;
     }
 
-    public boolean containsItem(final UUID targetId) {
-        return items.containsItem(targetId);
+    public List<Item> getItems() {
+        return items.getItems();
     }
 
     /**
@@ -53,14 +61,14 @@ public class ItemsNodeInterval extends NodeInterval {
     public void buildForExistingItems(final List<Item> output) {
         build(new BuildNodeCallback() {
             @Override
-            public void buildMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
-                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+            public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
                 items.buildForMissingInterval(startDate, endDate, output, false);
             }
 
             @Override
-            public void buildNode(final NodeInterval curNode) {
-                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+            public void onLastNode(final NodeInterval curNode) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
                 items.buildFromItems(output, false);
             }
         });
@@ -91,14 +99,14 @@ public class ItemsNodeInterval extends NodeInterval {
     public void mergeExistingAndProposed(final List<Item> output) {
         build(new BuildNodeCallback() {
             @Override
-            public void buildMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
-                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+            public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
                 items.buildForMissingInterval(startDate, endDate, output, true);
             }
 
             @Override
-            public void buildNode(final NodeInterval curNode) {
-                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+            public void onLastNode(final NodeInterval curNode) {
+                final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
                 items.buildFromItems(output, true);
             }
         });
@@ -115,8 +123,8 @@ public class ItemsNodeInterval extends NodeInterval {
             @Override
             public boolean onExistingNode(final NodeInterval existingNode) {
                 if (!existingNode.isRoot() && newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0) {
-                    final Item item = newNode.getItems().getItems().get(0);
-                    final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItems();
+                    final Item item = newNode.getItems().get(0);
+                    final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
                     existingOrNewNodeItems.insertSortedItem(item);
                 }
                 // There is no new node added but instead we just populated the list of items for the already existing node.
@@ -148,8 +156,8 @@ public class ItemsNodeInterval extends NodeInterval {
                 }
 
                 Preconditions.checkState(newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0);
-                final Item item = newNode.getItems().getItems().get(0);
-                final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItems();
+                final Item item = newNode.getItems().get(0);
+                final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
                 existingOrNewNodeItems.cancelItems(item);
                 // In the merge logic, whether we really insert the node or find an existing node on which to insert items should be seen
                 // as an insertion (so as to avoid keeping that proposed item, see how return value of addProposedItem is used)
@@ -164,10 +172,10 @@ public class ItemsNodeInterval extends NodeInterval {
                     return false;
                 }
 
-                final ItemsInterval insertionNodeItems = ((ItemsNodeInterval) insertionNode).getItems();
+                final ItemsInterval insertionNodeItems = ((ItemsNodeInterval) insertionNode).getItemsInterval();
                 Preconditions.checkState(insertionNodeItems.getItems().size() == 1, "Expected existing node to have only one item");
                 final Item insertionNodeItem = insertionNodeItems.getItems().get(0);
-                final Item newNodeItem = newNode.getItems().getItems().get(0);
+                final Item newNodeItem = newNode.getItems().get(0);
 
                 // If we receive a new proposed that is the same kind as the reversed existing we want to insert it to generate
                 // a piece of repair
@@ -194,13 +202,46 @@ public class ItemsNodeInterval extends NodeInterval {
         final NodeInterval node = findNode(new SearchCallback() {
             @Override
             public boolean isMatch(final NodeInterval curNode) {
-                return ((ItemsNodeInterval) curNode).getItems().containsItem(targetId);
+                return ((ItemsNodeInterval) curNode).getItemsInterval().containsItem(targetId);
             }
         });
         Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
         ((ItemsNodeInterval) node).setAdjustment(amount.negate(), targetId);
     }
 
+    public void jsonSerializeTree(final ObjectMapper mapper, final OutputStream output) throws IOException {
+
+        final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+        generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+        walkTree(new WalkCallback() {
+
+            private int curDepth = 0;
+
+            @Override
+            public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+                final ItemsNodeInterval node = (ItemsNodeInterval) curNode;
+                if (node.isRoot()) {
+                    return;
+                }
+
+                try {
+                    if (curDepth < depth) {
+                        generator.writeStartArray();
+                        curDepth = depth;
+                    } else if (curDepth > depth) {
+                        generator.writeEndArray();
+                        curDepth = depth;
+                    }
+                    generator.writeObject(node);
+                } catch (IOException e) {
+                    throw new RuntimeException("Failed to deserialize tree", e);
+                }
+            }
+        });
+        generator.close();
+    }
+
     protected void setAdjustment(final BigDecimal amount, final UUID linkedId) {
         items.setAdjustment(amount, linkedId);
     }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
index 02365c0..b1d1483 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
@@ -20,10 +20,11 @@ import java.util.List;
 
 import org.joda.time.LocalDate;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
-public abstract class NodeInterval {
+public class NodeInterval {
 
     protected NodeInterval parent;
     protected NodeInterval leftChild;
@@ -54,7 +55,7 @@ public abstract class NodeInterval {
         Preconditions.checkNotNull(callback);
 
         if (leftChild == null) {
-            callback.buildNode(this);
+            callback.onLastNode(this);
             return;
         }
 
@@ -62,7 +63,7 @@ public abstract class NodeInterval {
         NodeInterval curChild = leftChild;
         while (curChild != null) {
             if (curChild.getStart().compareTo(curDate) > 0) {
-                callback.buildMissingInterval(this, curDate, curChild.getStart());
+                callback.onMissingInterval(this, curDate, curChild.getStart());
             }
             curChild.build(callback);
             curDate = curChild.getEnd();
@@ -71,7 +72,7 @@ public abstract class NodeInterval {
 
         // Finally if there is a hole at the end, we build the missing piece from ourself
         if (curDate.compareTo(end) < 0) {
-            callback.buildMissingInterval(this, curDate, end);
+            callback.onMissingInterval(this, curDate, end);
         }
     }
 
@@ -145,6 +146,85 @@ public abstract class NodeInterval {
         }
     }
 
+    /**
+     * Return the first node satisfying the date and match callback.
+     *
+     * @param targetDate target date for possible match nodes whose interval comprises that date
+     * @param callback   custom logic to decide if a given node is a match
+     * @return the found node or null if there is nothing.
+     */
+    public NodeInterval findNode(final LocalDate targetDate, final SearchCallback callback) {
+
+        Preconditions.checkNotNull(callback);
+        Preconditions.checkNotNull(targetDate);
+
+        if (targetDate.compareTo(getStart()) < 0 || targetDate.compareTo(getEnd()) > 0) {
+            return null;
+        }
+
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            if (curChild.getStart().compareTo(targetDate) <= 0 && curChild.getEnd().compareTo(targetDate) >= 0) {
+                if (callback.isMatch(curChild)) {
+                    return curChild;
+                }
+                NodeInterval result = curChild.findNode(targetDate, callback);
+                if (result != null) {
+                    return result;
+                }
+            }
+            curChild = curChild.getRightSibling();
+        }
+        return null;
+    }
+
+    /**
+     * Return the first node satisfying the date and match callback.
+     *
+     * @param callback custom logic to decide if a given node is a match
+     * @return the found node or null if there is nothing.
+     */
+    public NodeInterval findNode(final SearchCallback callback) {
+
+        Preconditions.checkNotNull(callback);
+        if (callback.isMatch(this)) {
+            return this;
+        }
+
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            final NodeInterval result = curChild.findNode(callback);
+            if (result != null) {
+                return result;
+            }
+            curChild = curChild.getRightSibling();
+        }
+        return null;
+    }
+
+    /**
+     * Walk the tree (depth first search) and invoke callback for each node.
+     *
+     * @param callback
+     */
+    public void walkTree(WalkCallback callback) {
+        Preconditions.checkNotNull(callback);
+        walkTreeWithDepth(callback, 0);
+    }
+
+    private void walkTreeWithDepth(WalkCallback callback, int depth) {
+
+        Preconditions.checkNotNull(callback);
+        callback.onCurrentNode(depth, this, parent);
+
+        NodeInterval curChild = leftChild;
+        while (curChild != null) {
+            curChild.walkTreeWithDepth(callback, (depth + 1));
+            curChild = curChild.getRightSibling();
+        }
+    }
+
+
     public boolean isItemContained(final NodeInterval newNode) {
         return (newNode.getStart().compareTo(start) >= 0 &&
                 newNode.getStart().compareTo(end) <= 0 &&
@@ -159,6 +239,7 @@ public abstract class NodeInterval {
                  newNode.getEnd().compareTo(end) > 0));
     }
 
+    @JsonIgnore
     public boolean isRoot() {
         return parent == null;
     }
@@ -171,18 +252,22 @@ public abstract class NodeInterval {
         return end;
     }
 
+    @JsonIgnore
     public NodeInterval getParent() {
         return parent;
     }
 
+    @JsonIgnore
     public NodeInterval getLeftChild() {
         return leftChild;
     }
 
+    @JsonIgnore
     public NodeInterval getRightSibling() {
         return rightSibling;
     }
 
+    @JsonIgnore
     public int getNbChildren() {
         int result = 0;
         NodeInterval curChild = leftChild;
@@ -247,68 +332,22 @@ public abstract class NodeInterval {
     }
 
     /**
-     * Return the first node satisfying the date and match callback.
-     *
-     * @param targetDate target date for possible match nodes whose interval comprises that date
-     * @param callback   custom logic to decide if a given node is a match
-     * @return the found node or null if there is nothing.
+     * Provides callback for walking the tree.
      */
-    public NodeInterval findNode(final LocalDate targetDate, final SearchCallback callback) {
-
-        Preconditions.checkNotNull(callback);
-        Preconditions.checkNotNull(targetDate);
-
-        if (targetDate.compareTo(getStart()) < 0 || targetDate.compareTo(getEnd()) > 0) {
-            return null;
-        }
-
-        NodeInterval curChild = leftChild;
-        while (curChild != null) {
-            if (curChild.getStart().compareTo(targetDate) <= 0 && curChild.getEnd().compareTo(targetDate) >= 0) {
-                if (callback.isMatch(curChild)) {
-                    return curChild;
-                }
-                NodeInterval result = curChild.findNode(targetDate, callback);
-                if (result != null) {
-                    return result;
-                }
-            }
-            curChild = curChild.getRightSibling();
-        }
-        return null;
-    }
-
-    /**
-     * Return the first node satisfying the date and match callback.
-     *
-     * @param callback custom logic to decide if a given node is a match
-     * @return the found node or null if there is nothing.
-     */
-    public NodeInterval findNode(final SearchCallback callback) {
-
-        System.out.println("ENTERING [" + start + " - " + end + "]");
-
-        Preconditions.checkNotNull(callback);
-        if (/*!isRoot() && */callback.isMatch(this)) {
-            return this;
-        }
-
-        NodeInterval curChild = leftChild;
-        while (curChild != null) {
-            final NodeInterval result = curChild.findNode(callback);
-            if (result != null) {
-                return result;
-            }
-            curChild = curChild.getRightSibling();
-        }
-        return null;
+    public interface WalkCallback {
+        public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent);
     }
 
     /**
      * Provides custom logic for the search.
      */
     public interface SearchCallback {
-
+        /**
+         * Custom logic to decide which node to return.
+         *
+         * @param curNode found node
+         * @return evaluates whether this is the node that should be returned
+         */
         boolean isMatch(NodeInterval curNode);
     }
 
@@ -324,14 +363,14 @@ public abstract class NodeInterval {
          * @param startDate startDate of the new interval to build
          * @param endDate   endDate of the new interval to build
          */
-        public void buildMissingInterval(NodeInterval curNode, LocalDate startDate, LocalDate endDate);
+        public void onMissingInterval(NodeInterval curNode, LocalDate startDate, LocalDate endDate);
 
         /**
          * Called when we hit a node with no children
          *
          * @param curNode current node
          */
-        public void buildNode(NodeInterval curNode);
+        public void onLastNode(NodeInterval curNode);
     }
 
     /**
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 b04f343..8aa8312 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
@@ -28,6 +28,7 @@ import org.joda.time.LocalDate;
 import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.tree.Item.ItemAction;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
@@ -273,4 +274,8 @@ public class SubscriptionItemTree {
         return result;
     }
 
+    @VisibleForTesting
+    ItemsNodeInterval getRoot() {
+        return root;
+    }
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
index 4cc9d0d..9512591 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
@@ -16,29 +16,24 @@
 
 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 org.testng.Assert;
 import org.testng.annotations.Test;
 
-import com.ning.billing.catalog.api.Currency;
-import com.ning.billing.invoice.api.InvoiceItem;
-import com.ning.billing.invoice.model.RecurringInvoiceItem;
-import com.ning.billing.invoice.tree.Item.ItemAction;
 import com.ning.billing.invoice.tree.NodeInterval.AddNodeCallback;
 import com.ning.billing.invoice.tree.NodeInterval.BuildNodeCallback;
 import com.ning.billing.invoice.tree.NodeInterval.SearchCallback;
+import com.ning.billing.invoice.tree.NodeInterval.WalkCallback;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
 public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
 
-
     private AddNodeCallback CALLBACK = new DummyAddNodeCallback();
 
     public class DummyNodeInterval extends NodeInterval {
@@ -103,7 +98,6 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
         checkNode(thirdChildLevel2, 0, thirdChildLevel1, null, null);
     }
 
-
     @Test(groups = "fast")
     public void testAddExistingItemWithRebalance() {
         final DummyNodeInterval root = new DummyNodeInterval();
@@ -135,9 +129,8 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
         checkNode(thirdChildLevel2, 0, thirdChildLevel1, null, null);
     }
 
-
     @Test(groups = "fast")
-      public void testBuild() {
+    public void testBuild() {
         final DummyNodeInterval root = new DummyNodeInterval();
 
         final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
@@ -162,11 +155,12 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
         // Just build the missing pieces.
         root.build(new BuildNodeCallback() {
             @Override
-            public void buildMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+            public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
                 output.add(createNodeInterval(startDate, endDate));
             }
+
             @Override
-            public void buildNode(final NodeInterval curNode) {
+            public void onLastNode(final NodeInterval curNode) {
                 // Nothing
             }
         });
@@ -182,7 +176,6 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
         checkInterval(output.get(1), expected.get(1));
     }
 
-
     @Test(groups = "fast")
     public void testSearch() {
         final DummyNodeInterval root = new DummyNodeInterval();
@@ -234,6 +227,66 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
         assertNull(nullSearch);
     }
 
+    @Test(groups = "fast")
+    public void testWalkTree() {
+        final DummyNodeInterval root = new DummyNodeInterval();
+
+        final DummyNodeInterval firstChildLevel0 = createNodeInterval("2014-01-01", "2014-02-01");
+        root.addNode(firstChildLevel0, CALLBACK);
+
+        final DummyNodeInterval secondChildLevel0 = createNodeInterval("2014-02-01", "2014-03-01");
+        root.addNode(secondChildLevel0, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+        final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+        final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+        root.addNode(firstChildLevel1, CALLBACK);
+        root.addNode(secondChildLevel1, CALLBACK);
+        root.addNode(thirdChildLevel1, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+        final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-05");
+        final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+        root.addNode(firstChildLevel2, CALLBACK);
+        root.addNode(secondChildLevel2, CALLBACK);
+        root.addNode(thirdChildLevel2, CALLBACK);
+
+        final DummyNodeInterval firstChildLevel3 = createNodeInterval("2014-01-01", "2014-01-02");
+        final DummyNodeInterval secondChildLevel3 = createNodeInterval("2014-01-03", "2014-01-04");
+        root.addNode(firstChildLevel3, CALLBACK);
+        root.addNode(secondChildLevel3, CALLBACK);
+
+        final List<NodeInterval> expected = new LinkedList<NodeInterval>();
+        expected.add(root);
+        expected.add(firstChildLevel0);
+        expected.add(firstChildLevel1);
+        expected.add(firstChildLevel2);
+        expected.add(firstChildLevel3);
+        expected.add(secondChildLevel2);
+        expected.add(secondChildLevel3);
+        expected.add(secondChildLevel1);
+        expected.add(thirdChildLevel1);
+        expected.add(thirdChildLevel2);
+        expected.add(secondChildLevel0);
+
+        final List<NodeInterval> result = new LinkedList<NodeInterval>();
+        root.walkTree(new WalkCallback() {
+            @Override
+            public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+                result.add(curNode);
+            }
+        });
+
+        assertEquals(result.size(), expected.size());
+        for (int i = 0; i < result.size(); i++) {
+            if (i == 0) {
+                assertTrue(result.get(0).isRoot());
+                checkInterval(result.get(0), createNodeInterval("2014-01-01", "2014-03-01"));
+            } else {
+                checkInterval(result.get(i), expected.get(i));
+            }
+        }
+    }
 
     private void checkInterval(final NodeInterval real, final NodeInterval expected) {
         assertEquals(real.getStart(), expected.getStart());
@@ -247,6 +300,7 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB  */ {
         assertEquals(node.getLeftChild(), expectedLeftChild);
         assertEquals(node.getLeftChild(), expectedLeftChild);
     }
+
     private DummyNodeInterval createNodeInterval(final LocalDate startDate, final LocalDate endDate) {
         return new DummyNodeInterval(null, startDate, endDate);
     }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
index 5b8428a..b3fd61e 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
@@ -16,10 +16,14 @@
 
 package com.ning.billing.invoice.tree;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.testng.annotations.Test;
 
@@ -29,8 +33,11 @@ import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
 import com.ning.billing.invoice.model.ItemAdjInvoiceItem;
 import com.ning.billing.invoice.model.RecurringInvoiceItem;
 import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+import com.ning.billing.util.jackson.ObjectMapper;
 
+import com.apple.jobjc.appkit.NSToolbarItemGroup;
 import com.google.common.collect.Lists;
+import junit.framework.Assert;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
@@ -781,6 +788,41 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB  */ {
         verifyResult(tree.getView(), expectedResult);
     }
 
+    @Test(groups = "fast")
+    public void verifyJson() {
+
+        SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+        final UUID id1 = UUID.fromString("e8ba6ce7-9bd4-417d-af53-70951ecaa99f");
+        final InvoiceItem yearly1 = new RecurringInvoiceItem(id1, new DateTime(), invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, new LocalDate("2014-01-01"), new LocalDate("2015-01-01"), BigDecimal.TEN, BigDecimal.TEN, currency);
+        tree.addItem(yearly1);
+
+        final UUID id2 = UUID.fromString("48db1317-9a6e-4666-bcc5-fc7d3d0defc8");
+        final InvoiceItem newItem = new RecurringInvoiceItem(id2, new DateTime(), invoiceId, accountId, bundleId, subscriptionId, "other-plan", "other-plan", new LocalDate("2014-08-01"), new LocalDate("2015-01-01"), BigDecimal.ONE, BigDecimal.ONE, currency);
+        tree.addItem(newItem);
+
+        final UUID id3 = UUID.fromString("02ec57f5-2723-478b-86ba-ebeaedacb9db");
+        final InvoiceItem repair = new RepairAdjInvoiceItem(id3, new DateTime(), invoiceId, accountId, new LocalDate("2014-08-01"), new LocalDate("2015-01-01"), BigDecimal.TEN.negate(), currency, yearly1.getId());
+        tree.addItem(repair);
+
+
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            tree.getRoot().jsonSerializeTree(new ObjectMapper(), outputStream);
+
+            final  String json = outputStream.toString("UTF-8");
+            System.out.println(json);
+
+            final String expectedJson = "[{\"start\":\"2014-01-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"startDate\":\"2014-01-01\",\"endDate\":\"2015-01-01\",\"amount\":10,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"}]},[{\"start\":\"2014-08-01\",\"end\":\"2015-01-01\",\"items\":[{\"id\":\"48db1317-9a6e-4666-bcc5-fc7d3d0defc8\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":1,\"currency\":\"USD\",\"linkedId\":null,\"action\":\"ADD\"},{\"id\":\"02ec57f5-2723-478b-86ba-ebeaedacb9db\",\"startDate\":\"2014-08-01\",\"endDate\":\"2015-01-01\",\"amount\":10,\"currency\":\"USD\",\"linkedId\":\"e8ba6ce7-9bd4-417d-af53-70951ecaa99f\",\"action\":\"CANCEL\"}]}]]";
+
+            assertEquals(json, expectedJson);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+            Assert.fail(e.getMessage());
+        }
+
+    }
+
     private void verifyResult(final List<InvoiceItem> result, final List<InvoiceItem> expectedResult) {
         assertEquals(result.size(), expectedResult.size());
         for (int i = 0; i < expectedResult.size(); i++) {