killbill-memoizeit

Details

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
index 8c31b4b..aaec4eb 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
@@ -24,14 +24,13 @@ import java.util.UUID;
 import org.joda.time.DateTime;
 import org.joda.time.Days;
 import org.joda.time.LocalDate;
-
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.generator.InvoiceDateUtils;
 import org.killbill.billing.invoice.model.RecurringInvoiceItem;
 import org.killbill.billing.invoice.model.RepairAdjInvoiceItem;
-import org.killbill.billing.util.currency.KillBillMoney;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 
@@ -135,8 +134,7 @@ public class Item {
             return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, positiveAmount, rate, currency);
         } else {
             // We first compute the maximum amount after adjustment and that sets the amount limit of how much can be repaired.
-            final BigDecimal maxAvailableAmountAfterAdj = amount.subtract(adjustedAmount);
-            final BigDecimal maxAvailableAmountForRepair = maxAvailableAmountAfterAdj.subtract(currentRepairedAmount);
+            final BigDecimal maxAvailableAmountForRepair = getNetAmount();
             final BigDecimal positiveAmountForRepair = positiveAmount.compareTo(maxAvailableAmountForRepair) <= 0 ? positiveAmount : maxAvailableAmountForRepair;
             return positiveAmountForRepair.compareTo(BigDecimal.ZERO) > 0 ? new RepairAdjInvoiceItem(targetInvoiceId, accountId, newStartDate, newEndDate, positiveAmountForRepair.negate(), currency, linkedId) : null;
         }
@@ -152,6 +150,11 @@ public class Item {
         currentRepairedAmount = currentRepairedAmount.add(increment);
     }
 
+    @JsonIgnore
+    public BigDecimal getNetAmount() {
+        return amount.subtract(adjustedAmount).subtract(currentRepairedAmount);
+    }
+
     public ItemAction getAction() {
         return action;
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
index c840135..63d0bd8 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -27,6 +27,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.joda.time.LocalDate;
 import org.killbill.billing.invoice.api.InvoiceItem;
@@ -323,9 +324,7 @@ public class ItemsNodeInterval extends NodeInterval {
                                    public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
                                        final ItemsInterval curChildItems = ((ItemsNodeInterval) curNode).getItemsInterval();
                                        final Item cancelledItem = curChildItems.getCancelledItemIfExists(curCancelItem.getLinkedId());
-                                       if (cancelledItem != null) {
-                                           throw new IllegalStateException(String.format("Invalid cancelledItem=%s for cancelItem=%s", cancelledItem, curCancelItem));
-                                       }
+                                       Preconditions.checkState(cancelledItem == null, "Invalid cancelledItem=%s for cancelItem=%s", cancelledItem, curCancelItem);
                                    }
                                });
                     }
@@ -339,8 +338,25 @@ public class ItemsNodeInterval extends NodeInterval {
                             return cancelledItem != null;
                         }
                     });
-                    if (nodeIntervalForCancelledItem == null) {
-                        throw new IllegalStateException(String.format("Missing cancelledItem for cancelItem=%s", curCancelItem));
+                    Preconditions.checkState(nodeIntervalForCancelledItem != null, "Missing cancelledItem for cancelItem=%s", curCancelItem);
+                }
+
+                for (final Item curAddItem : curNodeItems.get_ADD_items()) {
+                    // Sanity: verify the item hasn't been adjusted too much
+                    if (curNode.getLeftChild() != null) {
+                        final AtomicReference<BigDecimal> totalRepaired = new AtomicReference<BigDecimal>(BigDecimal.ZERO);
+                        curNode.getLeftChild()
+                               .walkTree(new WalkCallback() {
+                                   @Override
+                                   public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+                                       final ItemsInterval curChildItems = ((ItemsNodeInterval) curNode).getItemsInterval();
+                                       final Item cancelledItem = curChildItems.getCancellingItemIfExists(curAddItem.getId());
+                                       if (cancelledItem != null) {
+                                           totalRepaired.set(totalRepaired.get().add(cancelledItem.getAmount()));
+                                       }
+                                   }
+                               });
+                        Preconditions.checkState(curAddItem.getNetAmount().compareTo(totalRepaired.get()) >= 0, "Item %s overly repaired", curAddItem);
                     }
                 }
 
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 16d7918..e340aab 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -985,6 +985,263 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
         }
     }
 
+    @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+    public void testItemPartiallyRepairedAndPartiallyAdjusted() throws InvoiceApiException {
+        final LocalDate startDate = new LocalDate("2016-01-01");
+
+        final BillingEventSet events = new MockBillingEventSet();
+        final BigDecimal amount = BigDecimal.TEN;
+        final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+        final Plan plan = new MockPlan("my-plan");
+        final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+        final BillingEvent event1 = invoiceUtil.createMockBillingEvent(account,
+                                                                      subscription,
+                                                                      startDate.toDateTimeAtStartOfDay(),
+                                                                      plan,
+                                                                      planPhase,
+                                                                      null,
+                                                                      amount,
+                                                                      account.getCurrency(),
+                                                                      BillingPeriod.MONTHLY,
+                                                                      1,
+                                                                      BillingMode.IN_ADVANCE,
+                                                                      "Billing Event Desc",
+                                                                      1L,
+                                                                      SubscriptionBaseTransitionType.CREATE);
+        events.add(event1);
+        final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account,
+                                                                       subscription,
+                                                                       startDate.plusDays(1).toDateTimeAtStartOfDay(),
+                                                                       plan,
+                                                                       planPhase,
+                                                                       null,
+                                                                       null,
+                                                                       Currency.USD,
+                                                                       BillingPeriod.NO_BILLING_PERIOD,
+                                                                       1,
+                                                                       BillingMode.IN_ADVANCE,
+                                                                       "Billing Event Desc",
+                                                                       2L,
+                                                                       SubscriptionBaseTransitionType.CANCEL);
+        events.add(event2);
+
+        // Subscription incorrectly invoiced
+        final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+        final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+        invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+                                                        startDate.toDateTimeAtStartOfDay(),
+                                                        invoice.getId(),
+                                                        account.getId(),
+                                                        subscription.getBundleId(),
+                                                        subscription.getId(),
+                                                        plan.getName(),
+                                                        planPhase.getName(),
+                                                        startDate,
+                                                        startDate.plusMonths(1),
+                                                        amount,
+                                                        amount,
+                                                        account.getCurrency()));
+        // Repaired by the system
+        invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+                                                        startDate.toDateTimeAtStartOfDay(),
+                                                        invoice.getId(),
+                                                        account.getId(),
+                                                        startDate.plusDays(1),
+                                                        startDate.plusMonths(1),
+                                                        new BigDecimal("9.68").negate(),
+                                                        account.getCurrency(),
+                                                        invoice.getInvoiceItems().get(0).getId()));
+        // Item adjust the remaining
+        invoice.addInvoiceItem(new ItemAdjInvoiceItem(invoice.getInvoiceItems().get(0),
+                                                      startDate,
+                                                      new BigDecimal("0.32").negate(),
+                                                      account.getCurrency()));
+        existingInvoices.add(invoice);
+
+        final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+                                                                                                     UUID.randomUUID(),
+                                                                                                     events,
+                                                                                                     existingInvoices,
+                                                                                                     startDate,
+                                                                                                     account.getCurrency(),
+                                                                                                     new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+                                                                                                     internalCallContext);
+        assertTrue(generatedItems.isEmpty());
+    }
+
+    @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+    public void testItemPartiallyRepairedAndPartiallyAdjustedV2() throws InvoiceApiException {
+        final LocalDate startDate = new LocalDate("2016-01-01");
+
+        final BillingEventSet events = new MockBillingEventSet();
+        final BigDecimal amount = BigDecimal.TEN;
+        final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+        final Plan plan = new MockPlan("my-plan");
+        final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+        final BillingEvent event1 = invoiceUtil.createMockBillingEvent(account,
+                                                                      subscription,
+                                                                      startDate.toDateTimeAtStartOfDay(),
+                                                                      plan,
+                                                                      planPhase,
+                                                                      null,
+                                                                      amount,
+                                                                      account.getCurrency(),
+                                                                      BillingPeriod.MONTHLY,
+                                                                      1,
+                                                                      BillingMode.IN_ADVANCE,
+                                                                      "Billing Event Desc",
+                                                                      1L,
+                                                                      SubscriptionBaseTransitionType.CREATE);
+        events.add(event1);
+        final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account,
+                                                                       subscription,
+                                                                       startDate.plusDays(1).toDateTimeAtStartOfDay(),
+                                                                       plan,
+                                                                       planPhase,
+                                                                       null,
+                                                                       null,
+                                                                       Currency.USD,
+                                                                       BillingPeriod.NO_BILLING_PERIOD,
+                                                                       1,
+                                                                       BillingMode.IN_ADVANCE,
+                                                                       "Billing Event Desc",
+                                                                       2L,
+                                                                       SubscriptionBaseTransitionType.CANCEL);
+        events.add(event2);
+
+        // Subscription incorrectly invoiced
+        final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+        final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+        invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+                                                        startDate.toDateTimeAtStartOfDay(),
+                                                        invoice.getId(),
+                                                        account.getId(),
+                                                        subscription.getBundleId(),
+                                                        subscription.getId(),
+                                                        plan.getName(),
+                                                        planPhase.getName(),
+                                                        startDate,
+                                                        startDate.plusMonths(1),
+                                                        amount,
+                                                        amount,
+                                                        account.getCurrency()));
+        // Item adjust the remaining
+        invoice.addInvoiceItem(new ItemAdjInvoiceItem(invoice.getInvoiceItems().get(0),
+                                                      startDate,
+                                                      BigDecimal.ONE.negate(),
+                                                      account.getCurrency()));
+        // Repaired by the system (the system would have consumed all the remaining amount available)
+        invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+                                                        startDate.toDateTimeAtStartOfDay(),
+                                                        invoice.getId(),
+                                                        account.getId(),
+                                                        startDate.plusDays(1),
+                                                        startDate.plusMonths(1),
+                                                        new BigDecimal("9").negate(),
+                                                        account.getCurrency(),
+                                                        invoice.getInvoiceItems().get(0).getId()));
+        existingInvoices.add(invoice);
+
+        final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+                                                                                                     UUID.randomUUID(),
+                                                                                                     events,
+                                                                                                     existingInvoices,
+                                                                                                     startDate,
+                                                                                                     account.getCurrency(),
+                                                                                                     new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+                                                                                                     internalCallContext);
+        assertTrue(generatedItems.isEmpty());
+    }
+
+    @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+    public void testItemPartiallyRepairedAndInvalidAdjustment() throws InvoiceApiException {
+        final LocalDate startDate = new LocalDate("2016-01-01");
+
+        final BillingEventSet events = new MockBillingEventSet();
+        final BigDecimal amount = BigDecimal.TEN;
+        final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+        final Plan plan = new MockPlan("my-plan");
+        final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+        final BillingEvent event1 = invoiceUtil.createMockBillingEvent(account,
+                                                                      subscription,
+                                                                      startDate.toDateTimeAtStartOfDay(),
+                                                                      plan,
+                                                                      planPhase,
+                                                                      null,
+                                                                      amount,
+                                                                      account.getCurrency(),
+                                                                      BillingPeriod.MONTHLY,
+                                                                      1,
+                                                                      BillingMode.IN_ADVANCE,
+                                                                      "Billing Event Desc",
+                                                                      1L,
+                                                                      SubscriptionBaseTransitionType.CREATE);
+        events.add(event1);
+        final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account,
+                                                                       subscription,
+                                                                       startDate.plusDays(1).toDateTimeAtStartOfDay(),
+                                                                       plan,
+                                                                       planPhase,
+                                                                       null,
+                                                                       null,
+                                                                       Currency.USD,
+                                                                       BillingPeriod.NO_BILLING_PERIOD,
+                                                                       1,
+                                                                       BillingMode.IN_ADVANCE,
+                                                                       "Billing Event Desc",
+                                                                       2L,
+                                                                       SubscriptionBaseTransitionType.CANCEL);
+        events.add(event2);
+
+        // Subscription incorrectly invoiced
+        final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+        final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+        invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+                                                        startDate.toDateTimeAtStartOfDay(),
+                                                        invoice.getId(),
+                                                        account.getId(),
+                                                        subscription.getBundleId(),
+                                                        subscription.getId(),
+                                                        plan.getName(),
+                                                        planPhase.getName(),
+                                                        startDate,
+                                                        startDate.plusMonths(1),
+                                                        amount,
+                                                        amount,
+                                                        account.getCurrency()));
+        // Repaired by the system
+        invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+                                                        startDate.toDateTimeAtStartOfDay(),
+                                                        invoice.getId(),
+                                                        account.getId(),
+                                                        startDate.plusDays(1),
+                                                        startDate.plusMonths(1),
+                                                        new BigDecimal("9.68").negate(),
+                                                        account.getCurrency(),
+                                                        invoice.getInvoiceItems().get(0).getId()));
+        // Invalid adjustment (too much)
+        invoice.addInvoiceItem(new ItemAdjInvoiceItem(invoice.getInvoiceItems().get(0),
+                                                      startDate,
+                                                      new BigDecimal("9.68").negate(),
+                                                      account.getCurrency()));
+        existingInvoices.add(invoice);
+
+        try {
+            final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+                                                                                                         UUID.randomUUID(),
+                                                                                                         events,
+                                                                                                         existingInvoices,
+                                                                                                         startDate,
+                                                                                                         account.getCurrency(),
+                                                                                                         new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+                                                                                                         internalCallContext);
+            fail();
+        } catch (final InvoiceApiException e) {
+            assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+            assertTrue(e.getCause().getMessage().endsWith("overly repaired"));
+        }
+    }
+
     // Simulate a bug in the generator where two fixed items for the same day and subscription end up in the resulting items
     @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
     public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDatePostMerge() throws InvoiceApiException {