diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index e0ff429..7befc66 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -932,9 +932,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
checkODState("OD1");
checkChangePlanWithOverdueState(baseEntitlement, true, true);
- invoiceChecker.checkInvoice(account.getId(), 4, callContext,
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
- new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 31), new LocalDate(2012, 8, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
// Fully adjust all invoices
final List<Invoice> invoicesToAdjust = getUnpaidInvoicesOrderFromRecent();
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index 81ec626..360216e 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -26,7 +26,6 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.LocalDate;
-
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.tree.Item.ItemAction;
@@ -48,7 +47,9 @@ public class SubscriptionItemTree {
private ItemsNodeInterval root;
private boolean isBuilt;
+ private boolean isMerged;
private List<Item> items;
+ private List<InvoiceItem> allExistingRecurringItems;
private List<InvoiceItem> existingFixedItems;
private Map<LocalDate, InvoiceItem> remainingFixedItems;
private List<InvoiceItem> pendingItemAdj;
@@ -78,6 +79,7 @@ public class SubscriptionItemTree {
this.targetInvoiceId = targetInvoiceId;
this.root = new ItemsNodeInterval(targetInvoiceId);
this.items = new LinkedList<Item>();
+ this.allExistingRecurringItems = new LinkedList<InvoiceItem>();
this.existingFixedItems = new LinkedList<InvoiceItem>();
this.remainingFixedItems = new HashMap<LocalDate, InvoiceItem>();
this.pendingItemAdj = new LinkedList<InvoiceItem>();
@@ -122,6 +124,7 @@ public class SubscriptionItemTree {
Preconditions.checkState(!isBuilt);
root.mergeExistingAndProposed(items);
isBuilt = true;
+ isMerged = true;
}
/**
@@ -134,6 +137,7 @@ public class SubscriptionItemTree {
Preconditions.checkState(!isBuilt);
switch (invoiceItem.getInvoiceItemType()) {
case RECURRING:
+ allExistingRecurringItems.add(invoiceItem);
root.addExistingItem(new ItemsNodeInterval(root, targetInvoiceId, new Item(invoiceItem, targetInvoiceId, ItemAction.ADD)));
break;
@@ -203,7 +207,22 @@ public class SubscriptionItemTree {
tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
@Override
public InvoiceItem apply(final Item input) {
- return input.toInvoiceItem();
+ final InvoiceItem resultingCandidate = input.toInvoiceItem();
+
+ // Post merge, the ADD items are the candidates for the resulting RECURRING items (see toInvoiceItem()).
+ // We will ignore any resulting item matching existing items on disk though as these are the result of full item adjustments.
+ // See https://github.com/killbill/killbill/issues/654
+ if (isMerged) {
+ for (final InvoiceItem existingRecurringItem : allExistingRecurringItems) {
+ // Note: we DO keep the item in case of partial matches, e.g. if the new proposed item end date is before
+ // the existing (adjusted) item. See TestSubscriptionItemTree#testMaxedOutProRation
+ if (resultingCandidate.matches(existingRecurringItem)) {
+ return null;
+ }
+ }
+ }
+
+ return resultingCandidate;
}
}), new Predicate<InvoiceItem>() {
@Override
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index f28bca2..25cdae3 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -57,6 +57,7 @@ import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.DefaultInvoicePayment;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
import org.killbill.billing.invoice.model.RecurringInvoiceItem;
import org.killbill.billing.invoice.model.RepairAdjInvoiceItem;
import org.killbill.billing.junction.BillingEvent;
@@ -1022,7 +1023,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
// Regression test for #170 (see https://github.com/killbill/killbill/pull/173)
@Test(groups = "fast")
public void testRegressionFor170() throws EntityPersistenceException, InvoiceApiException, CatalogApiException {
- final UUID accountId = UUID.randomUUID();
+ final UUID accountId = account.getId();
final Currency currency = Currency.USD;
final SubscriptionBase subscription = createSubscription();
final MockInternationalPrice recurringPrice = new MockInternationalPrice(new DefaultPrice(new BigDecimal("2.9500"), Currency.USD));
@@ -1069,34 +1070,30 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
- assertEquals(invoice.getNumberOfItems(), 7);
- assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getNumberOfItems(), 6);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2013, 6, 15));
- assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2013, 7, 15));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2013, 6, 21));
assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
- assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2013, 6, 15));
- assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2013, 6, 21));
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2013, 6, 26));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2013, 7, 15));
- assertEquals(invoice.getInvoiceItems().get(2).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
- assertEquals(invoice.getInvoiceItems().get(2).getStartDate(), new LocalDate(2013, 6, 26));
- assertEquals(invoice.getInvoiceItems().get(2).getEndDate(), new LocalDate(2013, 7, 15));
+ assertEquals(invoice.getInvoiceItems().get(2).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(2).getStartDate(), new LocalDate(2013, 7, 15));
+ assertEquals(invoice.getInvoiceItems().get(2).getEndDate(), new LocalDate(2013, 8, 15));
assertEquals(invoice.getInvoiceItems().get(3).getInvoiceItemType(), InvoiceItemType.RECURRING);
- assertEquals(invoice.getInvoiceItems().get(3).getStartDate(), new LocalDate(2013, 7, 15));
- assertEquals(invoice.getInvoiceItems().get(3).getEndDate(), new LocalDate(2013, 8, 15));
+ assertEquals(invoice.getInvoiceItems().get(3).getStartDate(), new LocalDate(2013, 8, 15));
+ assertEquals(invoice.getInvoiceItems().get(3).getEndDate(), new LocalDate(2013, 9, 15));
assertEquals(invoice.getInvoiceItems().get(4).getInvoiceItemType(), InvoiceItemType.RECURRING);
- assertEquals(invoice.getInvoiceItems().get(4).getStartDate(), new LocalDate(2013, 8, 15));
- assertEquals(invoice.getInvoiceItems().get(4).getEndDate(), new LocalDate(2013, 9, 15));
+ assertEquals(invoice.getInvoiceItems().get(4).getStartDate(), new LocalDate(2013, 9, 15));
+ assertEquals(invoice.getInvoiceItems().get(4).getEndDate(), new LocalDate(2013, 10, 15));
assertEquals(invoice.getInvoiceItems().get(5).getInvoiceItemType(), InvoiceItemType.RECURRING);
- assertEquals(invoice.getInvoiceItems().get(5).getStartDate(), new LocalDate(2013, 9, 15));
- assertEquals(invoice.getInvoiceItems().get(5).getEndDate(), new LocalDate(2013, 10, 15));
-
- assertEquals(invoice.getInvoiceItems().get(6).getInvoiceItemType(), InvoiceItemType.RECURRING);
- assertEquals(invoice.getInvoiceItems().get(6).getStartDate(), new LocalDate(2013, 10, 15));
- assertEquals(invoice.getInvoiceItems().get(6).getEndDate(), new LocalDate(2013, 11, 15));
+ assertEquals(invoice.getInvoiceItems().get(5).getStartDate(), new LocalDate(2013, 10, 15));
+ assertEquals(invoice.getInvoiceItems().get(5).getEndDate(), new LocalDate(2013, 11, 15));
// Add newly generated invoice to existing invoices
existingInvoices.add(invoice);
@@ -1205,6 +1202,55 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertTrue(invoice3.getBalance().compareTo(FIFTEEN.multiply(TWO).add(TWELVE)) == 0);
}
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/654")
+ public void testCancelEOTWithFullItemAdjustment() throws CatalogApiException, InvoiceApiException {
+ final BigDecimal rate = new BigDecimal("39.95");
+
+ final BillingEventSet events = new MockBillingEventSet(internalCallContext);
+
+ final SubscriptionBase sub = createSubscription();
+ final LocalDate startDate = invoiceUtil.buildDate(2016, 10, 9);
+ final LocalDate endDate = invoiceUtil.buildDate(2016, 11, 9);
+
+ final Plan plan = new MockPlan();
+ final PlanPhase phase = createMockMonthlyPlanPhase(rate);
+
+ final BillingEvent event = createBillingEvent(sub.getId(), sub.getBundleId(), startDate, plan, phase, 9);
+ events.add(event);
+
+ final LocalDate targetDate = invoiceUtil.buildDate(2016, 10, 9);
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+
+ assertNotNull(invoice);
+ assertEquals(invoice.getNumberOfItems(), 1);
+ assertEquals(invoice.getBalance(), KillBillMoney.of(rate, invoice.getCurrency()));
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), sub.getId());
+
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), startDate);
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), endDate);
+
+ // Cancel EOT and Add the item adjustment
+ final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account, sub, endDate.toDateTimeAtStartOfDay(),
+ null, phase,
+ ZERO, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, 9,
+ BillingMode.IN_ADVANCE, "Cancel", 2L,
+ SubscriptionBaseTransitionType.CANCEL);
+ events.add(event2);
+
+ final InvoiceItem itemAdj = new ItemAdjInvoiceItem(invoice.getInvoiceItems().get(0), new LocalDate(2016, 10, 12), rate.negate(), Currency.USD);
+
+ invoice.addInvoiceItem(itemAdj);
+
+ final List<Invoice> existingInvoices = new ArrayList<Invoice>();
+ existingInvoices.add(invoice);
+ final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext);
+ final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
+
+ assertNull(invoice2);
+ }
+
private void printDetailInvoice(final Invoice invoice) {
log.info("-------------------- START DETAIL ----------------------");
log.info("Invoice " + invoice.getId() + ": BALANCE = " + invoice.getBalance()