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();
}