killbill-memoizeit

invoice: fix usage invoicing after BCD change When computing

11/29/2018 7:02:50 AM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
index 52efaf9..29dd1fb 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithBCDUpdate.java
@@ -27,6 +27,7 @@ import javax.inject.Inject;
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountData;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
 import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
@@ -54,13 +55,13 @@ import com.google.common.collect.ImmutableList;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
 
 public class TestWithBCDUpdate extends TestIntegrationBase {
 
     @Inject
     protected SubscriptionBaseInternalApi subscriptionBaseInternalApi;
 
-
     @Test(groups = "slow")
     public void testBCDChangeInTrial() throws Exception {
 
@@ -123,7 +124,6 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         assertEquals(subscription.getBillingEndDate().compareTo(new LocalDate(2016, 5, 15)), 0);
     }
 
-
     @Test(groups = "slow")
     public void testBCDChangeAfterTrialFollowOtherBCDChange() throws Exception {
 
@@ -203,7 +203,6 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
 
     }
 
-
     @Test(groups = "slow")
     public void testBCDChangeBeforeChangePlan() throws Exception {
 
@@ -250,7 +249,6 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
 
     }
 
-
     @Test(groups = "slow")
     public void testBCDChangeAfterChangePlan() throws Exception {
 
@@ -347,7 +345,6 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
 
     }
 
-
     @Test(groups = "slow")
     public void testBCDChangeForAO() throws Exception {
 
@@ -494,7 +491,6 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
         expectedInvoices.clear();
     }
 
-
     @Test(groups = "slow")
     public void testBCDChangeWithEffectiveDateFromInTheFuture() throws Exception {
 
@@ -741,8 +737,6 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
                                     new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 15), new LocalDate(2016, 7, 15), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
     }
 
-
-
     @Test(groups = "slow")
     public void testWithBCDOnOperations() throws Exception {
 
@@ -793,4 +787,72 @@ public class TestWithBCDUpdate extends TestIntegrationBase {
 
 
     }
+
+    @Test(groups = "slow")
+    public void testBCDChangeForConsumableInArrearPlan() throws Exception {
+        // We take april as it has 30 days (easier to play with BCD)
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(new LocalDate(2012, 4, 1));
+
+        final AccountData accountData = getAccountData(1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        // Create BASE subscription
+        final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        // Check bundle after BP got created otherwise we get an error from auditApi.
+        subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+        invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        assertListenerStatus();
+
+        // Add ADD_ON on the same day
+        final DefaultEntitlement aoSubscription = addAOEntitlementAndCheckForCompletion(bpSubscription.getBundleId(), "Bullets", ProductCategory.ADD_ON, BillingPeriod.NO_BILLING_PERIOD, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+        assertListenerStatus();
+        assertNull(bpSubscription.getSubscriptionBase().getChargedThroughDate());
+
+        // Record usage for first month
+        recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 5), 100L, callContext);
+        recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
+
+        // 2012-05-01
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2013, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.USAGE, new BigDecimal("5.90")));
+
+        final DateTime bpExpectedCTD = account.getReferenceTime().withYear(2013).withMonthOfYear(5).withDayOfMonth(1);
+        assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(bpExpectedCTD), 0);
+        DateTime aoExpectedCTD = account.getReferenceTime().withMonthOfYear(5).withDayOfMonth(1);
+        assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(aoExpectedCTD), 0);
+
+        // 2012-05-05
+        clock.addDays(4);
+        assertListenerStatus();
+
+        // Set BCD to be the 5
+        busHandler.pushExpectedEvents(NextEvent.BCD_CHANGE, NextEvent.INVOICE);
+        subscriptionBaseInternalApi.updateBCD(aoSubscription.getId(), 5, null, internalCallContext);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 5), InvoiceItemType.USAGE, BigDecimal.ZERO));
+
+        // Record usage for second month
+        recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 5), 100L, callContext);
+        recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 4), 100L, callContext);
+
+        // 2012-06-05
+        busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 5), new LocalDate(2012, 6, 5), InvoiceItemType.USAGE, new BigDecimal("5.90")));
+
+        aoExpectedCTD = account.getReferenceTime().withMonthOfYear(6).withDayOfMonth(5);
+        assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(aoExpectedCTD), 0);
+    }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
index 98def8c..068e61d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java
@@ -129,23 +129,21 @@ public abstract class ContiguousIntervalUsageInArrear {
         }
         final LocalDate endDate = closedInterval ? internalTenantContext.toLocalDate(billingEvents.get(billingEvents.size() - 1).getEffectiveDate()) : targetDate;
 
-        final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, endDate, targetDate, getBCD(), usage.getBillingPeriod(), usage.getBillingMode());
-
-        int numberOfPeriod = 0;
-        // First billingCycleDate prior startDate
-        LocalDate nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
         if (startDate.compareTo(rawUsageStartDate) >= 0) {
             transitionTimes.add(startDate);
         }
-        while (!nextBillCycleDate.isAfter(endDate)) {
-            if (nextBillCycleDate.isAfter(startDate)) {
-                if (nextBillCycleDate.compareTo(rawUsageStartDate) >= 0) {
-                    transitionTimes.add(nextBillCycleDate);
-                }
+
+        for (int i = 0; i < billingEvents.size(); i++) {
+            final BillingEvent billingEvent = billingEvents.get(i);
+            if (i == billingEvents.size() - 1) {
+                addTransitionTimesForBillingEvent(startDate, endDate, billingEvent.getBillCycleDayLocal());
+            } else {
+                final BillingEvent nextBillingEvent = billingEvents.get(i + 1);
+                final LocalDate nextEndDate = internalTenantContext.toLocalDate(nextBillingEvent.getEffectiveDate());
+                addTransitionTimesForBillingEvent(startDate, nextEndDate, billingEvent.getBillCycleDayLocal());
             }
-            numberOfPeriod++;
-            nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
         }
+
         if (closedInterval &&
             transitionTimes.size() > 0 &&
             endDate.isAfter(transitionTimes.get(transitionTimes.size() - 1))) {
@@ -155,6 +153,23 @@ public abstract class ContiguousIntervalUsageInArrear {
         return this;
     }
 
+    private void addTransitionTimesForBillingEvent(final LocalDate startDate, final LocalDate endDate, final int bcd) {
+        final BillingIntervalDetail bid = new BillingIntervalDetail(startDate, endDate, targetDate, bcd, usage.getBillingPeriod(), usage.getBillingMode());
+
+        int numberOfPeriod = 0;
+        // First billingCycleDate prior startDate
+        LocalDate nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
+        while (!nextBillCycleDate.isAfter(endDate)) {
+            if (transitionTimes.isEmpty() || nextBillCycleDate.isAfter(transitionTimes.get(transitionTimes.size() - 1))) {
+                if (nextBillCycleDate.compareTo(rawUsageStartDate) >= 0) {
+                    transitionTimes.add(nextBillCycleDate);
+                }
+            }
+            numberOfPeriod++;
+            nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
+        }
+    }
+
     /**
      * Compute the missing usage invoice items based on what should be billed and what has been billed ($ amount comparison).
      *
@@ -413,10 +428,6 @@ public abstract class ContiguousIntervalUsageInArrear {
         return usage;
     }
 
-    public int getBCD() {
-        return billingEvents.get(0).getBillCycleDayLocal();
-    }
-
     public UUID getBundleId() {
         return billingEvents.get(0).getSubscription().getBundleId();
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
index bb3d830..1ec2bf6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/SubscriptionUsageInArrear.java
@@ -138,9 +138,6 @@ public class SubscriptionUsageInArrear {
         final Set<UsageKey> allSeenUsage = new HashSet<UsageKey>();
 
         for (final BillingEvent event : subscriptionBillingEvents) {
-
-
-
             // Extract all in arrear /consumable usage section for that billing event.
             final List<Usage> usages = findUsageInArrearUsages(event);
             allSeenUsage.addAll(Collections2.transform(usages, new Function<Usage, UsageKey>() {