killbill-memoizeit
Changes
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 {