killbill-aplcache
Changes
Details
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
new file mode 100644
index 0000000..99739c0
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java
@@ -0,0 +1,100 @@
+package com.ning.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+
+import com.google.common.collect.Lists;
+
+public class ItemsInterval {
+
+ private List<InvoiceItem> items;
+
+ public ItemsInterval() {
+ this(null);
+ }
+
+ public ItemsInterval(final InvoiceItem initialItem) {
+ this.items = Lists.newLinkedList();
+ if (initialItem != null) {
+ items.add(initialItem);
+ }
+ }
+
+ public InvoiceItem createRecuringItem(LocalDate startDate, LocalDate endDate) {
+ Iterator<InvoiceItem> it = items.iterator();
+ while (it.hasNext()) {
+ final InvoiceItem cur = it.next();
+ if (cur.getInvoiceItemType() == InvoiceItemType.RECURRING) {
+ // TODO STEPH calculate amount
+ final BigDecimal amount = BigDecimal.ONE;
+ return new RecurringInvoiceItem(cur.getInvoiceId(), cur.getAccountId(), cur.getBundleId(), cur.getSubscriptionId(),
+ cur.getPlanName(), cur.getPhaseName(), startDate, endDate, amount, cur.getRate(), cur.getCurrency());
+ }
+ }
+ return null;
+ }
+
+ public List<InvoiceItem> getItems() {
+ return items;
+ }
+
+ // Remove cancelling items
+ public void build(final List<InvoiceItem> output) {
+
+ boolean foundRecuring = false;
+
+ Iterator<InvoiceItem> it = items.iterator();
+ while (it.hasNext()) {
+ final InvoiceItem cur = it.next();
+ switch (cur.getInvoiceItemType()) {
+ case FIXED:
+ // TODO Not implemented
+ break;
+
+ case RECURRING:
+ foundRecuring = true;
+ output.add(cur);
+ break;
+
+ case REPAIR_ADJ:
+ if (!foundRecuring) {
+ output.add(cur);
+ }
+ break;
+
+ case ITEM_ADJ:
+ // TODO Not implemented
+ break;
+
+ // Ignored
+ case EXTERNAL_CHARGE:
+ case CBA_ADJ:
+ case CREDIT_ADJ:
+ case REFUND_ADJ:
+ default:
+ }
+ }
+ }
+
+ public void insertSortedItem(final InvoiceItem item) {
+ items.add(item);
+ Collections.sort(items, new Comparator<InvoiceItem>() {
+ @Override
+ public int compare(final InvoiceItem o1, final InvoiceItem o2) {
+
+ final int type1 = o1.getInvoiceItemType().ordinal();
+ final int type2 = o2.getInvoiceItemType().ordinal();
+ return (type1 < type2) ? -1 : ((type1 == type2) ? 0 : 1);
+ }
+ });
+ }
+}
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
new file mode 100644
index 0000000..e99e5a4
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.invoice.tree;
+
+import java.util.List;
+
+import org.joda.time.LocalDate;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+
+import com.google.common.collect.Lists;
+
+public class NodeInterval {
+
+ private LocalDate start;
+ private LocalDate end;
+ private ItemsInterval items;
+
+ private NodeInterval parent;
+ private NodeInterval leftChild;
+ private NodeInterval rightSibling;
+
+ public NodeInterval() {
+ this.items = new ItemsInterval();
+ }
+
+ public NodeInterval(final NodeInterval parent, final InvoiceItem item) {
+ this.start = item.getStartDate();
+ this.end = item.getEndDate();
+ this.items = new ItemsInterval(item);
+ this.parent = parent;
+ this.leftChild = null;
+ this.rightSibling = null;
+ }
+
+ public void build(final List<InvoiceItem> output) {
+
+ /*
+ // Compute start and end
+ if (isRoot()) {
+ this.start = leftChild.getStart();
+ NodeInterval cur = leftChild;
+ while (cur.getRightSibling() != null) {
+ cur = cur.getRightSibling();
+ }
+ this.end = cur.getEnd();
+ }
+ */
+
+ // There is no sub-interval, just add our own items.
+ if (leftChild == null) {
+ items.build(output);
+ return;
+ }
+
+ LocalDate curDate = start;
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ if (curChild.getStart().compareTo(curDate) > 0) {
+ output.add(items.createRecuringItem(curDate, curChild.getStart()));
+ }
+ curChild.build(output);
+ curDate = curChild.getEnd();
+ curChild = curChild.getRightSibling();
+ }
+ if (curDate.compareTo(end) < 0) {
+ output.add(items.createRecuringItem(curDate, end));
+ }
+ }
+
+ public boolean isItemContained(final InvoiceItem item) {
+ return (item.getStartDate().compareTo(start) >= 0 &&
+ item.getStartDate().compareTo(end) <= 0 &&
+ item.getEndDate().compareTo(start) >= 0 &&
+ item.getEndDate().compareTo(end) <= 0);
+ }
+
+ public boolean isItemOverlap(final InvoiceItem item) {
+ return ((item.getStartDate().compareTo(start) < 0 &&
+ item.getEndDate().compareTo(end) >= 0) ||
+ (item.getStartDate().compareTo(start) <= 0 &&
+ item.getEndDate().compareTo(end) > 0));
+ }
+
+ public void addItem(final InvoiceItem item) {
+
+ if (!isRoot() && item.getStartDate().compareTo(start) == 0 && item.getEndDate().compareTo(end) == 0) {
+ items.insertSortedItem(item);
+ return;
+ }
+
+ final NodeInterval newNode = new NodeInterval(this, item);
+ computeRootInterval(newNode);
+ addNode(newNode);
+ }
+
+ private void addNode(final NodeInterval newNode) {
+ final InvoiceItem item = newNode.getItems().get(0);
+ if (leftChild == null) {
+ leftChild = newNode;
+ return;
+ }
+
+ NodeInterval prevChild = null;
+ NodeInterval curChild = leftChild;
+ do {
+ if (curChild.isItemContained(item)) {
+ curChild.addItem(item);
+ 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;
+ } while (curChild != null);
+
+ prevChild.rightSibling = newNode;
+ }
+
+ private void rebalance(final NodeInterval newNode) {
+
+ final InvoiceItem invoiceItem = newNode.getItems().get(0);
+
+ NodeInterval prevRebalanced = null;
+ NodeInterval curChild = leftChild;
+ List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
+ do {
+ if (curChild.isItemOverlap(invoiceItem)) {
+ toBeRebalanced.add(curChild);
+ } else {
+ if (toBeRebalanced.size() > 0) {
+ break;
+ }
+ prevRebalanced = curChild;
+ }
+ curChild = curChild.rightSibling;
+ } while (curChild != null);
+
+ newNode.rightSibling = toBeRebalanced.get(toBeRebalanced.size() - 1).rightSibling;
+ if (prevRebalanced == null) {
+ leftChild = newNode;
+ } else {
+ prevRebalanced.rightSibling = newNode;
+ }
+
+ for (NodeInterval cur : toBeRebalanced) {
+ newNode.addNode(cur);
+ }
+ }
+
+ private void computeRootInterval(final NodeInterval newNode) {
+ if (!isRoot()) {
+ return;
+ }
+ this.start = (start == null || start.compareTo(newNode.getStart()) > 0) ? newNode.getStart() : start;
+ this.end = (end == null || end.compareTo(newNode.getEnd()) < 0) ? newNode.getEnd() : end;
+ }
+
+ public boolean isRoot() {
+ return parent == null;
+ }
+
+ public LocalDate getStart() {
+ return start;
+ }
+
+ public LocalDate getEnd() {
+ return end;
+ }
+
+ public NodeInterval getParent() {
+ return parent;
+ }
+
+ public NodeInterval getLeftChild() {
+ return leftChild;
+ }
+
+ public NodeInterval getRightSibling() {
+ return rightSibling;
+ }
+
+ public List<InvoiceItem> getItems() {
+ return items.getItems();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof NodeInterval)) {
+ return false;
+ }
+
+ final NodeInterval that = (NodeInterval) o;
+
+ if (end != null ? !end.equals(that.end) : that.end != null) {
+ return false;
+ }
+ if (items != null ? !items.equals(that.items) : that.items != null) {
+ return false;
+ }
+ if (leftChild != null ? !leftChild.equals(that.leftChild) : that.leftChild != null) {
+ return false;
+ }
+ if (parent != null ? !parent.equals(that.parent) : that.parent != null) {
+ return false;
+ }
+ if (rightSibling != null ? !rightSibling.equals(that.rightSibling) : that.rightSibling != null) {
+ return false;
+ }
+ if (start != null ? !start.equals(that.start) : that.start != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = start != null ? start.hashCode() : 0;
+ result = 31 * result + (end != null ? end.hashCode() : 0);
+ result = 31 * result + (items != null ? items.hashCode() : 0);
+ result = 31 * result + (parent != null ? parent.hashCode() : 0);
+ result = 31 * result + (leftChild != null ? leftChild.hashCode() : 0);
+ result = 31 * result + (rightSibling != null ? rightSibling.hashCode() : 0);
+ return result;
+ }
+}
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
new file mode 100644
index 0000000..f82ee25
--- /dev/null
+++ b/invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java
@@ -0,0 +1,57 @@
+package com.ning.billing.invoice.tree;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.ning.billing.invoice.api.InvoiceItem;
+
+public class SubscriptionItemTree {
+
+ private final UUID subscriptionId;
+ private final NodeInterval root;
+
+ public SubscriptionItemTree(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ this.root = new NodeInterval();
+ }
+
+ public void addItem(final InvoiceItem item) {
+ root.addItem(item);
+ }
+
+
+ public void build(final List<InvoiceItem> output) {
+ // compute start and end for root
+
+ root.build(output);
+
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SubscriptionItemTree)) {
+ return false;
+ }
+
+ final SubscriptionItemTree that = (SubscriptionItemTree) o;
+
+ if (root != null ? !root.equals(that.root) : that.root != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = subscriptionId != null ? subscriptionId.hashCode() : 0;
+ result = 31 * result + (root != null ? root.hashCode() : 0);
+ return result;
+ }
+}
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
new file mode 100644
index 0000000..086c74e
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -0,0 +1,202 @@
+package com.ning.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.invoice.model.RepairAdjInvoiceItem;
+
+import com.google.common.collect.Lists;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class TestSubscriptionItemTree /* 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;
+
+ @Test(groups = "fast")
+ public void testSimpleRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate repairDate = new LocalDate(2014, 1, 23);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final BigDecimal rate2 = new BigDecimal("14.85");
+ final BigDecimal amount2 = rate2;
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem newItem = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", repairDate, endDate, amount2, rate2, currency);
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate, endDate, amount1.negate(), currency, initial.getId());
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, repairDate, BigDecimal.ONE, rate1, currency);
+ expectedResult.add(expected1);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "someelse", "someelse", repairDate, endDate, amount2, rate2, currency);
+ expectedResult.add(expected2);
+
+ // First test with items in order
+ List<InvoiceItem> result = Lists.newLinkedList();
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(initial);
+ tree.addItem(newItem);
+ tree.addItem(repair);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+
+ result = Lists.newLinkedList();
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair);
+ tree.addItem(newItem);
+ tree.addItem(initial);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+
+ result = Lists.newLinkedList();
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair);
+ tree.addItem(initial);
+ tree.addItem(newItem);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+ }
+
+ @Test(groups = "fast")
+ public void testMultipleRepair() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate repairDate1 = new LocalDate(2014, 1, 23);
+
+ final LocalDate repairDate2 = new LocalDate(2014, 1, 26);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+ final BigDecimal rate2 = new BigDecimal("14.85");
+ final BigDecimal amount2 = rate2;
+
+ final BigDecimal rate3 = new BigDecimal("19.23");
+ final BigDecimal amount3 = rate3;
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem newItem1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate1, endDate, amount2, rate2, currency);
+ final InvoiceItem repair1 = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate1, endDate, amount1.negate(), currency, initial.getId());
+
+ final InvoiceItem newItem2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate2, endDate, amount3, rate3, currency);
+ final InvoiceItem repair2 = new RepairAdjInvoiceItem(invoiceId, accountId, repairDate2, endDate, amount2.negate(), currency, initial.getId());
+
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, repairDate1, BigDecimal.ONE, rate1, currency);
+ expectedResult.add(expected1);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate1, repairDate2, BigDecimal.ONE, rate2, currency);
+ expectedResult.add(expected2);
+ final InvoiceItem expected3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, repairDate2, endDate, amount3, rate3, currency);
+ expectedResult.add(expected3);
+
+
+ // First test with items in order
+ List<InvoiceItem> result = Lists.newLinkedList();
+
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(initial);
+ tree.addItem(newItem1);
+ tree.addItem(repair1);
+ tree.addItem(newItem2);
+ tree.addItem(repair2);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+
+ result = Lists.newLinkedList();
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair2);
+ tree.addItem(newItem1);
+ tree.addItem(newItem2);
+ tree.addItem(repair1);
+ tree.addItem(initial);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+
+ result = Lists.newLinkedList();
+ tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(repair1);
+ tree.addItem(newItem1);
+ tree.addItem(initial);
+ tree.addItem(repair2);
+ tree.addItem(newItem2);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+ }
+
+
+ @Test(groups = "fast")
+ public void testMultipleBlockedBillings() {
+
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final LocalDate blockStart1 = new LocalDate(2014, 1, 8);
+ final LocalDate unblockStart1 = new LocalDate(2014, 1, 10);
+
+ final LocalDate blockStart2 = new LocalDate(2014, 1, 17);
+ final LocalDate unblockStart2 = new LocalDate(2014, 1, 23);
+
+ final BigDecimal rate1 = new BigDecimal("12.00");
+ final BigDecimal amount1 = rate1;
+
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount1, rate1, currency);
+ final InvoiceItem block1 = new RepairAdjInvoiceItem(invoiceId, accountId, blockStart1, unblockStart1, amount1.negate(), currency, initial.getId());
+ final InvoiceItem block2 = new RepairAdjInvoiceItem(invoiceId, accountId, blockStart2, unblockStart2, amount1.negate(), currency, initial.getId());
+
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ final InvoiceItem expected1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, blockStart1, BigDecimal.ONE, rate1, currency);
+ expectedResult.add(expected1);
+ expectedResult.add(block1);
+ final InvoiceItem expected2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockStart1, blockStart2, BigDecimal.ONE, rate1, currency);
+ expectedResult.add(expected2);
+ expectedResult.add(block2);
+ final InvoiceItem expected3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, unblockStart2, endDate, BigDecimal.ONE, rate1, currency);
+ expectedResult.add(expected3);
+
+
+ // First test with items in order
+ List<InvoiceItem> result = Lists.newLinkedList();
+
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId);
+ tree.addItem(initial);
+ tree.addItem(block1);
+ tree.addItem(block2);
+ tree.build(result);
+ verifyResult(result, expectedResult);
+ }
+
+
+ private void verifyResult(final List<InvoiceItem> result, final List<InvoiceItem> expectedResult) {
+ assertEquals(result.size(), expectedResult.size());
+ for (int i = 0; i < expectedResult.size(); i++) {
+ assertTrue(result.get(i).matches(expectedResult.get(i)));
+ }
+ }
+
+}