diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index ec6768b..eb85775 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -19,11 +19,14 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import javax.inject.Inject;
+
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
@@ -37,8 +40,13 @@ import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
@@ -52,6 +60,10 @@ import static org.testng.Assert.assertNotNull;
public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
+ @Inject
+ protected InvoiceDao invoiceDao;
+
+
@AfterMethod(groups = "slow")
public void afterMethod() throws Exception {
super.afterMethod();
@@ -607,4 +619,124 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
checkNoMoreInvoiceToGenerate(account);
}
+
+ @Test(groups = "slow")
+ public void testWithSuperflousRepairedItems() throws Exception {
+
+ // We take april as it has 30 days (easier to play with BCD)
+ final LocalDate today = new LocalDate(2012, 4, 1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(today);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String pricelistName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+
+ final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
+ assertNotNull(bpEntitlement);
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 1);
+ ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+ invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+ //
+ // Check we get the first invoice at the phase event
+ //
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ // Move the clock to 2012-05-02
+ clock.addDays(31);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 2);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+ invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ final Invoice lastInvoice = invoices.get(1);
+ invoiceChecker.checkInvoice(lastInvoice.getId(), callContext, toBeChecked);
+
+
+ //
+ // Let's add a bunch of items by hand to pretend we have lots of cancelling items that should be cleaned (data issue after a potential invoice bug)
+ //
+ // We test both full and partial repair.
+ final InvoiceModelDao shellInvoice = new InvoiceModelDao(UUID.randomUUID(), lastInvoice.getCreatedDate(), lastInvoice.getAccountId(), null,
+ lastInvoice.getInvoiceDate(), lastInvoice.getTargetDate(), lastInvoice.getCurrency(), false);
+
+ final InvoiceItemModelDao recurring1 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+ null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+ final InvoiceItemModelDao repair1 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+ null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("-249.95"), new BigDecimal("-249.95"), account.getCurrency(), recurring1.getId());
+
+ final InvoiceItemModelDao recurring2 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+ null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+
+ final InvoiceItemModelDao repair21 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+ null, new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 13), new BigDecimal("-100.95"), new BigDecimal("-100.95"), account.getCurrency(), recurring2.getId());
+
+ final InvoiceItemModelDao repair22 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+ null, new LocalDate(2012, 5, 13), new LocalDate(2012, 5, 22), new BigDecimal("-100"), new BigDecimal("-100"), account.getCurrency(), recurring2.getId());
+
+ final InvoiceItemModelDao repair23 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+ null, new LocalDate(2012, 5, 22), new LocalDate(2012, 6, 1), new BigDecimal("-49"), new BigDecimal("-49"), account.getCurrency(), recurring2.getId());
+
+
+
+ final InvoiceItemModelDao recurring3 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+ null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+
+ final InvoiceItemModelDao repair3 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+ bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+ null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("-249.95"), new BigDecimal("-249.95"), account.getCurrency(), recurring3.getId());
+
+ List<InvoiceItemModelDao> newItems = new ArrayList<InvoiceItemModelDao>();
+ newItems.add(recurring1);
+ newItems.add(repair1);
+ newItems.add(recurring2);
+ newItems.add(repair21);
+ newItems.add(repair22);
+ newItems.add(repair23);
+ newItems.add(recurring3);
+ newItems.add(repair3);
+ invoiceDao.createInvoice(shellInvoice, newItems, false, new FutureAccountNotifications(null, new HashMap<UUID, List<SubscriptionNotification>>()), internalCallContext);
+
+
+ // Move ahead one month, verify nothing from previous data was generated
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+ assertEquals(invoices.size(), 3);
+
+ toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+
+
+ }
+
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 102f9c7..edc9cf5 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -394,6 +394,189 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
verifyResult(tree.getView(), expectedResult);
}
+ // Will test the case A from ItemsNodeInterval#prune logic (when we delete a node while walking the tree)
+ @Test(groups = "fast")
+ public void testFullRepairPruneLogic1() {
+
+ final LocalDate startDate1 = new LocalDate(2015, 1, 1);
+ final LocalDate endDate1 = new LocalDate(2015, 2, 1);
+
+
+ final LocalDate startDate2 = endDate1;
+ final LocalDate endDate2 = new LocalDate(2015, 3, 1);
+
+ final LocalDate startDate3 = endDate2;
+ final LocalDate endDate3 = new LocalDate(2015, 4, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, endDate1, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repairMonthly2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, endDate2, new BigDecimal("-12.00"), currency, monthly2.getId());
+ final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate3, endDate3, monthlyAmount, monthlyRate, currency);
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly1);
+ expectedResult.add(monthly3);
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.addItem(repairMonthly2);
+ tree.addItem(monthly3);
+
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+
+ // Will test the case A from ItemsNodeInterval#prune logic (an item is left on the interval)
+ @Test(groups = "fast")
+ public void testFullRepairPruneLogic2() {
+
+ final LocalDate startDate1 = new LocalDate(2015, 1, 1);
+ final LocalDate endDate1 = new LocalDate(2015, 2, 1);
+
+
+ final LocalDate startDate2 = endDate1;
+ final LocalDate endDate2 = new LocalDate(2015, 3, 1);
+
+ final LocalDate startDate3 = endDate2;
+ final LocalDate endDate3 = new LocalDate(2015, 4, 1);
+
+ final BigDecimal monthlyRateInit = new BigDecimal("12.00");
+ final BigDecimal monthlyAmountInit = monthlyRateInit;
+
+ final BigDecimal monthlyRateFinal = new BigDecimal("15.00");
+ final BigDecimal monthlyAmountFinal = monthlyRateFinal;
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, endDate1, monthlyRateInit, monthlyAmountInit, currency);
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyRateInit, monthlyAmountInit, currency);
+ final InvoiceItem repairMonthly2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, endDate2, new BigDecimal("-12.00"), currency, monthly2.getId());
+
+ final InvoiceItem monthly2New = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyRateFinal, monthlyAmountFinal, currency);
+
+ final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate3, endDate3, monthlyRateFinal, monthlyAmountFinal, currency);
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly1);
+ expectedResult.add(monthly2New);
+ expectedResult.add(monthly3);
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(monthly1);
+ tree.addItem(monthly2);
+ tree.addItem(repairMonthly2);
+ tree.addItem(monthly2New);
+ tree.addItem(monthly3);
+
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+
+ }
+
+
+ // Will test the case B from ItemsNodeInterval#prune logic
+ @Test(groups = "fast")
+ public void testFullRepairByPartsPruneLogic1() {
+
+ final LocalDate startDate = new LocalDate(2015, 2, 1);
+ final LocalDate intermediate1 = new LocalDate(2015, 2, 8);
+ final LocalDate intermediate2 = new LocalDate(2015, 2, 16);
+ final LocalDate intermediate3 = new LocalDate(2015, 2, 24);
+ final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repair11 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly1.getId());
+ final InvoiceItem repair12 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly1.getId());
+ final InvoiceItem repair13 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly1.getId());
+ final InvoiceItem repair14 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly1.getId());
+
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repair21 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly2.getId());
+ final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly2.getId());
+ final InvoiceItem repair23 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly2.getId());
+ final InvoiceItem repair24 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly2.getId());
+
+ final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly3);
+
+ // First test with items in order
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(monthly1);
+ tree.addItem(repair11);
+ tree.addItem(repair12);
+ tree.addItem(repair13);
+ tree.addItem(repair14);
+
+ tree.addItem(monthly2);
+ tree.addItem(repair21);
+ tree.addItem(repair22);
+ tree.addItem(repair23);
+ tree.addItem(repair24);
+
+ tree.addItem(monthly3);
+
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+ // Will test the case A and B from ItemsNodeInterval#prune logic
+ @Test(groups = "fast")
+ public void testFullRepairByPartsPruneLogic2() {
+
+ final LocalDate startDate = new LocalDate(2015, 2, 1);
+ final LocalDate intermediate1 = new LocalDate(2015, 2, 8);
+ final LocalDate intermediate2 = new LocalDate(2015, 2, 16);
+ final LocalDate intermediate3 = new LocalDate(2015, 2, 24);
+ final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+ final BigDecimal monthlyRate = new BigDecimal("12.00");
+ final BigDecimal monthlyAmount = monthlyRate;
+
+
+ final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repair11 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, new BigDecimal("-12.00"), currency, monthly1.getId());
+
+ final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+ final InvoiceItem repair21 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly2.getId());
+ final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly2.getId());
+ final InvoiceItem repair23 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly2.getId());
+ final InvoiceItem repair24 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly2.getId());
+
+ final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+
+ final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+ expectedResult.add(monthly3);
+
+ // First test with items in order
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(monthly1);
+ tree.addItem(repair11);
+
+ tree.addItem(monthly2);
+ tree.addItem(repair21);
+ tree.addItem(repair22);
+ tree.addItem(repair23);
+ tree.addItem(repair24);
+
+ tree.addItem(monthly3);
+
+ tree.build();
+ verifyResult(tree.getView(), expectedResult);
+ }
+
+
+
@Test(groups = "fast")
public void testMergeWithNoExisting() {