killbill-aplcache

Prototype implementation for invoice rework (tree of items

2/6/2014 2:18:40 AM

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)));
+        }
+    }
+
+}