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")