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