Details
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
index 36e7b9a..96bbda4 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java
@@ -68,11 +68,11 @@ public class AccountItemTree {
public void build() {
Preconditions.checkState(!isBuilt);
- if (allExistingItems.size() > 0) {
- for (InvoiceItem item : allExistingItems) {
+ if (pendingItemAdj.size() > 0) {
+ for (InvoiceItem item : pendingItemAdj) {
addExistingItem(item, true);
}
- allExistingItems.clear();
+ pendingItemAdj.clear();
}
for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
tree.build();
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
new file mode 100644
index 0000000..df31e26
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java
@@ -0,0 +1,208 @@
+package com.ning.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import com.google.common.base.Preconditions;
+
+public class ItemsNodeInterval extends NodeInterval {
+
+ private ItemsInterval items;
+
+ public ItemsNodeInterval() {
+ this.items = new ItemsInterval(this);
+ }
+
+ public ItemsNodeInterval(final NodeInterval parent, final Item item) {
+ super(parent, item.getStartDate(), item.getEndDate());
+ this.items = new ItemsInterval(this, item);
+ }
+
+ public ItemsInterval getItems() {
+ return items;
+ }
+
+ public boolean containsItem(final UUID targetId) {
+ return items.containsItem(targetId);
+ }
+
+ /**
+ * <p/>
+ * There is no limit in the depth of the tree,
+ * and the build strategy is to first consider the lowest child for a given period
+ * and go up the tree adding missing interval if needed. For e.g, one of the possible scenario:
+ * <pre>
+ * D1 D2
+ * |---------------------------------------------------| Plan P1
+ * D1' D2'
+ * |---------------|/////////////////////////////| Plan P2, REPAIR
+ *
+ * In that case we will generate:
+ * [D1,D1') on Plan P1; [D1', D2') on Plan P2, and [D2', D2) repair item
+ *
+ * <pre/>
+ *
+ * In the merge mode, the strategy is different, the tree is fairly shallow
+ * and the goal is to generate the repair items; @see addProposedItem
+ *
+ * @param output result list of built items
+ */
+ public void buildForExistingItems(final List<Item> output) {
+ build(new BuildNodeCallback() {
+ @Override
+ public void buildMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+ items.buildForMissingInterval(startDate, endDate, output, false);
+ }
+
+ @Override
+ public void buildNode(final NodeInterval curNode) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+ items.buildFromItems(output, false);
+ }
+ });
+ }
+
+ /**
+ * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
+ * <p/>
+ * That means that if we were to not merge any new proposed items, we would end up with only those reversed existing
+ * items, and they would all end up repaired-- which is what we want.
+ * <p/>
+ * However, if there are new proposed items, then we look to see if they are children one our existing reverse items
+ * so that we can generate the repair pieces missing. For e.g, below is one scenario among so many:
+ * <p/>
+ * <pre>
+ * D1 D2
+ * |---------------------------------------------------| (existing reversed (CANCEL) item
+ * D1' D2'
+ * |---------------| (proposed same plan)
+ * </pre>
+ * In that case we want to generated a repair for [D1, D1') and [D2',D2)
+ * <p/>
+ * Note that this tree is never very deep, only 3 levels max, with exiting at the first level
+ * and proposed that are the for the exact same plan but for different dates below.
+ *
+ * @param output result list of built items
+ */
+ public void mergeExistingAndProposed(final List<Item> output) {
+ build(new BuildNodeCallback() {
+ @Override
+ public void buildMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+ items.buildForMissingInterval(startDate, endDate, output, true);
+ }
+
+ @Override
+ public void buildNode(final NodeInterval curNode) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItems();
+ items.buildFromItems(output, true);
+ }
+ });
+ }
+
+ /**
+ * Add existing item into the tree.
+ *
+ * @param newNode an existing item
+ */
+ public boolean addExistingItem(final ItemsNodeInterval newNode) {
+
+ return addNode(newNode, new AddNodeCallback() {
+ @Override
+ public boolean onExistingNode(final NodeInterval existingNode) {
+ if (!existingNode.isRoot() && newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0) {
+ final Item item = newNode.getItems().getItems().get(0);
+ final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItems();
+ existingOrNewNodeItems.insertSortedItem(item);
+ }
+ // There is no new node added but instead we just populated the list of items for the already existing node.
+ return false;
+ }
+
+ @Override
+ public boolean shouldInsertNode(final NodeInterval insertionNode) {
+ // Always want to insert node in the tree when we find the right place.
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Add proposed item into the (flattened and reversed) tree.
+ *
+ * @param newNode a new proposed item
+ * @return true if the item was merged and will trigger a repair or false if the proposed item should be kept as such
+ * and no repair generated.
+ */
+ public boolean addProposedItem(final ItemsNodeInterval newNode) {
+
+ return addNode(newNode, new AddNodeCallback() {
+ @Override
+ public boolean onExistingNode(final NodeInterval existingNode) {
+ if (!shouldInsertNode(existingNode)) {
+ return false;
+ }
+
+ Preconditions.checkState(newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0);
+ final Item item = newNode.getItems().getItems().get(0);
+ final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItems();
+ 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'.
+ if (insertionNode.isRoot()) {
+ return false;
+ }
+
+ final ItemsInterval insertionNodeItems = ((ItemsNodeInterval) insertionNode).getItems();
+ Preconditions.checkState(insertionNodeItems.getItems().size() == 1, "Expected existing node to have only one item");
+ final Item insertionNodeItem = insertionNodeItems.getItems().get(0);
+ final Item newNodeItem = newNode.getItems().getItems().get(0);
+
+ // 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(newNodeItem)) {
+ return true;
+ // If not, then keep the proposed outside of the tree.
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ /**
+ * Add the adjustment amount on the item specified by the targetId.
+ *
+ * @param adjustementDate date of the adjustment
+ * @param amount amount of the adjustment
+ * @param targetId item that has been adjusted
+ */
+ public void addAdjustment(final LocalDate adjustementDate, final BigDecimal amount, final UUID targetId) {
+ // TODO we should really be using findNode(adjustementDate, new SearchCallback() instead but wrong dates in test
+ // creates test panic.
+ final NodeInterval node = findNode(new SearchCallback() {
+ @Override
+ public boolean isMatch(final NodeInterval curNode) {
+ return ((ItemsNodeInterval) curNode).getItems().containsItem(targetId);
+ }
+ });
+ Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
+ ((ItemsNodeInterval) node).setAdjustment(amount.negate(), targetId);
+ }
+
+ protected void setAdjustment(final BigDecimal amount, final UUID linkedId) {
+ items.setAdjustment(amount, linkedId);
+ }
+
+}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
index f6dcaab..0f90d62 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
@@ -16,66 +16,45 @@
package com.ning.billing.invoice.tree;
-import java.math.BigDecimal;
import java.util.List;
-import java.util.UUID;
import org.joda.time.LocalDate;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
-public class NodeInterval {
+public abstract class NodeInterval {
- private LocalDate start;
- private LocalDate end;
- private ItemsInterval items;
+ protected NodeInterval parent;
+ protected NodeInterval leftChild;
+ protected NodeInterval rightSibling;
- private NodeInterval parent;
- private NodeInterval leftChild;
- private NodeInterval rightSibling;
+ protected LocalDate start;
+ protected LocalDate end;
public NodeInterval() {
- this.items = new ItemsInterval(this);
+ this(null, null, null);
}
- public NodeInterval(final NodeInterval parent, final Item item) {
- this.start = item.getStartDate();
- this.end = item.getEndDate();
- this.items = new ItemsInterval(this, item);
+ public NodeInterval(final NodeInterval parent, final LocalDate startDate, final LocalDate endDate) {
+ this.start = startDate;
+ this.end = endDate;
this.parent = parent;
this.leftChild = null;
this.rightSibling = null;
}
/**
- * Build the output list from the elements in the tree.
- * <p/>
- * In the simple mode, mergeMode = false, there is no limit in the depth of the tree,
- * and the build strategy is to first consider the lowest child for a given period
- * and go up the tree adding missing interval if needed. For e.g, one of the possible scenario:
- * <pre>
- * D1 D2
- * |---------------------------------------------------| Plan P1
- * D1' D2'
- * |---------------|/////////////////////////////| Plan P2, REPAIR
+ * Build the tree by calling the callback on the last node in the tree or remaining part with no children.
*
- * In that case we will generate:
- * [D1,D1') on Plan P1; [D1', D2') on Plan P2, and [D2', D2) repair item
- *
- * <pre/>
- *
- * In the merge mode, the strategy is different, the tree is fairly shallow
- * and the goal is to generate the repair items; @see mergeProposedItem
- *
- * @param output result list of items
- * @param mergeMode mode used to produce output list
+ * @param callback the callback which perform the build logic.
*/
- public void build(final List<Item> output, final boolean mergeMode) {
+ public void build(final BuildNodeCallback callback) {
+
+ Preconditions.checkNotNull(callback);
- // There is no sub-interval, just add our own items.
if (leftChild == null) {
- items.buildFromItems(output, mergeMode);
+ callback.buildNode(this);
return;
}
@@ -83,151 +62,103 @@ public class NodeInterval {
NodeInterval curChild = leftChild;
while (curChild != null) {
if (curChild.getStart().compareTo(curDate) > 0) {
- items.buildForMissingInterval(curDate, curChild.getStart(), output, mergeMode);
+ callback.buildMissingInterval(this, curDate, curChild.getStart());
}
- curChild.build(output, mergeMode);
- curDate = curChild.getEnd();
+ curChild.build(callback);
+ curDate = curChild.getEnd();
curChild = curChild.getRightSibling();
}
// Finally if there is a hole at the end, we build the missing piece from ourself
if (curDate.compareTo(end) < 0) {
- items.buildForMissingInterval(curDate, end, output, mergeMode);
+ callback.buildMissingInterval(this, curDate, end);
}
}
/**
- * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
- * That means that if we were to not merge any new proposed items, we would end up with only those reversed existing
- * items, and they would all end up repaired-- which is what we want.
- * <p/>
- * However, if there are new proposed items, then we look to see if they are children one our existing reverse items
- * so that we can generate the repair pieces missing. For e.g, below is one scenario among so many:
- * <p/>
- * <pre>
- * D1 D2
- * |---------------------------------------------------| (existing reversed (CANCEL) item
- * D1' D2'
- * |---------------| (proposed same plan)
- * </pre>
- * In that case we want to generated a repair for [D1, D1') and [D2',D2)
- * <p/>
- * Note that this tree is never very deep, only 3 levels max, with exiting at the first level
- * and proposed that are the for the exact same plan but for different dates below.
+ * Add a new node in the tree.
*
- * @param newNode a new proposed item
- * @return true if the item was merged and will trigger a repair or false if the proposed item should be kept as such
- * and no repair generated.
+ * @param newNode the node to be added
+ * @param callback the callback that will allow to specify insertion and return behavior.
+ * @return true if node was inserted. Note that this is driven by the callback, this method is generic
+ * and specific behavior can be tuned through specific callbacks.
*/
- public boolean mergeProposedItem(final NodeInterval newNode) {
+ public boolean addNode(final NodeInterval newNode, final AddNodeCallback callback) {
- Preconditions.checkState(newNode.getItems().size() == 1, "Expected new node to have only one item");
- final Item newNodeItem = newNode.getItems().get(0);
+ Preconditions.checkNotNull(newNode);
+ Preconditions.checkNotNull(callback);
- if (!isRoot() && newNodeItem.getStartDate().compareTo(start) == 0 && newNodeItem.getEndDate().compareTo(end) == 0) {
- items.cancelItems(newNodeItem);
- return true;
+ if (!isRoot() && newNode.getStart().compareTo(start) == 0 && newNode.getEnd().compareTo(end) == 0) {
+ return callback.onExistingNode(this);
}
+
computeRootInterval(newNode);
+ newNode.parent = this;
if (leftChild == null) {
- // There is no existing items, only new proposed one, nothing to add in that merge tree
- if (isRoot()) {
- return false;
- } else {
- // Proposed item is the first child of an existing item with the same product info.
- newNode.parent = this;
+ if (callback.shouldInsertNode(this)) {
leftChild = newNode;
return true;
-
+ } else {
+ return false;
}
}
NodeInterval prevChild = null;
NodeInterval curChild = leftChild;
- do {
- if (curChild.isItemContained(newNodeItem)) {
- final Item existingNodeItem = curChild.getItems().get(0);
+ while (curChild != null) {
+ if (curChild.isItemContained(newNode)) {
+ return curChild.addNode(newNode, callback);
+ }
- Preconditions.checkState(curChild.getItems().size() == 1, "Expected existing node to have only one item");
- if (existingNodeItem.isSameKind(newNodeItem)) {
- // Proposed item has same product info than parent and is contained so insert it at the right place in the tree
- curChild.mergeProposedItem(newNode);
+ if (curChild.isItemOverlap(newNode)) {
+ if (callback.shouldInsertNode(this)) {
+ rebalance(newNode);
return true;
} else {
return false;
}
}
- if (newNodeItem.getStartDate().compareTo(curChild.getStart()) < 0) {
- newNode.parent = this;
- newNode.rightSibling = curChild;
- if (prevChild == null) {
- leftChild = newNode;
+ if (newNode.getStart().compareTo(curChild.getStart()) < 0) {
+ if (callback.shouldInsertNode(this)) {
+ newNode.rightSibling = curChild;
+ if (prevChild == null) {
+ leftChild = newNode;
+ } else {
+ prevChild.rightSibling = newNode;
+ }
+ return true;
} else {
- prevChild.rightSibling = newNode;
+ return false;
}
- return true;
}
-
prevChild = curChild;
curChild = curChild.rightSibling;
- } while (curChild != null);
+ }
- if (isRoot()) {
- // The new proposed item spans over a new interval, nothing to add in the merge tree
- return false;
- } else {
- newNode.parent = this;
+ if (callback.shouldInsertNode(this)) {
prevChild.rightSibling = newNode;
return true;
+ } else {
+ return false;
}
}
- /**
- * Add an existing item in the tree of items.
- *
- * @param newNode new existing item to be added
- */
- public void addExistingItem(final NodeInterval newNode) {
- final Item item = newNode.getItems().get(0);
- if (!isRoot() && item.getStartDate().compareTo(start) == 0 && item.getEndDate().compareTo(end) == 0) {
- items.insertSortedItem(item);
- return;
- }
- computeRootInterval(newNode);
- addNode(newNode);
- }
-
- /**
- * Add the adjustment amount on the item specified by the targetId.
- *
- * @param adjustementDate date of the adjustment
- * @param amount amount of the adjustment
- * @param targetId item that has been adjusted
- */
- public void addAdjustment(final LocalDate adjustementDate, final BigDecimal amount, final UUID targetId) {
- NodeInterval node = findNode(adjustementDate, targetId);
- Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
- node.setAdjustment(amount.negate(), targetId);
- }
-
- public boolean isItemContained(final Item item) {
- return (item.getStartDate().compareTo(start) >= 0 &&
- item.getStartDate().compareTo(end) <= 0 &&
- item.getEndDate().compareTo(start) >= 0 &&
- item.getEndDate().compareTo(end) <= 0);
+ public boolean isItemContained(final NodeInterval newNode) {
+ return (newNode.getStart().compareTo(start) >= 0 &&
+ newNode.getStart().compareTo(end) <= 0 &&
+ newNode.getEnd().compareTo(start) >= 0 &&
+ newNode.getEnd().compareTo(end) <= 0);
}
- public boolean isItemOverlap(final Item item) {
- return ((item.getStartDate().compareTo(start) < 0 &&
- item.getEndDate().compareTo(end) >= 0) ||
- (item.getStartDate().compareTo(start) <= 0 &&
- item.getEndDate().compareTo(end) > 0));
+ public boolean isItemOverlap(final NodeInterval newNode) {
+ return ((newNode.getStart().compareTo(start) < 0 &&
+ newNode.getEnd().compareTo(end) >= 0) ||
+ (newNode.getStart().compareTo(start) <= 0 &&
+ newNode.getEnd().compareTo(end) > 0));
}
-
-
public boolean isRoot() {
return parent == null;
}
@@ -252,7 +183,6 @@ public class NodeInterval {
return rightSibling;
}
-
public int getNbChildren() {
int result = 0;
NodeInterval curChild = leftChild;
@@ -263,52 +193,6 @@ public class NodeInterval {
return result;
}
- public List<Item> getItems() {
- return items.getItems();
- }
-
- public boolean containsItem(final UUID targetId) {
- return items.containsItem(targetId);
- }
-
- private void addNode(final NodeInterval newNode) {
-
- newNode.parent = this;
- final Item item = newNode.getItems().get(0);
- if (leftChild == null) {
- leftChild = newNode;
- return;
- }
-
- NodeInterval prevChild = null;
- NodeInterval curChild = leftChild;
- while (curChild != null) {
- if (curChild.isItemContained(item)) {
- curChild.addExistingItem(newNode);
- return;
- }
-
- if (curChild.isItemOverlap(item)) {
- rebalance(newNode);
- return;
- }
-
- if (item.getStartDate().compareTo(curChild.getStart()) < 0) {
- newNode.rightSibling = curChild;
- if (prevChild == null) {
- leftChild = newNode;
- } else {
- prevChild.rightSibling = newNode;
- }
- return;
- }
- prevChild = curChild;
- curChild = curChild.rightSibling;
- }
-
- prevChild.rightSibling = newNode;
- }
-
/**
* Since items may be added out of order, there is no guarantee that we don't suddenly have a new node
* whose interval emcompasses cuurent node(s). In which case we need to rebalance the tree.
@@ -317,13 +201,11 @@ public class NodeInterval {
*/
private void rebalance(final NodeInterval newNode) {
- final Item item = newNode.getItems().get(0);
-
NodeInterval prevRebalanced = null;
NodeInterval curChild = leftChild;
List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
do {
- if (curChild.isItemOverlap(item)) {
+ if (curChild.isItemOverlap(newNode)) {
toBeRebalanced.add(curChild);
} else {
if (toBeRebalanced.size() > 0) {
@@ -364,28 +246,31 @@ public class NodeInterval {
this.end = (end == null || end.compareTo(newNode.getEnd()) < 0) ? newNode.getEnd() : end;
}
- private void setAdjustment(final BigDecimal amount, final UUID linkedId) {
- items.setAdjustment(amount, linkedId);
- }
+ /**
+ * Return the first node satisfying the date and match callback.
+ *
+ * @param targetDate target date for possible match nodes whose interval comprises that date
+ * @param callback custom logic to decide if a given node is a match
+ * @return the found node or null if there is nothing.
+ */
+ public NodeInterval findNode(final LocalDate targetDate, final SearchCallback callback) {
- private NodeInterval findNode(final LocalDate date, final UUID targetItemId) {
- Preconditions.checkState(isRoot(), "findNode can only be called from root");
- return findNodeRecursively2(this, date, targetItemId);
- }
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkNotNull(targetDate);
- // TODO That method should be use instead of findNodeRecursively2 to search the node more effectively using the time
- // but unfortunately that fails because of our test that use the wrong date when doing adjustments.
- private NodeInterval findNodeRecursively(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
- if (date.compareTo(curNode.getStart()) < 0 || date.compareTo(curNode.getEnd()) > 0) {
+ if (targetDate.compareTo(getStart()) < 0 || targetDate.compareTo(getEnd()) > 0) {
return null;
}
- NodeInterval curChild = curNode.getLeftChild();
+
+ NodeInterval curChild = leftChild;
while (curChild != null) {
- if (curChild.getStart().compareTo(date) <= 0 && curChild.getEnd().compareTo(date) >= 0) {
- if (curChild.containsItem(targetItemId)) {
+ if (curChild.getStart().compareTo(targetDate) <= 0 && curChild.getEnd().compareTo(targetDate) >= 0) {
+ if (callback.isMatch(curChild)) {
return curChild;
- } else {
- return findNodeRecursively(curChild, date, targetItemId);
+ }
+ NodeInterval result = findNode(targetDate, callback);
+ if (result != null) {
+ return result;
}
}
curChild = curChild.getRightSibling();
@@ -393,15 +278,22 @@ public class NodeInterval {
return null;
}
- private NodeInterval findNodeRecursively2(final NodeInterval curNode, final LocalDate date, final UUID targetItemId) {
+ /**
+ * Return the first node satisfying the date and match callback.
+ *
+ * @param callback custom logic to decide if a given node is a match
+ * @return the found node or null if there is nothing.
+ */
+ public NodeInterval findNode(final SearchCallback callback) {
- if (!curNode.isRoot() && curNode.containsItem(targetItemId)) {
- return curNode;
+ Preconditions.checkNotNull(callback);
+ if (/*!isRoot() && */callback.isMatch(this)) {
+ return this;
}
- NodeInterval curChild = curNode.getLeftChild();
+ NodeInterval curChild = leftChild;
while (curChild != null) {
- final NodeInterval result = findNodeRecursively2(curChild, date, targetItemId);
+ final NodeInterval result = curChild.findNode(callback);
if (result != null) {
return result;
}
@@ -409,4 +301,57 @@ public class NodeInterval {
}
return null;
}
+
+ /**
+ * Provides custom logic for the search.
+ */
+ public interface SearchCallback {
+
+ boolean isMatch(NodeInterval curNode);
+ }
+
+ /**
+ * Provides the custom logic for when building resulting state from the tree.
+ */
+ public interface BuildNodeCallback {
+
+ /**
+ * Called when we hit a missing interval where there is no child.
+ *
+ * @param curNode current node
+ * @param startDate startDate of the new interval to build
+ * @param endDate endDate of the new interval to build
+ */
+ public void buildMissingInterval(NodeInterval curNode, LocalDate startDate, LocalDate endDate);
+
+ /**
+ * Called when we hit a node with no children
+ *
+ * @param curNode current node
+ */
+ public void buildNode(NodeInterval curNode);
+ }
+
+ /**
+ * Provides the custom logic for when adding nodes in the tree.
+ */
+ public interface AddNodeCallback {
+
+ /**
+ * Called when trying to insert a new node in the tree but there is already
+ * such a node for that same interval.
+ *
+ * @param existingNode
+ * @return this is the return value for the addNode method
+ */
+ public boolean onExistingNode(final NodeInterval existingNode);
+
+ /**
+ * Called prior to insert the new node in the tree
+ *
+ * @param insertionNode the parent node where this new node would be inserted
+ * @return true if addNode should proceed with the insertion and false otherwise
+ */
+ public boolean shouldInsertNode(final NodeInterval insertionNode);
+ }
}
diff --git a/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
index 0c4a250..b04f343 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
@@ -43,7 +43,7 @@ public class SubscriptionItemTree {
private boolean isBuilt;
private final UUID subscriptionId;
- private NodeInterval root;
+ private ItemsNodeInterval root;
private List<Item> items;
@@ -71,7 +71,7 @@ public class SubscriptionItemTree {
public SubscriptionItemTree(final UUID subscriptionId) {
this.subscriptionId = subscriptionId;
- this.root = new NodeInterval();
+ this.root = new ItemsNodeInterval();
this.items = new LinkedList<Item>();
this.existingFixedItems = new LinkedList<InvoiceItem>();
this.remainingFixedItems = new LinkedList<InvoiceItem>();
@@ -88,7 +88,7 @@ public class SubscriptionItemTree {
root.addAdjustment(item.getStartDate(), item.getAmount(), item.getLinkedItemId());
}
pendingItemAdj.clear();
- root.build(items, false);
+ root.buildForExistingItems(items);
isBuilt = true;
}
@@ -103,10 +103,10 @@ public class SubscriptionItemTree {
if (!isBuilt) {
build();
}
- root = new NodeInterval();
+ root = new ItemsNodeInterval();
for (Item item : items) {
Preconditions.checkState(item.getAction() == ItemAction.ADD);
- root.addExistingItem(new NodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
}
items.clear();
isBuilt = false;
@@ -114,7 +114,7 @@ public class SubscriptionItemTree {
public void buildForMerge() {
Preconditions.checkState(!isBuilt);
- root.build(items, true);
+ root.mergeExistingAndProposed(items);
isBuilt = true;
}
@@ -128,11 +128,11 @@ public class SubscriptionItemTree {
Preconditions.checkState(!isBuilt);
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
- root.addExistingItem(new NodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
break;
case REPAIR_ADJ:
- root.addExistingItem(new NodeInterval(root, new Item(invoiceItem, ItemAction.CANCEL)));
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.CANCEL)));
break;
case FIXED:
@@ -158,7 +158,7 @@ public class SubscriptionItemTree {
Preconditions.checkState(!isBuilt);
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
- final boolean result = root.mergeProposedItem(new NodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+ final boolean result = root.addProposedItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
if (!result) {
items.add(new Item(invoiceItem, ItemAction.ADD));
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
index 530f12f..8c1d3f2 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java
@@ -20,46 +20,65 @@ import java.math.BigDecimal;
import java.util.UUID;
import org.joda.time.LocalDate;
-import org.testng.Assert;
import org.testng.annotations.Test;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.model.RecurringInvoiceItem;
import com.ning.billing.invoice.tree.Item.ItemAction;
+import com.ning.billing.invoice.tree.NodeInterval.AddNodeCallback;
import static org.testng.Assert.assertEquals;
public class TestNodeInterval /* extends InvoiceTestSuiteNoDB */ {
- private final UUID invoiceId = UUID.randomUUID();
- private final UUID accountId = UUID.randomUUID();
- private final UUID subscriptionId = UUID.randomUUID();
- private final UUID bundleId = UUID.randomUUID();
- private final String planName = "my-plan";
- private final String phaseName = "my-phase";
- private final Currency currency = Currency.USD;
+
+ private AddNodeCallback CALLBACK = new DummyAddNodeCallback();
+
+ public class DummyNodeInterval extends NodeInterval {
+
+ public DummyNodeInterval() {
+ }
+
+ public DummyNodeInterval(final NodeInterval parent, final LocalDate startDate, final LocalDate endDate) {
+ super(parent, startDate, endDate);
+ }
+
+ }
+
+ public class DummyAddNodeCallback implements AddNodeCallback {
+
+ @Override
+ public boolean onExistingNode(final NodeInterval existingNode) {
+ return false;
+ }
+
+ @Override
+ public boolean shouldInsertNode(final NodeInterval insertionNode) {
+ return true;
+ }
+ }
@Test(groups = "fast")
public void testAddExistingItemSimple() {
- final NodeInterval root = new NodeInterval();
+ final DummyNodeInterval root = new DummyNodeInterval();
- final NodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
- root.addExistingItem(top);
+ final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(top, CALLBACK);
- final NodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
- final NodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
- final NodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
- root.addExistingItem(firstChildLevel1);
- root.addExistingItem(secondChildLevel1);
- root.addExistingItem(thirdChildLevel1);
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
- final NodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
- final NodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
- final NodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
- root.addExistingItem(firstChildLevel2);
- root.addExistingItem(secondChildLevel2);
- root.addExistingItem(thirdChildLevel2);
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
checkNode(top, 3, root, firstChildLevel1, null);
checkNode(firstChildLevel1, 2, top, firstChildLevel2, secondChildLevel1);
@@ -74,24 +93,24 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB */ {
@Test(groups = "fast")
public void testAddExistingItemWithRebalance() {
- final NodeInterval root = new NodeInterval();
+ final DummyNodeInterval root = new DummyNodeInterval();
- final NodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
- root.addExistingItem(top);
+ final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(top, CALLBACK);
- final NodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
- final NodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
- final NodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
- root.addExistingItem(firstChildLevel2);
- root.addExistingItem(secondChildLevel2);
- root.addExistingItem(thirdChildLevel2);
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
- final NodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
- final NodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
- final NodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
- root.addExistingItem(firstChildLevel1);
- root.addExistingItem(secondChildLevel1);
- root.addExistingItem(thirdChildLevel1);
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
checkNode(top, 3, root, firstChildLevel1, null);
checkNode(firstChildLevel1, 2, top, firstChildLevel2, secondChildLevel1);
@@ -105,7 +124,7 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB */ {
- private void checkNode(final NodeInterval node, final int expectedChildren, final NodeInterval expectedParent, final NodeInterval expectedLeftChild, final NodeInterval expectedRightSibling) {
+ private void checkNode(final NodeInterval node, final int expectedChildren, final DummyNodeInterval expectedParent, final DummyNodeInterval expectedLeftChild, final DummyNodeInterval expectedRightSibling) {
assertEquals(node.getNbChildren(), expectedChildren);
assertEquals(node.getParent(), expectedParent);
assertEquals(node.getRightSibling(), expectedRightSibling);
@@ -113,15 +132,8 @@ public class TestNodeInterval /* extends InvoiceTestSuiteNoDB */ {
assertEquals(node.getLeftChild(), expectedLeftChild);
}
- private NodeInterval createNodeInterval(final String startDate, final String endDate) {
- return new NodeInterval(null, createItem(startDate, endDate));
+ private DummyNodeInterval createNodeInterval(final String startDate, final String endDate) {
+ return new DummyNodeInterval(null, new LocalDate(startDate), new LocalDate(endDate));
}
- private Item createItem(final String startDate, final String endDate) {
- final BigDecimal amount = BigDecimal.TEN;
- final BigDecimal rate = BigDecimal.TEN;
- final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, new LocalDate(startDate), new LocalDate(endDate), amount, rate, currency);
- Item item = new Item(invoiceItem, ItemAction.ADD);
- return item;
- }
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
index 9152117..5b8428a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -76,7 +76,6 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB */ {
tree.addItem(repair);
tree.build();
verifyResult(tree.getView(), expectedResult);
-
tree = new SubscriptionItemTree(subscriptionId);
tree.addItem(repair);
tree.addItem(newItem);
@@ -500,6 +499,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB */ {
verifyResult(tree.getView(), expectedResult);
}
+
@Test(groups = "fast")
public void testMergeCancellationWithTwoMiddleRepair() {
@@ -524,7 +524,6 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB */ {
tree.mergeProposedItem(proposed1);
tree.mergeProposedItem(proposed2);
- tree.mergeProposedItem(proposed1);
tree.mergeProposedItem(proposed3);
tree.buildForMerge();
@@ -536,7 +535,7 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB */ {
verifyResult(tree.getView(), expectedResult);
- // Dot it again but with propsoed items out of order
+ // Dot it again but with proposed items out of order
final SubscriptionItemTree treeAgain = new SubscriptionItemTree(subscriptionId);
final InvoiceItem monthlyAgain = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
treeAgain.addItem(monthlyAgain);
@@ -548,7 +547,6 @@ public class TestSubscriptionItemTree /* extends InvoiceTestSuiteNoDB */ {
treeAgain.mergeProposedItem(proposed1Again);
treeAgain.mergeProposedItem(proposed2Again);
- treeAgain.mergeProposedItem(proposed1Again);
treeAgain.mergeProposedItem(proposed3Again);
treeAgain.buildForMerge();