killbill-aplcache

Changes

Details

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 51bff88..a338da9 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -211,7 +211,7 @@ public class InvoiceDispatcher {
             final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context);
             final DryRunArguments dryRunArguments = dryRunForNotification ? TARGET_DATE_DRY_RUN_ARGUMENTS : null;
 
-            return processAccount(accountId, targetDate, dryRunArguments, context);
+            return processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, context);
         } catch (final SubscriptionBaseApiException e) {
             log.warn("Failed handling SubscriptionBase change.",
                       new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
@@ -219,11 +219,10 @@ public class InvoiceDispatcher {
         }
     }
 
-    public Invoice processAccount(final UUID accountId,
-                                  @Nullable final LocalDate targetDate,
-                                  @Nullable final DryRunArguments dryRunArguments,
-                                  final InternalCallContext context) throws InvoiceApiException {
-        // Note that all API calls (dryRun or not) will bypass this (see processAccount below)
+    public Invoice processAccountFromNotificationOrBusEvent(final UUID accountId,
+                                                            @Nullable final LocalDate targetDate,
+                                                            @Nullable final DryRunArguments dryRunArguments,
+                                                            final InternalCallContext context) throws InvoiceApiException {
         if (!invoiceConfig.isInvoicingSystemEnabled(context)) {
             log.warn("Invoicing system is off, parking accountId='{}'", accountId);
             parkAccount(accountId, context);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index 2e103c4..4f20b6d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -95,7 +95,7 @@ public class InvoiceListener {
         try {
             final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
             final UUID accountId = accountApi.getByRecordId(event.getSearchKey1(), context);
-            dispatcher.processAccount(accountId, null, null, context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, null, null, context);
         } catch (InvoiceApiException e) {
             log.warn("Unable to process event {}", event, e);
         } catch (AccountApiException e) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
index 3d20a84..ab9b982 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
@@ -61,7 +61,7 @@ public class InvoiceTagHandler {
 
     private void processUnpaid_AUTO_INVOICING_OFF_invoices(final UUID accountId, final InternalCallContext context) {
         try {
-            dispatcher.processAccount(accountId, null, null, context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, null, null, context);
         } catch (final InvoiceApiException e) {
             log.warn("Failed to process tag removal AUTO_INVOICING_OFF for accountId='{}'", accountId, e);
         }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
index 515b88a..43e5ea5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -134,37 +134,37 @@ public class ItemsNodeInterval extends NodeInterval {
         return addNode(newNode, new AddNodeCallback() {
             @Override
             public boolean onExistingNode(final NodeInterval existingNode) {
-                if (!shouldInsertNode(existingNode)) {
+                // If we receive a new proposed that is the same kind as the reversed existing (current node),
+                // we match existing and proposed. If not, we keep the proposed item as-is outside of the tree.
+                if (isSameKind((ItemsNodeInterval) existingNode)) {
+                    final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
+                    existingOrNewNodeItems.cancelItems(item);
+                    return true;
+                } else {
                     return false;
                 }
-
-                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)
-                return true;
             }
 
             @Override
             public boolean shouldInsertNode(final NodeInterval insertionNode) {
-                // The root level is solely for the reversed existing items. If there is a new node that does not fit below the level
-                // of reversed existing items, we want to return false and keep it outside of the tree. It should be 'kept as such'.
+                // At this stage, we're currently merging a proposed item that does not fit any of the existing intervals.
+                // If this new node is about to be inserted at the root level, this means the proposed item overlaps any
+                // existing item. We keep these as-is, outside of the tree: they will become part of the resulting list.
                 if (insertionNode.isRoot()) {
                     return false;
                 }
 
-                final List<Item> insertionNodeItems = ((ItemsNodeInterval) insertionNode).getItems();
+                // If we receive a new proposed that is the same kind as the reversed existing (parent node),
+                // we want to insert it to generate a piece of repair (see SubscriptionItemTree#buildForMerge).
+                // If not, we keep the proposed item as-is outside of the tree.
+                return isSameKind((ItemsNodeInterval) insertionNode);
+            }
+
+            private boolean isSameKind(final ItemsNodeInterval insertionNode) {
+                final List<Item> insertionNodeItems = insertionNode.getItems();
                 Preconditions.checkState(insertionNodeItems.size() == 1, "Expected existing node to have only one item");
                 final Item insertionNodeItem = insertionNodeItems.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
-                if (insertionNodeItem.isSameKind(item)) {
-                    return true;
-                } else {
-                    // If not, then keep the proposed outside of the tree.
-                    return false;
-                }
+                return insertionNodeItem.isSameKind(item);
             }
         });
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index 9c90462..df593b8 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -160,6 +160,7 @@ public class SubscriptionItemTree {
 
         switch (invoiceItem.getInvoiceItemType()) {
             case RECURRING:
+                // merged means we've either matched the proposed to an existing, or triggered a repair
                 final boolean merged = root.addProposedItem(new ItemsNodeInterval(root, new Item(invoiceItem, targetInvoiceId, ItemAction.ADD)));
                 if (!merged) {
                     items.add(new Item(invoiceItem, targetInvoiceId, ItemAction.ADD));
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/TreePrinter.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/TreePrinter.java
new file mode 100644
index 0000000..72a5bdd
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/TreePrinter.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.tree;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TreePrinter {
+
+    public static String print(final ItemsNodeInterval root) {
+        return print(buildCoordinates(root));
+    }
+
+    private static String print(final SortedMap<XY, ItemsNodeInterval> tree) {
+        // Make left most node start at X=0
+        translate(tree);
+
+        final AtomicInteger totalOrdering = new AtomicInteger(64);
+        final Map<String, ItemsNodeInterval> legend = new LinkedHashMap<String, ItemsNodeInterval>();
+
+        final List<StringBuilder> builders = new LinkedList<StringBuilder>();
+        for (int level = 0; level >= maxOffset(tree).Y; level--) {
+            builders.add(new StringBuilder());
+            // Draw edges for that level
+            drawLevel(true, level, tree, builders, totalOrdering, legend);
+
+            // Draw nodes for that level
+            builders.add(new StringBuilder());
+            drawLevel(false, level, tree, builders, totalOrdering, legend);
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        for (final StringBuilder levelBuilder : builders) {
+            builder.append(levelBuilder.toString());
+        }
+
+        builder.append("\n");
+        for (final String key : legend.keySet()) {
+            builder.append(key).append(": ");
+            appendNodeDetails(legend.get(key), builder);
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
+
+    private static void drawLevel(final boolean drawEdges,
+                                  final int level,
+                                  final SortedMap<XY, ItemsNodeInterval> tree,
+                                  final List<StringBuilder> builders,
+                                  final AtomicInteger totalOrdering,
+                                  final Map<String, ItemsNodeInterval> legend) {
+        if (drawEdges && level == 0) {
+            // Nothing to do for root
+            return;
+        }
+
+        final StringBuilder builder = builders.get(builders.size() - 1);
+
+        int posX = 0;
+        boolean sibling;
+        for (final XY levelXY : tree.keySet()) {
+            if (levelXY.Y > level) {
+                // Sorted - we haven't reached that level yet
+                continue;
+            } else if (levelXY.Y < level) {
+                // Sorted - we are done for that level
+                break;
+            }
+
+            sibling = levelXY.parent == null;
+            while (posX < levelXY.X) {
+                if (drawEdges || !sibling || level == 0) {
+                    builder.append(" ");
+                } else {
+                    // Link siblings
+                    builder.append("-");
+                }
+                posX++;
+            }
+
+            if (drawEdges) {
+                if (sibling) {
+                    builder.append(" ");
+                } else {
+                    builder.append("/");
+                }
+            } else {
+                if (sibling && level != 0) {
+                    builder.append("");
+                }
+
+                // Print this node
+                String node = Character.toString((char) totalOrdering.incrementAndGet());
+                if (sibling && level != 0) {
+                    node = node.toLowerCase();
+                }
+                legend.put(node, tree.get(levelXY));
+                builder.append(node);
+            }
+            posX++;
+        }
+        builder.append("\n");
+    }
+
+    private static void appendNodeDetails(final ItemsNodeInterval interval, final StringBuilder builder) {
+        builder.append("[")
+               .append(interval.getStart())
+               .append(",")
+               .append(interval.getEnd())
+               .append("]");
+
+        if (interval.getItems().isEmpty()) {
+            return;
+        }
+
+        builder.append("(");
+        final List<Item> items = interval.getItems();
+        for (int i = 0; i < items.size(); i++) {
+            final Item item = items.get(i);
+            if (i > 0) {
+                builder.append(",");
+            }
+            builder.append(item.getAction().name().charAt(0));
+        }
+        builder.append(")");
+    }
+
+    public static SortedMap<XY, ItemsNodeInterval> buildCoordinates(final ItemsNodeInterval root) {
+        final XY reference = new XY(0, 0);
+
+        final SortedMap<XY, ItemsNodeInterval> result = new TreeMap<XY, ItemsNodeInterval>();
+        result.put(reference, root);
+        result.putAll(buildCoordinates(root, reference));
+
+        return result;
+    }
+
+    public static Map<XY, ItemsNodeInterval> buildCoordinates(final ItemsNodeInterval root, final XY initialCoords) {
+        final Map<XY, ItemsNodeInterval> result = new HashMap<XY, ItemsNodeInterval>();
+        if (root == null) {
+            return result;
+        }
+
+        // Compute the coordinate of the left most child
+        ItemsNodeInterval curChild = (ItemsNodeInterval) root.getLeftChild();
+        if (curChild == null) {
+            return result;
+        }
+
+        XY curXY = leftChildXY(initialCoords);
+        result.put(curXY, curChild);
+        // Compute the coordinates of the tree below that child
+        result.putAll(buildCoordinates(curChild, curXY));
+
+        curChild = (ItemsNodeInterval) curChild.getRightSibling();
+        while (curChild != null) {
+            curXY = rightSiblingXY(curXY);
+
+            // Compute the coordinates of the tree below that child
+            final Map<XY, ItemsNodeInterval> subtree = buildCoordinates(curChild, curXY);
+            final XY offset = translate(subtree);
+            translate(offset, curXY);
+            result.put(curXY, curChild);
+            result.putAll(subtree);
+
+            curChild = (ItemsNodeInterval) curChild.getRightSibling();
+        }
+
+        return result;
+    }
+
+    private static XY translate(final Map<XY, ItemsNodeInterval> subtree) {
+        final XY offset = maxOffset(subtree);
+        for (final XY xy : subtree.keySet()) {
+            translate(offset, xy);
+        }
+        return offset;
+    }
+
+    private static void translate(final XY offset, final XY xy) {
+        xy.X = xy.X - offset.X;
+    }
+
+    private static XY maxOffset(final Map<XY, ItemsNodeInterval> tree) {
+        final XY res = new XY(0, 0);
+        for (final XY xy : tree.keySet()) {
+            if (xy.X < res.X) {
+                res.X = xy.X;
+            }
+            if (xy.Y < res.Y) {
+                res.Y = xy.Y;
+            }
+        }
+        return res;
+    }
+
+    private static XY leftChildXY(final XY parent) {
+        return new XY(parent.X - 1, parent.Y - 1, parent);
+    }
+
+    private static XY rightSiblingXY(final XY leftSibling) {
+        return new XY(leftSibling.X + 1, leftSibling.Y);
+    }
+
+    static class XY implements Comparable<XY> {
+
+        int X;
+        int Y;
+        XY parent;
+
+        public XY(final int x, final int y) {
+            this(x, y, null);
+        }
+
+        public XY(final int x, final int y, final XY parent) {
+            X = x;
+            Y = y;
+            this.parent = parent;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder("(");
+            sb.append(X);
+            sb.append(",").append(Y);
+            sb.append(')');
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final XY xy = (XY) o;
+
+            if (X != xy.X) {
+                return false;
+            }
+            return Y == xy.Y;
+
+        }
+
+        @Override
+        public int hashCode() {
+            int result = X;
+            result = 31 * result + Y;
+            return result;
+        }
+
+        @Override
+        public int compareTo(final XY o) {
+            return Y == o.Y ? X < o.X ? -1 : X == o.X ? 0 : 1 : Y < o.Y ? 1 : -1;
+        }
+    }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index 36a536b..a83301d 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -104,21 +104,21 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
                                                                    internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    null, invoiceConfig, clock, parkedAccountsManager);
 
-        Invoice invoice = dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
+        Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
         Assert.assertNotNull(invoice);
 
         List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(context);
         Assert.assertEquals(invoices.size(), 0);
 
         // Try it again to double check
-        invoice = dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
+        invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
         Assert.assertNotNull(invoice);
 
         invoices = invoiceDao.getInvoicesByAccount(context);
         Assert.assertEquals(invoices.size(), 0);
 
         // This time no dry run
-        invoice = dispatcher.processAccount(accountId, target, null, context);
+        invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, context);
         Assert.assertNotNull(invoice);
 
         invoices = invoiceDao.getInvoicesByAccount(context);
@@ -196,7 +196,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
                                  context);
 
         try {
-            dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
             Assert.fail();
         } catch (final InvoiceApiException e) {
             Assert.assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
@@ -207,7 +207,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertTrue(tagUserApi.getTagsForAccount(accountId, true, callContext).isEmpty());
 
         try {
-            dispatcher.processAccount(accountId, target, null, context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, context);
             Assert.fail();
         } catch (final InvoiceApiException e) {
             Assert.assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
@@ -220,7 +220,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertEquals(tags.get(0).getTagDefinitionId(), SystemTags.PARK_TAG_DEFINITION_ID);
 
         // isApiCall=false
-        final Invoice nullInvoice1 = dispatcher.processAccount(accountId, target, null, context);
+        final Invoice nullInvoice1 = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, context);
         Assert.assertNull(nullInvoice1);
 
         // No dry-run and isApiCall=true
@@ -246,7 +246,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         });
 
         // Dry-run and isApiCall=false: still parked
-        final Invoice nullInvoice2 = dispatcher.processAccount(accountId, target, new DryRunFutureDateArguments(), context);
+        final Invoice nullInvoice2 = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
         Assert.assertNull(nullInvoice2);
 
         // Dry-run and isApiCall=true: call goes through
@@ -297,7 +297,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
                                                                    internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    null, invoiceConfig, clock, parkedAccountsManager);
 
-        final Invoice invoice = dispatcher.processAccount(account.getId(), new LocalDate("2012-07-30"), null, context);
+        final Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), new LocalDate("2012-07-30"), null, context);
         Assert.assertNotNull(invoice);
 
         final List<InvoiceItem> invoiceItems = invoice.getInvoiceItems();
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index a7ec5de..38dd1af 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -33,7 +33,6 @@ import org.killbill.billing.GuicyKillbillTestSuite;
 import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
-import org.killbill.billing.account.api.AccountData;
 import org.killbill.billing.account.api.AccountInternalApi;
 import org.killbill.billing.account.api.AccountUserApi;
 import org.killbill.billing.account.api.ImmutableAccountInternalApi;
@@ -217,7 +216,7 @@ public class TestInvoiceHelper {
                                                                    invoiceDao, internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    null, invoiceConfig, clock, parkedAccountsManager);
 
-        Invoice invoice = dispatcher.processAccount(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
+        Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
         Assert.assertNotNull(invoice);
 
         final InternalCallContext context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
@@ -225,7 +224,7 @@ public class TestInvoiceHelper {
         List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(context);
         Assert.assertEquals(invoices.size(), 0);
 
-        invoice = dispatcher.processAccount(account.getId(), targetDate, null, context);
+        invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), targetDate, null, context);
         Assert.assertNotNull(invoice);
 
         invoices = invoiceDao.getInvoicesByAccount(context);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 9f592b3..26f4170 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -63,6 +63,58 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
     private final String phaseName = "my-phase";
     private final Currency currency = Currency.USD;
 
+    @Test(groups = "fast", description = "Complex multi-level tree, mostly used to test the tree printer")
+    public void testMultipleLevels() throws Exception {
+        final LocalDate startDate = new LocalDate(2014, 1, 1);
+        final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+        final LocalDate startRepairDate1 = new LocalDate(2014, 1, 10);
+        final LocalDate endRepairDate1 = new LocalDate(2014, 1, 15);
+
+        final LocalDate startRepairDate11 = new LocalDate(2014, 1, 10);
+        final LocalDate endRepairDate12 = new LocalDate(2014, 1, 12);
+
+        final LocalDate startRepairDate2 = new LocalDate(2014, 1, 20);
+        final LocalDate endRepairDate2 = new LocalDate(2014, 1, 25);
+
+        final LocalDate startRepairDate21 = new LocalDate(2014, 1, 22);
+        final LocalDate endRepairDate22 = new LocalDate(2014, 1, 23);
+
+        final BigDecimal rate = BigDecimal.TEN;
+        final BigDecimal amount = rate;
+
+        final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, rate, currency);
+
+        final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate1, endRepairDate1, amount, rate, currency);
+        final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate1, endRepairDate1, amount.negate(), currency, initial.getId());
+
+        final InvoiceItem newItem11 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate11, endRepairDate12, amount, rate, currency);
+        final InvoiceItem repair12 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate11, endRepairDate12, amount.negate(), currency, newItem1.getId());
+
+        final InvoiceItem newItem2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate2, endRepairDate2, amount, rate, currency);
+        final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate2, endRepairDate2, amount.negate(), currency, initial.getId());
+
+        final InvoiceItem newItem21 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startRepairDate21, endRepairDate22, amount, rate, currency);
+        final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, startRepairDate21, endRepairDate22, amount.negate(), currency, newItem2.getId());
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(initial);
+        tree.addItem(newItem1);
+        tree.addItem(repair1);
+        tree.addItem(newItem11);
+        tree.addItem(repair12);
+        tree.addItem(newItem2);
+        tree.addItem(repair2);
+        tree.addItem(newItem21);
+        tree.addItem(repair22);
+
+        tree.build();
+        //printTree(tree);
+
+        tree.flatten(true);
+        //printTree(tree);
+    }
+
     @Test(groups = "fast")
     public void testWithExistingSplitRecurring() {
 
@@ -1441,9 +1493,13 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
         Assert.assertEquals(previousExistingSize, 3);
     }
 
-    private void printTree(final SubscriptionItemTree tree) throws IOException {
+    private void printTreeJSON(final SubscriptionItemTree tree) throws IOException {
         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         tree.getRoot().jsonSerializeTree(OBJECT_MAPPER, outputStream);
         System.out.println(outputStream.toString("UTF-8"));
     }
+
+    private void printTree(final SubscriptionItemTree tree) throws IOException {
+        System.out.println(TreePrinter.print(tree.getRoot()));
+    }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestTreePrinter.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestTreePrinter.java
new file mode 100644
index 0000000..6159bdc
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestTreePrinter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.Map;
+import java.util.SortedMap;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.tree.Item.ItemAction;
+import org.killbill.billing.invoice.tree.TreePrinter.XY;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class TestTreePrinter extends InvoiceTestSuiteNoDB {
+
+    private ItemsNodeInterval root;
+    private ItemsNodeInterval node11;
+    private ItemsNodeInterval node21;
+    private ItemsNodeInterval node22;
+    private ItemsNodeInterval node12;
+    private ItemsNodeInterval node23;
+    private ItemsNodeInterval node31;
+
+    @BeforeMethod(groups = "fast")
+    public void setUp() throws Exception {
+        final InvoiceItem item = Mockito.mock(InvoiceItem.class);
+        Mockito.when(item.getAmount()).thenReturn(BigDecimal.ZERO);
+
+        root = new ItemsNodeInterval(null, new Item(item, new LocalDate(2016, 1, 1), new LocalDate(2016, 2, 1), null, ItemAction.ADD));
+
+        node11 = new ItemsNodeInterval(root, new Item(item, new LocalDate(2016, 1, 10), new LocalDate(2016, 1, 15), null, ItemAction.ADD));
+        node21 = new ItemsNodeInterval(node11, new Item(item, new LocalDate(2016, 1, 10), new LocalDate(2016, 1, 12), null, ItemAction.ADD));
+        node22 = new ItemsNodeInterval(node11, new Item(item, new LocalDate(2016, 1, 14), new LocalDate(2016, 1, 15), null, ItemAction.ADD));
+
+        node12 = new ItemsNodeInterval(root, new Item(item, new LocalDate(2016, 1, 20), new LocalDate(2016, 1, 25), null, ItemAction.ADD));
+        node23 = new ItemsNodeInterval(node12, new Item(item, new LocalDate(2016, 1, 22), new LocalDate(2016, 1, 24), null, ItemAction.ADD));
+        node31 = new ItemsNodeInterval(node23, new Item(item, new LocalDate(2016, 1, 22), new LocalDate(2016, 1, 23), null, ItemAction.ADD));
+    }
+
+    @Test(groups = "fast")
+    public void testSimpleTranslate() throws Exception {
+        root.leftChild = node11;
+        node11.rightSibling = node12;
+        node12.leftChild = node23;
+
+        final SortedMap<XY, ItemsNodeInterval> coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 4);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(1, -1)), node12);
+        Assert.assertEquals(coords.get(new XY(0, -2)), node23);
+        //System.out.println(TreePrinter.print(root));
+    }
+
+    @Test(groups = "fast")
+    public void testComplexMultiLevelTree() throws Exception {
+        Map<XY, ItemsNodeInterval> coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 1);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+
+        root.leftChild = node11;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 2);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+
+        node11.rightSibling = node12;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 3);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(0, -1)), node12);
+
+        node11.leftChild = node21;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 4);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(0, -1)), node12);
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+
+        node21.rightSibling = node22;
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 5);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(0, -1)), node12);
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+        Assert.assertEquals(coords.get(new XY(-1, -2)), node22);
+
+        node12.leftChild = node23;
+        //System.out.println(TreePrinter.print(root));
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 6);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(1, -1)), node12); // (0,-1) before translation
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+        Assert.assertEquals(coords.get(new XY(-1, -2)), node22);
+        Assert.assertEquals(coords.get(new XY(0, -2)), node23); // (-1,-2) before translation
+
+        node23.leftChild = node31;
+        //System.out.println(TreePrinter.print(root));
+
+        coords = TreePrinter.buildCoordinates(root);
+        Assert.assertEquals(coords.size(), 7);
+        Assert.assertEquals(coords.get(new XY(0, 0)), root);
+        Assert.assertEquals(coords.get(new XY(-1, -1)), node11);
+        Assert.assertEquals(coords.get(new XY(2, -1)), node12); // (1,-1) before translation
+        Assert.assertEquals(coords.get(new XY(-2, -2)), node21);
+        Assert.assertEquals(coords.get(new XY(-1, -2)), node22);
+        Assert.assertEquals(coords.get(new XY(1, -2)), node23); // (0,-2) before translation
+        Assert.assertEquals(coords.get(new XY(0, -3)), node31); // (-1,-3) before translation
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
index 037ab58..066353b 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
@@ -49,6 +49,8 @@ import com.google.common.collect.Iterables;
 
 public class PhasePriceOverrideJson {
 
+
+    private final String planName;
     private final String phaseName;
     private final String phaseType;
     private final BigDecimal fixedPrice;
@@ -56,11 +58,13 @@ public class PhasePriceOverrideJson {
     private final List<UsagePriceOverrideJson> usagePriceOverrides;
 
     @JsonCreator
-    public PhasePriceOverrideJson(@JsonProperty("phaseName") final String phaseName,
+    public PhasePriceOverrideJson(@JsonProperty("planName") final String planName,
+                                  @JsonProperty("phaseName") final String phaseName,
                                   @JsonProperty("phaseType") final String phaseType,
                                   @Nullable @JsonProperty("fixedPrice") final BigDecimal fixedPrice,
                                   @Nullable @JsonProperty("recurringPrice") final BigDecimal recurringPrice,
                                   @Nullable @JsonProperty("usageOverrides") final List<UsagePriceOverrideJson> usagePriceOverrides) {
+        this.planName = planName;
         this.phaseName = phaseName;
         this.phaseType = phaseType;
         this.fixedPrice = fixedPrice;
@@ -68,12 +72,14 @@ public class PhasePriceOverrideJson {
         this.usagePriceOverrides = usagePriceOverrides;
     }
 
-    public PhasePriceOverrideJson(final String phaseName,
+    public PhasePriceOverrideJson(final String planName,
+                                  final String phaseName,
                                   final String phaseType,
                                   final BigDecimal fixedPrice,
                                   final BigDecimal recurringPrice,
                                   final Usage[] usagePriceOverrides,
                                   final Currency currency) throws CatalogApiException {
+        this.planName = planName;
         this.phaseName = phaseName;
         this.phaseType = phaseType;
         this.fixedPrice = fixedPrice;
@@ -100,6 +106,10 @@ public class PhasePriceOverrideJson {
 
     }
 
+    public String getPlanName() {
+        return planName;
+    }
+
     public BigDecimal getFixedPrice() {
         return fixedPrice;
     }
@@ -124,6 +134,7 @@ public class PhasePriceOverrideJson {
     @Override
     public String toString() {
         return "PhasePriceOverrideJson{" +
+               "planName='" + planName + '\'' +
                "phaseName='" + phaseName + '\'' +
                "phaseType='" + phaseType + '\'' +
                ", fixedPrice=" + fixedPrice +
@@ -147,6 +158,9 @@ public class PhasePriceOverrideJson {
         if (fixedPrice != null ? fixedPrice.compareTo(that.fixedPrice) != 0 : that.fixedPrice != null) {
             return false;
         }
+        if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+            return false;
+        }
         if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
             return false;
         }
@@ -164,7 +178,8 @@ public class PhasePriceOverrideJson {
 
     @Override
     public int hashCode() {
-        int result = phaseName != null ? phaseName.hashCode() : 0;
+        int result = planName != null ? planName.hashCode() : 0;
+        result = 31 * result + (phaseName != null ? phaseName.hashCode() : 0);
         result = 31 * result + (recurringPrice != null ? recurringPrice.hashCode() : 0);
         result = 31 * result + (phaseType != null ? phaseType.hashCode() : 0);
         result = 31 * result + (recurringPrice != null ? recurringPrice.hashCode() : 0);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index 46a69f0..3a254de 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -30,6 +30,7 @@ import org.killbill.billing.ObjectType;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
@@ -83,20 +84,22 @@ public class SubscriptionJson extends JsonBase {
         private final String eventId;
         private final String billingPeriod;
         private final LocalDate effectiveDate;
+        private final String plan;
         private final String product;
         private final String priceList;
+        private final String phase;
         @ApiModelProperty(dataType = "org.killbill.billing.entitlement.api.SubscriptionEventType")
         private final String eventType;
         private final Boolean isBlockedBilling;
         private final Boolean isBlockedEntitlement;
         private final String serviceName;
         private final String serviceStateName;
-        private final String phase;
 
         @JsonCreator
         public EventSubscriptionJson(@JsonProperty("eventId") final String eventId,
                                      @JsonProperty("billingPeriod") final String billingPeriod,
                                      @JsonProperty("effectiveDt") final LocalDate effectiveDate,
+                                     @JsonProperty("plan") final String plan,
                                      @JsonProperty("product") final String product,
                                      @JsonProperty("priceList") final String priceList,
                                      @JsonProperty("eventType") final String eventType,
@@ -110,6 +113,7 @@ public class SubscriptionJson extends JsonBase {
             this.eventId = eventId;
             this.billingPeriod = billingPeriod;
             this.effectiveDate = effectiveDate;
+            this.plan = plan;
             this.product = product;
             this.priceList = priceList;
             this.eventType = eventType;
@@ -124,12 +128,14 @@ public class SubscriptionJson extends JsonBase {
 
             super(toAuditLogJson(getAuditLogsForSubscriptionEvent(subscriptionEvent, accountAuditLogs)));
             final BillingPeriod billingPeriod = subscriptionEvent.getNextBillingPeriod() != null ? subscriptionEvent.getNextBillingPeriod() : subscriptionEvent.getPrevBillingPeriod();
+            final Plan plan = subscriptionEvent.getNextPlan() != null ? subscriptionEvent.getNextPlan() : subscriptionEvent.getPrevPlan();
             final Product product = subscriptionEvent.getNextProduct() != null ? subscriptionEvent.getNextProduct() : subscriptionEvent.getPrevProduct();
             final PriceList priceList = subscriptionEvent.getNextPriceList() != null ? subscriptionEvent.getNextPriceList() : subscriptionEvent.getPrevPriceList();
             final PlanPhase phase = subscriptionEvent.getNextPhase() != null ? subscriptionEvent.getNextPhase() : subscriptionEvent.getPrevPhase();
             this.eventId = subscriptionEvent.getId().toString();
             this.billingPeriod = billingPeriod != null ? billingPeriod.toString() : null;
             this.effectiveDate = subscriptionEvent.getEffectiveDate();
+            this.plan = plan != null ? plan.getName() : null;
             this.product = product != null ? product.getName() : null;
             this.priceList = priceList != null ? priceList.getName() : null;
             this.eventType = subscriptionEvent.getSubscriptionEventType().toString();
@@ -166,6 +172,10 @@ public class SubscriptionJson extends JsonBase {
             return effectiveDate;
         }
 
+        public String getPlan() {
+            return plan;
+        }
+
         public String getProduct() {
             return product;
         }
@@ -204,6 +214,7 @@ public class SubscriptionJson extends JsonBase {
             sb.append("eventId='").append(eventId).append('\'');
             sb.append(", billingPeriod='").append(billingPeriod).append('\'');
             sb.append(", effectiveDate=").append(effectiveDate);
+            sb.append(", plan='").append(plan).append('\'');
             sb.append(", product='").append(product).append('\'');
             sb.append(", priceList='").append(priceList).append('\'');
             sb.append(", eventType='").append(eventType).append('\'');
@@ -251,6 +262,9 @@ public class SubscriptionJson extends JsonBase {
             if (priceList != null ? !priceList.equals(that.priceList) : that.priceList != null) {
                 return false;
             }
+            if (plan != null ? !plan.equals(that.plan) : that.plan != null) {
+                return false;
+            }
             if (product != null ? !product.equals(that.product) : that.product != null) {
                 return false;
             }
@@ -269,6 +283,7 @@ public class SubscriptionJson extends JsonBase {
             int result = eventId != null ? eventId.hashCode() : 0;
             result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0);
             result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+            result = 31 * result + (plan != null ? plan.hashCode() : 0);
             result = 31 * result + (product != null ? product.hashCode() : 0);
             result = 31 * result + (priceList != null ? priceList.hashCode() : 0);
             result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
@@ -378,19 +393,20 @@ public class SubscriptionJson extends JsonBase {
         this.events = new LinkedList<EventSubscriptionJson>();
         // We fill the catalog info every time we get the currency from the account (even if this is not overridden Plan)
         this.priceOverrides = new ArrayList<PhasePriceOverrideJson>();
+
         String currentPhaseName = null;
         for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
             this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
             if (currency != null) {
-                final PlanPhase cur = subscriptionEvent.getNextPhase();
-                if (cur == null || cur.getName().equals(currentPhaseName)) {
+                final PlanPhase curPlanPhase = subscriptionEvent.getNextPhase();
+                if (curPlanPhase == null || curPlanPhase.getName().equals(currentPhaseName)) {
                     continue;
                 }
-                currentPhaseName = cur.getName();
+                currentPhaseName = curPlanPhase.getName();
 
-                final BigDecimal fixedPrice = cur.getFixed() != null ? cur.getFixed().getPrice().getPrice(currency) : null;
-                final BigDecimal recurringPrice = cur.getRecurring() != null ? cur.getRecurring().getRecurringPrice().getPrice(currency) : null;
-                final PhasePriceOverrideJson phase = new PhasePriceOverrideJson(cur.getName(), cur.getPhaseType().toString(), fixedPrice, recurringPrice, cur.getUsages(),currency);
+                final BigDecimal fixedPrice = curPlanPhase.getFixed() != null ? curPlanPhase.getFixed().getPrice().getPrice(currency) : null;
+                final BigDecimal recurringPrice = curPlanPhase.getRecurring() != null ? curPlanPhase.getRecurring().getRecurringPrice().getPrice(currency) : null;
+                final PhasePriceOverrideJson phase = new PhasePriceOverrideJson(subscriptionEvent.getNextPlan().getName(), curPlanPhase.getName(), curPlanPhase.getPhaseType().toString(), fixedPrice, recurringPrice, curPlanPhase.getUsages(),currency);
                 priceOverrides.add(phase);
             }
         }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
index 056ce07..4ca6487 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
@@ -89,6 +89,8 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 @Api(value = JaxrsResource.ADMIN_PATH, description = "Admin operations (will require special privileges)")
 public class AdminResource extends JaxRsResourceBase {
 
+    private static final String OK = "OK";
+
     private final AdminPaymentApi adminPaymentApi;
     private final InvoiceUserApi invoiceUserApi;
     private final TenantUserApi tenantApi;
@@ -154,28 +156,26 @@ public class AdminResource extends JaxRsResourceBase {
         // TODO Consider adding a real invoice API post 0.18.x
         final Pagination<Tag> tags = tagUserApi.searchTags(SystemTags.PARK_TAG_DEFINITION_NAME, offset, limit, callContext);
 
-        // Return the accounts still parked
         final StreamingOutput json = new StreamingOutput() {
             @Override
             public void write(final OutputStream output) throws IOException, WebApplicationException {
                 final JsonGenerator generator = mapper.getFactory().createGenerator(output);
                 generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
 
-                generator.writeStartArray();
+                generator.writeStartObject();
                 for (final Tag tag : tags) {
                     final UUID accountId = tag.getObjectId();
                     try {
                         invoiceUserApi.triggerInvoiceGeneration(accountId, clock.getUTCToday(), null, callContext);
+                        generator.writeStringField(accountId.toString(), OK);
                     } catch (final InvoiceApiException e) {
-                        if (e.getCode() == ErrorCode.UNEXPECTED_ERROR.getCode()) {
-                            generator.writeString(accountId.toString());
-                        }
                         if (e.getCode() != ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
                             log.warn("Unable to trigger invoice generation for accountId='{}'", accountId);
                         }
+                        generator.writeStringField(accountId.toString(), ErrorCode.fromCode(e.getCode()).toString());
                     }
                 }
-                generator.writeEndArray();
+                generator.writeEndObject();
                 generator.close();
             }
         };
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index 3230c8b..dd67ebf 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -48,6 +48,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
                                                                       UUID.randomUUID().toString(),
                                                                       UUID.randomUUID().toString(),
                                                                       UUID.randomUUID().toString(),
+                                                                      UUID.randomUUID().toString(),
                                                                       true,
                                                                       false,
                                                                       UUID.randomUUID().toString(),
@@ -55,7 +56,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
                                                                       UUID.randomUUID().toString(),
                                                                       null);
 
-        final PhasePriceOverrideJson priceOverride = new PhasePriceOverrideJson(null, "somePhaseType", BigDecimal.ONE, null, null);
+        final PhasePriceOverrideJson priceOverride = new PhasePriceOverrideJson(null, null, "somePhaseType", BigDecimal.ONE, null, null);
 
         final SubscriptionJson subscription = new SubscriptionJson(UUID.randomUUID().toString(),
                                                                    UUID.randomUUID().toString(),
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
index 5749e61..114d0fc 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
@@ -39,6 +39,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuiteNoDB {
                                                                       UUID.randomUUID().toString(),
                                                                       UUID.randomUUID().toString(),
                                                                       UUID.randomUUID().toString(),
+                                                                      UUID.randomUUID().toString(),
                                                                       true,
                                                                       false,
                                                                       UUID.randomUUID().toString(),
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
index adceb4a..5bc6273 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
@@ -56,6 +56,7 @@ public class TestEntitlementJsonWithEvents extends JaxrsTestSuiteNoDB {
                                                                          effectiveDate.toLocalDate(),
                                                                          UUID.randomUUID().toString(),
                                                                          UUID.randomUUID().toString(),
+                                                                         UUID.randomUUID().toString(),
                                                                          SubscriptionBaseTransitionType.CREATE.toString(),
                                                                          false,
                                                                          true,
@@ -64,7 +65,7 @@ public class TestEntitlementJsonWithEvents extends JaxrsTestSuiteNoDB {
                                                                          PhaseType.DISCOUNT.toString(),
                                                                          auditLogs);
 
-        final PhasePriceOverrideJson priceOverride = new PhasePriceOverrideJson("bar", null, BigDecimal.TEN, BigDecimal.ONE,null);
+        final PhasePriceOverrideJson priceOverride = new PhasePriceOverrideJson("foo", "bar", null, BigDecimal.TEN, BigDecimal.ONE,null);
 
         final SubscriptionJson entitlementJsonWithEvents = new SubscriptionJson(accountId,
                                                                                 bundleId,
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
index 8ce5b3e..34cb5d3 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
@@ -136,9 +136,9 @@ public class TenantFilter implements Filter {
             if (    // Chicken - egg problem
                     isTenantCreationRequest(path, httpMethod) ||
                     // Retrieve user permissions should not require tenant info since this is cross tenants
-                    isPermissionRequest(path, httpMethod) ||
+                    isPermissionRequest(path) ||
                     // Node request are cross tenant
-                    isNodeCreationRequest(path, httpMethod) ||
+                    isNodeInfoRequest(path) ||
                     // Metrics servlets
                     isMetricsRequest(path, httpMethod) ||
                     // See KillBillShiroWebModule#CorsBasicHttpAuthenticationFilter
@@ -157,7 +157,7 @@ public class TenantFilter implements Filter {
 
 
 
-    private boolean isPermissionRequest(final String path, final String httpMethod) {
+    private boolean isPermissionRequest(final String path) {
         return path != null && path.startsWith(JaxrsResource.SECURITY_PATH);
     }
 
@@ -165,8 +165,8 @@ public class TenantFilter implements Filter {
         return JaxrsResource.TENANTS_PATH.equals(path) && "POST".equals(httpMethod);
     }
 
-    private boolean isNodeCreationRequest(final String path, final String httpMethod) {
-        return JaxrsResource.NODES_INFO_PATH.equals(path) && "POST".equals(httpMethod);
+    private boolean isNodeInfoRequest(final String path) {
+        return JaxrsResource.NODES_INFO_PATH.equals(path);
     }
 
     private boolean isMetricsRequest(final String path, final String httpMethod) {
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
index 59d24be..4377c5a 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
@@ -18,9 +18,8 @@
 package org.killbill.billing.jaxrs;
 
 import java.math.BigDecimal;
-import java.util.Collection;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -45,6 +44,7 @@ import org.testng.annotations.Test;
 
 import com.ning.http.client.Response;
 
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
@@ -101,7 +101,7 @@ public class TestAdmin extends TestJaxrsBase {
         final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
         clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
 
-        final Collection<UUID> accounts = new HashSet<UUID>();
+        final List<UUID> accounts = new LinkedList<UUID>();
         for (int i = 0; i < 5; i++) {
             final Account accountJson = createAccountWithDefaultPaymentMethod();
             assertNotNull(accountJson);
@@ -151,12 +151,17 @@ public class TestAdmin extends TestJaxrsBase {
 
         // Fix one account
         final Response response = triggerInvoiceGenerationForParkedAccounts(1);
-        Assert.assertEquals(response.getResponseBody(), "[]");
+        Assert.assertEquals(response.getResponseBody(), "{\"" + accounts.get(0) + "\":\"OK\"}");
         Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), 11);
 
         // Fix all accounts
         final Response response2 = triggerInvoiceGenerationForParkedAccounts(5);
-        Assert.assertEquals(response2.getResponseBody(), "[]");
+        final Map<String,String> fixedAccounts = mapper.readValue(response2.getResponseBody(), new TypeReference<Map<String,String>>() {});
+        Assert.assertEquals(fixedAccounts.size(), 4);
+        Assert.assertEquals(fixedAccounts.get(accounts.get(1).toString()), "OK");
+        Assert.assertEquals(fixedAccounts.get(accounts.get(2).toString()), "OK");
+        Assert.assertEquals(fixedAccounts.get(accounts.get(3).toString()), "OK");
+        Assert.assertEquals(fixedAccounts.get(accounts.get(4).toString()), "OK");
         Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), 15);
     }
 
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
index b0a736a..ef680e5 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -41,7 +41,9 @@ import org.killbill.billing.client.model.Invoice;
 import org.killbill.billing.client.model.PhasePriceOverride;
 import org.killbill.billing.client.model.Subscription;
 import org.killbill.billing.client.model.Tags;
+import org.killbill.billing.entitlement.EntitlementTransitionType;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
 import org.killbill.billing.util.api.AuditLevel;
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -246,21 +248,75 @@ public class TestEntitlement extends TestJaxrsBase {
         input.setBillingPeriod(BillingPeriod.MONTHLY);
         input.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
         final List<PhasePriceOverride> overrides = new ArrayList<PhasePriceOverride>();
-        overrides.add(new PhasePriceOverride(null, PhaseType.TRIAL.toString(), BigDecimal.TEN, null, null));
+        overrides.add(new PhasePriceOverride(null, null, PhaseType.TRIAL.toString(), BigDecimal.TEN, null, null));
         input.setPriceOverrides(overrides);
 
         final Subscription subscription = killBillClient.createSubscription(input, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, requestOptions);
         Assert.assertEquals(subscription.getPriceOverrides().size(), 2);
-        Assert.assertEquals(subscription.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-1-trial");
-        Assert.assertEquals(subscription.getPriceOverrides().get(0).getFixedPrice().compareTo(BigDecimal.TEN), 0);
-        Assert.assertNull(subscription.getPriceOverrides().get(0).getRecurringPrice());
-        Assert.assertEquals(subscription.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-1-evergreen");
-        Assert.assertNull(subscription.getPriceOverrides().get(1).getFixedPrice());
-        Assert.assertEquals(subscription.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
+
+        Assert.assertEquals(subscription.getEvents().size(), 3);
+        Assert.assertEquals(subscription.getEvents().get(0).getEventType(), SubscriptionEventType.START_ENTITLEMENT.name());
+        Assert.assertEquals(subscription.getEvents().get(0).getPlan(), "shotgun-monthly-1");
+        Assert.assertEquals(subscription.getEvents().get(0).getPhase(), "shotgun-monthly-1-trial");
+        Assert.assertEquals(subscription.getEvents().get(0).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription.getEvents().get(0).getProduct(), "Shotgun");
+
+        Assert.assertEquals(subscription.getEvents().get(1).getEventType(), SubscriptionEventType.START_BILLING.name());
+        Assert.assertEquals(subscription.getEvents().get(1).getPlan(), "shotgun-monthly-1");
+        Assert.assertEquals(subscription.getEvents().get(1).getPhase(), "shotgun-monthly-1-trial");
+        Assert.assertEquals(subscription.getEvents().get(1).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription.getEvents().get(1).getProduct(), "Shotgun");
+
+        Assert.assertEquals(subscription.getEvents().get(2).getEventType(), SubscriptionEventType.PHASE.name());
+        Assert.assertEquals(subscription.getEvents().get(2).getPlan(), "shotgun-monthly-1");
+        Assert.assertEquals(subscription.getEvents().get(2).getPhase(), "shotgun-monthly-1-evergreen");
+        Assert.assertEquals(subscription.getEvents().get(2).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription.getEvents().get(2).getProduct(), "Shotgun");
+
 
         final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL, requestOptions);
         assertEquals(invoices.size(), 1);
         assertEquals(invoices.get(0).getAmount().compareTo(BigDecimal.TEN), 0);
+
+        // Move clock after phase
+        clock.addDays(30);
+        crappyWaitForLackOfProperSynchonization();
+
+        final Subscription subscription2 = killBillClient.getSubscription(subscription.getSubscriptionId(), requestOptions);
+        Assert.assertEquals(subscription2.getEvents().size(), 3);
+
+        clock.addDays(3);
+
+        // Change Plan
+        final Subscription newInput = new Subscription();
+        newInput.setSubscriptionId(subscription2.getSubscriptionId());
+        newInput.setPlanName("pistol-monthly");
+        final Subscription subscription3 = killBillClient.updateSubscription(newInput, null, BillingActionPolicy.IMMEDIATE, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, basicRequestOptions());
+
+        Assert.assertEquals(subscription3.getEvents().size(), 4);
+        Assert.assertEquals(subscription3.getEvents().get(0).getEventType(), SubscriptionEventType.START_ENTITLEMENT.name());
+        Assert.assertEquals(subscription3.getEvents().get(0).getPlan(), "shotgun-monthly-1");
+        Assert.assertEquals(subscription3.getEvents().get(0).getPhase(), "shotgun-monthly-1-trial");
+        Assert.assertEquals(subscription3.getEvents().get(0).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription3.getEvents().get(0).getProduct(), "Shotgun");
+
+        Assert.assertEquals(subscription3.getEvents().get(1).getEventType(), SubscriptionEventType.START_BILLING.name());
+        Assert.assertEquals(subscription3.getEvents().get(1).getPlan(), "shotgun-monthly-1");
+        Assert.assertEquals(subscription3.getEvents().get(1).getPhase(), "shotgun-monthly-1-trial");
+        Assert.assertEquals(subscription3.getEvents().get(1).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription3.getEvents().get(1).getProduct(), "Shotgun");
+
+        Assert.assertEquals(subscription3.getEvents().get(2).getEventType(), SubscriptionEventType.PHASE.name());
+        Assert.assertEquals(subscription3.getEvents().get(2).getPlan(), "shotgun-monthly-1");
+        Assert.assertEquals(subscription3.getEvents().get(2).getPhase(), "shotgun-monthly-1-evergreen");
+        Assert.assertEquals(subscription3.getEvents().get(2).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription3.getEvents().get(2).getProduct(), "Shotgun");
+
+        Assert.assertEquals(subscription3.getEvents().get(3).getEventType(), SubscriptionEventType.CHANGE.name());
+        Assert.assertEquals(subscription3.getEvents().get(3).getPlan(), "pistol-monthly");
+        Assert.assertEquals(subscription3.getEvents().get(3).getPhase(), "pistol-monthly-evergreen");
+        Assert.assertEquals(subscription3.getEvents().get(3).getPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME.toString());
+        Assert.assertEquals(subscription3.getEvents().get(3).getProduct(), "Pistol");
     }
 
     @Test(groups = "slow", description = "Create a base entitlement and also addOns entitlements under the same bundle")