killbill-uncached
Changes
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java 44(+28 -16)
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..e557041 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 = new DateTime("2013-05-01T00:00:00.000Z");
+ assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(bpExpectedCTD), 0);
+ DateTime aoExpectedCTD = new DateTime("2012-05-01T00:00:00.000Z");
+ 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 = new DateTime("2012-06-05T00:00:00.000Z");
+ assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(aoExpectedCTD), 0);
+ }
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index ad332e6..c898f14 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -247,7 +247,8 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
DefaultPlan.class, ""));
}
- if (recurringBillingMode == null) {
+ // Pure usage based plans would not have a recurringBillingMode
+ if (!BillingPeriod.NO_BILLING_PERIOD.equals(getRecurringBillingPeriod()) && recurringBillingMode == null) {
errors.add(new ValidationError(String.format("Invalid recurring billingMode for plan '%s'", name), DefaultPlan.class, ""));
}
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..d156a15 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,22 @@ 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);
+ final LocalDate transitionStartDate = internalTenantContext.toLocalDate(billingEvent.getEffectiveDate());
+ if (i == billingEvents.size() - 1) {
+ addTransitionTimesForBillingEvent(transitionStartDate, endDate, billingEvent.getBillCycleDayLocal());
+ } else {
+ final BillingEvent nextBillingEvent = billingEvents.get(i + 1);
+ final LocalDate nextEndDate = internalTenantContext.toLocalDate(nextBillingEvent.getEffectiveDate());
+ addTransitionTimesForBillingEvent(transitionStartDate, nextEndDate, billingEvent.getBillCycleDayLocal());
}
- numberOfPeriod++;
- nextBillCycleDate = bid.getFutureBillingDateFor(numberOfPeriod);
}
+
if (closedInterval &&
transitionTimes.size() > 0 &&
endDate.isAfter(transitionTimes.get(transitionTimes.size() - 1))) {
@@ -155,6 +154,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 +429,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>() {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalUsageInArrear.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalUsageInArrear.java
new file mode 100644
index 0000000..326e3c6
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestContiguousIntervalUsageInArrear.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.usage;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.killbill.billing.catalog.DefaultTier;
+import org.killbill.billing.catalog.DefaultTieredBlock;
+import org.killbill.billing.catalog.DefaultUsage;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.TierBlockPolicy;
+import org.killbill.billing.catalog.api.Usage;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.usage.RawUsage;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestContiguousIntervalUsageInArrear extends TestUsageInArrearBase {
+
+ @Test(groups = "fast")
+ public void testVerifyTransitionTimes() {
+ final DefaultTieredBlock block = createDefaultTieredBlock("unit", 100, 1000, BigDecimal.ONE);
+ final DefaultTier tier = createDefaultTierWithBlocks(block);
+ final DefaultUsage usage = createConsumableInArrearUsage(usageName, BillingPeriod.MONTHLY, TierBlockPolicy.ALL_TIERS, tier);
+
+ final LocalDate targetDate = new LocalDate(2019, 3, 10);
+
+ final BillingEvent billingEvent1 = createMockBillingEvent(1,
+ new LocalDate(2019, 1, 1).toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList());
+ final BillingEvent billingEvent2 = createMockBillingEvent(1,
+ new LocalDate(2019, 1, 31).toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList());
+ final BillingEvent billingEvent3 = createMockBillingEvent(5,
+ new LocalDate(2019, 2, 5).toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList());
+ final BillingEvent billingEvent4 = createMockBillingEvent(10,
+ new LocalDate(2019, 3, 10).toDateTimeAtStartOfDay(DateTimeZone.UTC),
+ BillingPeriod.MONTHLY,
+ Collections.<Usage>emptyList());
+ final ContiguousIntervalConsumableUsageInArrear intervalConsumableInArrear = createContiguousIntervalConsumableInArrear(usage,
+ ImmutableList.<RawUsage>of(),
+ targetDate,
+ false,
+ billingEvent1,
+ billingEvent2,
+ billingEvent3,
+ billingEvent4);
+
+ Assert.assertEquals(intervalConsumableInArrear.getTransitionTimes().size(), 5);
+ Assert.assertEquals(intervalConsumableInArrear.getTransitionTimes().get(0), new LocalDate(2019, 1, 1));
+ Assert.assertEquals(intervalConsumableInArrear.getTransitionTimes().get(1), new LocalDate(2019, 2, 1));
+ Assert.assertEquals(intervalConsumableInArrear.getTransitionTimes().get(2), new LocalDate(2019, 2, 5));
+ Assert.assertEquals(intervalConsumableInArrear.getTransitionTimes().get(3), new LocalDate(2019, 3, 5));
+ Assert.assertEquals(intervalConsumableInArrear.getTransitionTimes().get(4), new LocalDate(2019, 3, 10));
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
index d9ac9f3..4215728 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/usage/TestUsageInArrearBase.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -166,9 +166,13 @@ public abstract class TestUsageInArrearBase extends InvoiceTestSuiteNoDB {
}
protected BillingEvent createMockBillingEvent(final DateTime effectiveDate, final BillingPeriod billingPeriod, final List<Usage> usages) {
+ return createMockBillingEvent(BCD, effectiveDate, billingPeriod, usages);
+ }
+
+ protected BillingEvent createMockBillingEvent(final int bcd, final DateTime effectiveDate, final BillingPeriod billingPeriod, final List<Usage> usages) {
final BillingEvent result = Mockito.mock(BillingEvent.class);
Mockito.when(result.getCurrency()).thenReturn(Currency.BTC);
- Mockito.when(result.getBillCycleDayLocal()).thenReturn(BCD);
+ Mockito.when(result.getBillCycleDayLocal()).thenReturn(bcd);
Mockito.when(result.getEffectiveDate()).thenReturn(effectiveDate);
Mockito.when(result.getBillingPeriod()).thenReturn(billingPeriod);