killbill-memoizeit
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java 64(+22 -42)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestInArrearWithCatalogVersions.java 175(+175 -0)
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java 26(+12 -14)
invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalUsageInArrear.java 14(+12 -2)
junction/pom.xml 3(+1 -2)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java 12(+12 -0)
Details
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
index dfbe4f6..7af38bf 100644
--- a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
+++ b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
@@ -33,11 +33,10 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
public interface BillingEvent extends Comparable<BillingEvent> {
-
/**
* @return the billCycleDay in the account timezone as seen for that subscription at that time
- * <p/>
- * Note: The billCycleDay may come from the Account, or the bundle or the subscription itself
+ * <p/>
+ * Note: The billCycleDay may come from the Account, or the bundle or the subscription itself
*/
int getBillCycleDayLocal();
@@ -97,9 +96,13 @@ public interface BillingEvent extends Comparable<BillingEvent> {
Long getTotalOrdering();
/**
- *
* @return the list of {@code Usage} section
*/
List<Usage> getUsages();
-}
+ /**
+ *
+ * @return the catalog version (effective date) associated with this billing event.
+ */
+ public DateTime getCatalogEffectiveDate();
+}
\ No newline at end of file
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
index bb9e84c..6abcdad 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegration.java
@@ -1094,15 +1094,16 @@ public class TestIntegration extends TestIntegrationBase {
clock.addDays(30);
assertListenerStatus();
invoiceChecker.checkInvoice(account.getId(), 2, callContext,
- new ExpectedInvoiceItemCheck(new LocalDate(2015, 10, 1), new LocalDate(2016, 10, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+ new ExpectedInvoiceItemCheck(new LocalDate(2015, 10, 1), new LocalDate(2016, 10, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")),
+ new ExpectedInvoiceItemCheck(new LocalDate(2015, 9, 1), new LocalDate(2015, 10, 1), InvoiceItemType.USAGE, BigDecimal.ZERO));
// 2015-11-1
- busHandler.pushExpectedEvent(NextEvent.NULL_INVOICE);
+ busHandler.pushExpectedEvent(NextEvent.INVOICE);
clock.addMonths(1);
assertListenerStatus();
// 2015-12-1
- busHandler.pushExpectedEvent(NextEvent.NULL_INVOICE);
+ busHandler.pushExpectedEvent(NextEvent.INVOICE);
clock.addMonths(1);
assertListenerStatus();
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index f10a98c..66eea8c 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -110,12 +110,17 @@ import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineAp
import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.usage.api.SubscriptionUsageRecord;
+import org.killbill.billing.usage.api.UnitUsageRecord;
+import org.killbill.billing.usage.api.UsageApiException;
+import org.killbill.billing.usage.api.UsageRecord;
import org.killbill.billing.usage.api.UsageUserApi;
import org.killbill.billing.util.api.RecordIdApi;
import org.killbill.billing.util.api.TagApiException;
import org.killbill.billing.util.api.TagDefinitionApiException;
import org.killbill.billing.util.api.TagUserApi;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.config.definition.InvoiceConfig;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.billing.util.dao.NonEntityDao;
@@ -125,6 +130,8 @@ import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBus.EventBusException;
import org.skife.config.ConfigurationObjectFactory;
import org.skife.config.TimeSpan;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
@@ -913,6 +920,28 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
return result;
}
+ protected void recordUsageData(final UUID subscriptionId, final String unitType, final LocalDate startDate, final Long amount, final CallContext context) throws UsageApiException {
+ final List<UsageRecord> usageRecords = new ArrayList<UsageRecord>();
+ usageRecords.add(new UsageRecord(startDate, amount));
+ final List<UnitUsageRecord> unitUsageRecords = new ArrayList<UnitUsageRecord>();
+ unitUsageRecords.add(new UnitUsageRecord(unitType, usageRecords));
+ final SubscriptionUsageRecord record = new SubscriptionUsageRecord(subscriptionId, UUID.randomUUID().toString(), unitUsageRecords);
+ usageUserApi.recordRolledUpUsage(record, context);
+ }
+
+
+ protected void removeUsageData(final UUID subscriptionId, final String unitType, final LocalDate recordedDate) {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ handle.execute("delete from rolled_up_usage where subscription_id = ? and unit_type = ? and record_date = ?",
+ subscriptionId, unitType, recordedDate);
+ return null;
+ }
+ });
+ }
+
+
protected static class TestDryRunArguments implements DryRunArguments {
private final DryRunType dryRunType;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
index b4a65ac..551822f 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
@@ -18,13 +18,8 @@
package org.killbill.billing.beatrix.integration.usage;
import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
@@ -33,21 +28,11 @@ import org.killbill.billing.beatrix.integration.TestIntegrationBase;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.InvoiceItemType;
-import org.killbill.billing.mock.MockAccountBuilder;
import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.usage.api.SubscriptionUsageRecord;
-import org.killbill.billing.usage.api.UnitUsageRecord;
-import org.killbill.billing.usage.api.UsageApiException;
-import org.killbill.billing.usage.api.UsageRecord;
-import org.killbill.billing.util.callcontext.CallContext;
-import org.skife.jdbi.v2.Handle;
-import org.skife.jdbi.v2.tweak.HandleCallback;
import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -77,8 +62,8 @@ public class TestConsumableInArrear extends TestIntegrationBase {
//
final DefaultEntitlement aoSubscription = addAOEntitlementAndCheckForCompletion(bpSubscription.getBundleId(), "Bullets", ProductCategory.ADD_ON, BillingPeriod.NO_BILLING_PERIOD, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 1), 99L, callContext);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 1), 99L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addDays(30);
@@ -96,8 +81,8 @@ public class TestConsumableInArrear extends TestIntegrationBase {
invoiceChecker.checkInvoice(account.getId(), 3, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.USAGE, BigDecimal.ZERO));
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 1), 50L, callContext);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 16), 300L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 1), 50L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 6, 16), 300L, callContext);
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addMonths(1);
@@ -107,14 +92,18 @@ public class TestConsumableInArrear extends TestIntegrationBase {
new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.USAGE, new BigDecimal("11.80")));
// Should be ignored because this is outside of optimization range (org.killbill.invoice.readMaxRawUsagePreviousPeriod = 2) => we will only look for items > 2012-7-1 - 2 months = 2012-5-1
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 30), 100L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 30), 100L, callContext);
// Should be invoiced from past period
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 1), 199L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 1), 199L, callContext);
// New usage for this past period
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 7, 1), 50L, callContext);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 7, 16), 300L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 7, 1), 50L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 7, 16), 300L, callContext);
+
+ // Remove old data, should be ignored by the system because readMaxRawUsagePreviousPeriod = 2, so:
+ // * Last endDate invoiced is 2012-7-1 => Anything 2 period prior that will be ignored => Anything prior 2012-5-1 should be ignored
+ removeUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15));
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addMonths(1);
@@ -130,7 +119,7 @@ public class TestConsumableInArrear extends TestIntegrationBase {
int currentInvoice = 6;
for (int i = 0; i < 8; i++) {
- setUsage(aoSubscription.getId(), "bullets", startDate.plusDays(15), 350L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", startDate.plusDays(15), 350L, callContext);
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addMonths(1);
@@ -168,8 +157,8 @@ public class TestConsumableInArrear extends TestIntegrationBase {
//
final DefaultEntitlement aoSubscription = addAOEntitlementAndCheckForCompletion(bpSubscription.getBundleId(), "Bullets", ProductCategory.ADD_ON, BillingPeriod.NO_BILLING_PERIOD, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 1), 99L, callContext);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 1), 99L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 4, 15), 100L, callContext);
busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addDays(30);
@@ -178,11 +167,11 @@ public class TestConsumableInArrear extends TestIntegrationBase {
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")));
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 3), 99L, callContext);
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 5), 100L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 3), 99L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 5), 100L, callContext);
// This one should be ignored
- setUsage(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 29), 100L, callContext);
+ recordUsageData(aoSubscription.getId(), "bullets", new LocalDate(2012, 5, 29), 100L, callContext);
clock.addDays(27);
busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
@@ -215,8 +204,8 @@ public class TestConsumableInArrear extends TestIntegrationBase {
Assert.assertNull(bpSubscription.getSubscriptionBase().getChargedThroughDate());
// Record usage for first month
- setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 4, 5), 85L, callContext);
- setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 4, 15), 150L, callContext);
+ recordUsageData(bpSubscription.getId(), "stones", new LocalDate(2012, 4, 5), 85L, callContext);
+ recordUsageData(bpSubscription.getId(), "stones", new LocalDate(2012, 4, 15), 150L, callContext);
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addMonths(1);
@@ -242,8 +231,8 @@ public class TestConsumableInArrear extends TestIntegrationBase {
Assert.assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(secondExpectedCTD), 0);
// Record usage for third month (verify invoicing resumes)
- setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 6, 5), 25L, callContext);
- setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 6, 15), 50L, callContext);
+ recordUsageData(bpSubscription.getId(), "stones", new LocalDate(2012, 6, 5), 25L, callContext);
+ recordUsageData(bpSubscription.getId(), "stones", new LocalDate(2012, 6, 15), 50L, callContext);
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
clock.addMonths(1);
@@ -255,13 +244,4 @@ public class TestConsumableInArrear extends TestIntegrationBase {
final DateTime thirdExpectedCTD = account.getReferenceTime().withMonthOfYear(7).withDayOfMonth(1);
Assert.assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(thirdExpectedCTD), 0);
}
-
- private void setUsage(final UUID subscriptionId, final String unitType, final LocalDate startDate, final Long amount, final CallContext context) throws UsageApiException {
- final List<UsageRecord> usageRecords = new ArrayList<UsageRecord>();
- usageRecords.add(new UsageRecord(startDate, amount));
- final List<UnitUsageRecord> unitUsageRecords = new ArrayList<UnitUsageRecord>();
- unitUsageRecords.add(new UnitUsageRecord(unitType, usageRecords));
- final SubscriptionUsageRecord record = new SubscriptionUsageRecord(subscriptionId, UUID.randomUUID().toString(), unitUsageRecords);
- usageUserApi.recordRolledUpUsage(record, context);
- }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestInArrearWithCatalogVersions.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestInArrearWithCatalogVersions.java
new file mode 100644
index 0000000..f1693b2
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestInArrearWithCatalogVersions.java
@@ -0,0 +1,175 @@
+/*
+ * 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.beatrix.integration.usage;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+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.integration.TestIntegrationBase;
+import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.entitlement.api.DefaultEntitlementSpecifier;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class TestInArrearWithCatalogVersions extends TestIntegrationBase {
+
+ @Override
+ protected KillbillConfigSource getConfigSource() {
+ return super.getConfigSource(null, ImmutableMap.of("org.killbill.catalog.uri", "catalogs/testInArrearWithCatalogVersions",
+ "org.killbill.invoice.readMaxRawUsagePreviousPeriod", "0"));
+ }
+
+ @Test(groups = "slow")
+ public void testWithNoUsageInPeriodAndOldUsage() throws Exception {
+ // 30 days month
+ clock.setDay(new LocalDate(2016, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("electricity-monthly");
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+ final UUID entitlementId = entitlementApi.createBaseEntitlement(account.getId(), new DefaultEntitlementSpecifier(spec), null, null, null, false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 4, 1), 143L, callContext);
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 4, 18), 57L, callContext);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 5, 1), InvoiceItemType.USAGE, new BigDecimal("300.00")));
+
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 5, 2), 100L, callContext); // -> Uses v1 : $150
+
+ // Catalog change with new price on 2016-05-08
+ // Schedule CHANGE_PLAN on 2016-05-09
+ final Entitlement bp = entitlementApi.getEntitlementForId(entitlementId, callContext);
+
+ bp.changePlanWithDate(new DefaultEntitlementSpecifier(spec), new LocalDate("2016-05-09"), null, callContext);
+ assertListenerStatus();
+
+ busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(8);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 5, 9), InvoiceItemType.USAGE, new BigDecimal("150.00")));
+
+
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 5, 10), 100L, callContext); // -> Uses v2 : $250
+
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(23);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 9), new LocalDate(2016, 6, 1), InvoiceItemType.USAGE, new BigDecimal("250.00")));
+
+ }
+
+
+ // We are not using catalog versions in this test but testing the overridden value of 'readMaxRawUsagePreviousPeriod = 0'
+ @Test(groups = "slow")
+ public void testWithRemovedData() throws Exception {
+ // 30 days month
+ clock.setDay(new LocalDate(2016, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("electricity-monthly");
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+ final UUID entitlementId = entitlementApi.createBaseEntitlement(account.getId(), new DefaultEntitlementSpecifier(spec), null, null, null, false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 4, 5), 1L, callContext);
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 4, 5), 99L, callContext);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 4, 1), new LocalDate(2016, 5, 1), InvoiceItemType.USAGE, new BigDecimal("150.00")));
+
+
+
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 5, 5), 100L, callContext);
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 5, 8), 900L, callContext);
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 5, 9), 200L, callContext); // Move to tier 2.
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 5, 1), new LocalDate(2016, 6, 1), InvoiceItemType.USAGE, new BigDecimal("1900.00")));
+
+
+
+ // Remove Usage data from period 2016-5-1 -> 2016-6-1 and verify there is no issue (readMaxRawUsagePreviousPeriod = 0 => We ignore any past invoiced period)
+ // Full deletion on the second tier
+ removeUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 5, 9));
+
+ //
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 6, 5), 100L, callContext);
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 6, 8), 900L, callContext);
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 6, 12), 50L, callContext); // Move to tier 2.
+ recordUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 6, 13), 50L, callContext);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ invoiceChecker.checkInvoice(account.getId(), 3, callContext, ImmutableList.<ExpectedInvoiceItemCheck>of(
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 1), new LocalDate(2016, 7, 1), InvoiceItemType.USAGE, new BigDecimal("1700.00"))));
+
+
+ // Remove Usage data from period 2016-6-1 -> 2016-7-1 and verify there is no issue (readMaxRawUsagePreviousPeriod = 0 => We ignore any past invoiced period)
+ // Partial deletion on the second tier
+ removeUsageData(entitlementId, "kilowatt-hour", new LocalDate(2016, 6, 13));
+
+ // No usage this MONTH
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // Check invoicing occurred and - i.e system did not detect deletion of passed invoiced data.
+ invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+ new ExpectedInvoiceItemCheck(new LocalDate(2016, 7, 1), new LocalDate(2016, 8, 1), InvoiceItemType.USAGE, BigDecimal.ZERO));
+ }
+
+
+}
\ No newline at end of file
diff --git a/beatrix/src/test/resources/catalogs/testInArrearWithCatalogVersions/Utility-v1.xml b/beatrix/src/test/resources/catalogs/testInArrearWithCatalogVersions/Utility-v1.xml
new file mode 100644
index 0000000..eef11de
--- /dev/null
+++ b/beatrix/src/test/resources/catalogs/testInArrearWithCatalogVersions/Utility-v1.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ Copyright 2014 The Billing Project, Inc.
+ ~
+ ~ Ning 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.
+ -->
+
+<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="CatalogSchema.xsd ">
+
+ <effectiveDate>2016-02-08T00:00:00+00:00</effectiveDate>
+ <catalogName>TestInArrearWithCatalogVersions</catalogName>
+
+ <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
+
+ <currencies>
+ <currency>USD</currency>
+ </currencies>
+
+ <units>
+ <unit name="kilowatt-hour"/>
+ </units>
+
+ <products>
+ <product name="Electricity">
+ <category>BASE</category>
+ </product>
+ </products>
+
+ <rules>
+ <changePolicy>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ </changePolicy>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
+ </rules>
+
+ <plans>
+
+ <plan name="electricity-monthly">
+ <product>Electricity</product>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <usages>
+ <usage name="electricity-monthly-usage" billingMode="IN_ARREAR" usageType="CONSUMABLE" tierBlockPolicy="ALL_TIERS">
+ <billingPeriod>MONTHLY</billingPeriod>
+ <tiers>
+ <tier>
+ <blocks>
+ <tieredBlock>
+ <unit>kilowatt-hour</unit>
+ <size>1</size>
+ <prices>
+ <price>
+ <currency>USD</currency>
+ <value>1.50</value>
+ </price>
+ </prices>
+ <max>1000</max>
+ </tieredBlock>
+ </blocks>
+ </tier>
+ <tier>
+ <blocks>
+ <tieredBlock>
+ <unit>kilowatt-hour</unit>
+ <size>1</size>
+ <prices>
+ <price>
+ <currency>USD</currency>
+ <value>2.00</value>
+ </price>
+ </prices>
+ <max>-1</max>
+ </tieredBlock>
+ </blocks>
+ </tier>
+ </tiers>
+ </usage>
+ </usages>
+ </finalPhase>
+ </plan>
+
+ </plans>
+ <priceLists>
+ <defaultPriceList name="DEFAULT">
+ <plans>
+ <plan>electricity-monthly</plan>
+ </plans>
+ </defaultPriceList>
+ </priceLists>
+</catalog>
diff --git a/beatrix/src/test/resources/catalogs/testInArrearWithCatalogVersions/Utility-v2.xml b/beatrix/src/test/resources/catalogs/testInArrearWithCatalogVersions/Utility-v2.xml
new file mode 100644
index 0000000..5700fbb
--- /dev/null
+++ b/beatrix/src/test/resources/catalogs/testInArrearWithCatalogVersions/Utility-v2.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ 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.
+ -->
+
+<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="CatalogSchema.xsd ">
+
+ <effectiveDate>2016-05-08T00:00:00+00:00</effectiveDate>
+ <catalogName>TestInArrearWithCatalogVersions</catalogName>
+
+ <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
+
+ <currencies>
+ <currency>USD</currency>
+ </currencies>
+
+ <units>
+ <unit name="kilowatt-hour"/>
+ </units>
+
+ <products>
+ <product name="Electricity">
+ <category>BASE</category>
+ </product>
+ </products>
+
+ <rules>
+ <changePolicy>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ </changePolicy>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
+ </rules>
+
+ <plans>
+
+ <!-- Price increase from v1 version -->
+ <plan name="electricity-monthly">
+ <effectiveDateForExistingSubscriptions>2016-05-08T00:00:00+00:00</effectiveDateForExistingSubscriptions>
+ <product>Electricity</product>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <usages>
+ <usage name="electricity-monthly-usage" billingMode="IN_ARREAR" usageType="CONSUMABLE" tierBlockPolicy="ALL_TIERS">
+ <billingPeriod>MONTHLY</billingPeriod>
+ <tiers>
+ <tier>
+ <blocks>
+ <tieredBlock>
+ <unit>kilowatt-hour</unit>
+ <size>1</size>
+ <prices>
+ <price>
+ <currency>USD</currency>
+ <value>2.50</value>
+ </price>
+ </prices>
+ <max>1000</max>
+ </tieredBlock>
+ </blocks>
+ </tier>
+ <tier>
+ <blocks>
+ <tieredBlock>
+ <unit>kilowatt-hour</unit>
+ <size>1</size>
+ <prices>
+ <price>
+ <currency>USD</currency>
+ <value>3.00</value>
+ </price>
+ </prices>
+ <max>-1</max>
+ </tieredBlock>
+ </blocks>
+ </tier>
+ </tiers>
+ </usage>
+ </usages>
+ </finalPhase>
+ </plan>
+
+ </plans>
+ <priceLists>
+ <defaultPriceList name="DEFAULT">
+ <plans>
+ <plan>electricity-monthly</plan>
+ </plans>
+ </defaultPriceList>
+ </priceLists>
+</catalog>
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 a61fa3c..f47f3a0 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -97,6 +97,8 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
private String priceListName;
+ private DateTime catalogEffectiveDate;
+
// For deserialization
public DefaultPlan() {
initialPhases = new DefaultPlanPhase[0];
@@ -219,6 +221,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
}
this.priceListName = this.priceListName != null ? this.priceListName : findPriceListForPlan(catalog);
+ this.catalogEffectiveDate = new DateTime(catalog.getEffectiveDate());
}
@Override
@@ -402,4 +405,8 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
public void writeExternal(final ObjectOutput oo) throws IOException {
MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
}
+
+ public DateTime getCatalogEffectiveDate() {
+ return catalogEffectiveDate;
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java
index 747a501..22e812c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/ContiguousIntervalConsumableUsageInArrear.java
@@ -208,21 +208,19 @@ public class ContiguousIntervalConsumableUsageInArrear extends ContiguousInterva
remainingUnits = 0;
}
// We generate an entry if we consumed anything on this tier or if this is the first tier to also support $0 Usage item
- if (tierNum == 1 || nbUsedTierBlocks > 0) {
- if (hasPreviousUsage) {
- final Integer previousUsageQuantity = tierNum <= lastPreviousUsageTier ? previousUsage.get(tierNum - 1).getQuantity() : 0;
- if (tierNum < lastPreviousUsageTier) {
- Preconditions.checkState(nbUsedTierBlocks == previousUsageQuantity, "Expected usage for tier='%d', unit='%s' to be full, instead found units='[%d/%d]'",
- tierNum, tieredBlock.getUnit().getName(), nbUsedTierBlocks, previousUsageQuantity);
- } else {
- Preconditions.checkState(nbUsedTierBlocks - previousUsageQuantity >= 0, "Expected usage for tier='%d', unit='%s' to contain at least as mush as current usage, instead found units='[%d/%d]",
- tierNum, tieredBlock.getUnit().getName(), nbUsedTierBlocks, previousUsageQuantity);
- }
- nbUsedTierBlocks = nbUsedTierBlocks - previousUsageQuantity;
- }
- if (tierNum == 1 || nbUsedTierBlocks > 0) {
- toBeBilledDetails.add(new UsageConsumableInArrearTierUnitAggregate(tierNum, tieredBlock.getUnit().getName(), tieredBlock.getPrice().getPrice(getCurrency()), blockTierSize, nbUsedTierBlocks));
+ if (hasPreviousUsage) {
+ final Integer previousUsageQuantity = tierNum <= lastPreviousUsageTier ? previousUsage.get(tierNum - 1).getQuantity() : 0;
+ if (tierNum < lastPreviousUsageTier) {
+ Preconditions.checkState(nbUsedTierBlocks == previousUsageQuantity, String.format("Expected usage for tier='%d', unit='%s' to be full, instead found units='[%d/%d]'",
+ tierNum, tieredBlock.getUnit().getName(), nbUsedTierBlocks, previousUsageQuantity));
+ } else {
+ Preconditions.checkState(nbUsedTierBlocks - previousUsageQuantity >= 0, String.format("Expected usage for tier='%d', unit='%s' to contain at least as mush as current usage, instead found units='[%d/%d]",
+ tierNum, tieredBlock.getUnit().getName(), nbUsedTierBlocks, previousUsageQuantity));
}
+ nbUsedTierBlocks = nbUsedTierBlocks - previousUsageQuantity;
+ }
+ if (tierNum == 1 || nbUsedTierBlocks > 0) {
+ toBeBilledDetails.add(new UsageConsumableInArrearTierUnitAggregate(tierNum, tieredBlock.getUnit().getName(), tieredBlock.getPrice().getPrice(getCurrency()), blockTierSize, nbUsedTierBlocks));
}
}
return toBeBilledDetails;
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 f83f6da..bcc13cb 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
@@ -234,12 +234,22 @@ public abstract class ContiguousIntervalUsageInArrear {
@VisibleForTesting
List<RolledUpUsage> getRolledUpUsage() {
+
+ final List<RolledUpUsage> result = new ArrayList<RolledUpUsage>();
+
final Iterator<RawUsage> rawUsageIterator = rawSubscriptionUsage.iterator();
if (!rawUsageIterator.hasNext()) {
- return ImmutableList.of();
+ final LocalDate startDate = transitionTimes.get(transitionTimes.size() - 2);
+ final LocalDate endDate = transitionTimes.get(transitionTimes.size() - 1);
+ for (String unitType : unitTypes) {
+ final List<RolledUpUnit> emptyRolledUptUnits = new ArrayList<RolledUpUnit>();
+ emptyRolledUptUnits.add(new DefaultRolledUpUnit(unitType, 0L));
+ final DefaultRolledUpUsage defaultForUnit = new DefaultRolledUpUsage(getSubscriptionId(), startDate, endDate, emptyRolledUptUnits);
+ result.add(defaultForUnit);
+ }
+ return result;
}
- final List<RolledUpUsage> result = new ArrayList<RolledUpUsage>();
//
// Skip all items before our first transition date
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java b/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java
index b0f27f8..1e513ee 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/usage/RawUsageOptimizer.java
@@ -67,7 +67,7 @@ public class RawUsageOptimizer {
}
public RawUsageOptimizerResult getInArrearUsage(final LocalDate firstEventStartDate, final LocalDate targetDate, final Iterable<InvoiceItem> existingUsageItems, final Map<String, Usage> knownUsage, final InternalCallContext internalCallContext) {
- final LocalDate targetStartDate = config.getMaxRawUsagePreviousPeriod(internalCallContext) > 0 ? getOptimizedRawUsageStartDate(firstEventStartDate, targetDate, existingUsageItems, knownUsage, internalCallContext) : firstEventStartDate;
+ final LocalDate targetStartDate = config.getMaxRawUsagePreviousPeriod(internalCallContext) >= 0 ? getOptimizedRawUsageStartDate(firstEventStartDate, targetDate, existingUsageItems, knownUsage, internalCallContext) : firstEventStartDate;
log.debug("ConsumableInArrear accountRecordId='{}', rawUsageStartDate='{}', firstEventStartDate='{}'",
internalCallContext.getAccountRecordId(), targetStartDate, firstEventStartDate);
final List<RawUsage> rawUsageData = usageApi.getRawUsageForAccount(targetStartDate, targetDate, internalCallContext);
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 93bbd42..bb3d830 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
@@ -27,6 +27,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingMode;
@@ -132,52 +133,56 @@ public class SubscriptionUsageInArrear {
List<ContiguousIntervalUsageInArrear> computeInArrearUsageInterval() {
final List<ContiguousIntervalUsageInArrear> usageIntervals = Lists.newLinkedList();
- final Map<String, ContiguousIntervalUsageInArrear> inFlightInArrearUsageIntervals = new HashMap<String, ContiguousIntervalUsageInArrear>();
+ final Map<UsageKey, ContiguousIntervalUsageInArrear> inFlightInArrearUsageIntervals = new HashMap<UsageKey, ContiguousIntervalUsageInArrear>();
- final Set<String> allSeenUsage = new HashSet<String>();
+ 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, String>() {
+ allSeenUsage.addAll(Collections2.transform(usages, new Function<Usage, UsageKey>() {
@Override
- public String apply(final Usage input) {
- return input.getName();
+ public UsageKey apply(final Usage input) {
+ return new UsageKey(input.getName(), event.getCatalogEffectiveDate());
}
}));
// All inflight usage interval are candidates to be closed unless we see that current billing event referencing the same usage section.
- final Set<String> toBeClosed = new HashSet<String>(allSeenUsage);
+ final Set<UsageKey> toBeClosed = new HashSet<UsageKey>(allSeenUsage);
for (final Usage usage : usages) {
+ final UsageKey usageKey = new UsageKey(usage.getName(), event.getCatalogEffectiveDate());
+
// Add inflight usage interval if non existent
- ContiguousIntervalUsageInArrear existingInterval = inFlightInArrearUsageIntervals.get(usage.getName());
+ ContiguousIntervalUsageInArrear existingInterval = inFlightInArrearUsageIntervals.get(usageKey);
if (existingInterval == null) {
existingInterval = usage.getUsageType() == UsageType.CAPACITY ?
new ContiguousIntervalCapacityUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext) :
new ContiguousIntervalConsumableUsageInArrear(usage, accountId, invoiceId, rawSubscriptionUsage, targetDate, rawUsageStartDate, usageDetailMode, internalTenantContext);
- inFlightInArrearUsageIntervals.put(usage.getName(), existingInterval);
+ inFlightInArrearUsageIntervals.put(usageKey, existingInterval);
}
// Add billing event for that usage interval
existingInterval.addBillingEvent(event);
// Remove usage interval for toBeClosed set
- toBeClosed.remove(usage.getName());
+ toBeClosed.remove(usageKey);
}
// Build the usage interval that are no longer referenced
- for (final String usageName : toBeClosed) {
- final ContiguousIntervalUsageInArrear interval = inFlightInArrearUsageIntervals.remove(usageName);
+ for (final UsageKey usageKey : toBeClosed) {
+ final ContiguousIntervalUsageInArrear interval = inFlightInArrearUsageIntervals.remove(usageKey);
if (interval != null) {
interval.addBillingEvent(event);
usageIntervals.add(interval.build(true));
}
}
}
- for (final String usageName : inFlightInArrearUsageIntervals.keySet()) {
- usageIntervals.add(inFlightInArrearUsageIntervals.get(usageName).build(false));
+ for (final UsageKey usageKey : inFlightInArrearUsageIntervals.keySet()) {
+ usageIntervals.add(inFlightInArrearUsageIntervals.get(usageKey).build(false));
}
inFlightInArrearUsageIntervals.clear();
return usageIntervals;
@@ -233,4 +238,40 @@ public class SubscriptionUsageInArrear {
return perUsageNotificationDates != null ? perUsageNotificationDates : ImmutableMap.<String, LocalDate>of();
}
}
+
+ private static class UsageKey {
+
+ private final String usageName;
+ private final DateTime catalogVersion;
+
+ public UsageKey(final String usageName, final DateTime catalogVersion) {
+ this.usageName = usageName;
+ this.catalogVersion = catalogVersion;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final UsageKey usageKey = (UsageKey) o;
+
+ if (usageName != null ? !usageName.equals(usageKey.usageName) : usageKey.usageName != null) {
+ return false;
+ }
+ return catalogVersion != null ? catalogVersion.compareTo(usageKey.catalogVersion) == 0 : usageKey.catalogVersion == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = usageName != null ? usageName.hashCode() : 0;
+ result = 31 * result + (catalogVersion != null ? catalogVersion.hashCode() : 0);
+ return result;
+ }
+ }
+
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index 73ae1fd..c19fdce 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -425,6 +425,11 @@ public class TestInvoiceHelper {
}
@Override
+ public DateTime getCatalogEffectiveDate() {
+ return null;
+ }
+
+ @Override
public int compareTo(final BillingEvent e1) {
if (!getSubscription().getId().equals(e1.getSubscription().getId())) { // First order by subscription
return getSubscription().getId().compareTo(e1.getSubscription().getId());
junction/pom.xml 3(+1 -2)
diff --git a/junction/pom.xml b/junction/pom.xml
index 4cef49f..bfc8c82 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -91,12 +91,11 @@
<dependency>
<groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
+ <type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
index c08a7be..f7285c5 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
@@ -24,6 +24,7 @@ import java.util.List;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import org.killbill.billing.catalog.DefaultPlan;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -59,6 +60,8 @@ public class DefaultBillingEvent implements BillingEvent {
private final boolean isDisableEvent;
private final PlanPhase nextPlanPhase;
+ private final DateTime catalogEffectiveDate;
+
public DefaultBillingEvent(final SubscriptionInternalEvent transition,
final SubscriptionBase subscription,
final int billCycleDayLocal,
@@ -88,6 +91,8 @@ public class DefaultBillingEvent implements BillingEvent {
final PlanPhase prevPlanPhase = (prevPhaseName != null && prevPlan != null) ? prevPlan.findPhase(prevPhaseName) : null;
this.billingPeriod = getRecurringBillingPeriod(prevPlanPhase);
}
+ // TODO It would be nice to have an Plan api to get that date; actually the following would be nice
+ this.catalogEffectiveDate = ((DefaultPlan) plan).getCatalogEffectiveDate();
this.billCycleDayLocal = billCycleDayLocal;
this.catalog = catalog;
@@ -124,6 +129,8 @@ public class DefaultBillingEvent implements BillingEvent {
this.usages = initializeUsage(isActive);
this.isDisableEvent = isDisableEvent;
this.nextPlanPhase = isDisableEvent ? null : planPhase;
+ this.catalogEffectiveDate = plan != null ? ((DefaultPlan) plan).getCatalogEffectiveDate() : null;
+
}
@Override
@@ -348,4 +355,9 @@ public class DefaultBillingEvent implements BillingEvent {
}
return result;
}
+
+ @Override
+ public DateTime getCatalogEffectiveDate() {
+ return catalogEffectiveDate;
+ }
}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/DefaultHealthcheckPluginRegistry.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/DefaultHealthcheckPluginRegistry.java
new file mode 100644
index 0000000..a56baf9
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/DefaultHealthcheckPluginRegistry.java
@@ -0,0 +1,65 @@
+/*
+ * 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.server.modules;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.killbill.billing.osgi.api.Healthcheck;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultHealthcheckPluginRegistry implements OSGIServiceRegistration<Healthcheck> {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultHealthcheckPluginRegistry.class);
+
+ private final Map<String, Healthcheck> pluginsByName = new ConcurrentHashMap<String, Healthcheck>();
+
+ @Override
+ public void registerService(final OSGIServiceDescriptor desc, final Healthcheck healthcheck) {
+ log.info("Registering service='{}'", desc.getRegistrationName());
+ pluginsByName.put(desc.getRegistrationName(), healthcheck);
+ }
+
+ @Override
+ public void unregisterService(final String serviceName) {
+ log.info("Unregistering service='{}'", serviceName);
+ pluginsByName.remove(serviceName);
+ }
+
+ @Override
+ public Healthcheck getServiceForName(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null healthcheck name");
+ }
+ return pluginsByName.get(name);
+ }
+
+ @Override
+ public Set<String> getAllServices() {
+ return pluginsByName.keySet();
+ }
+
+ @Override
+ public Class<Healthcheck> getServiceType() {
+ return Healthcheck.class;
+ }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
index 5d43a8f..f43a948 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
@@ -54,6 +54,8 @@ import org.killbill.billing.jaxrs.resources.TransactionResource;
import org.killbill.billing.jaxrs.resources.UsageResource;
import org.killbill.billing.jaxrs.util.KillbillEventHandler;
import org.killbill.billing.junction.glue.DefaultJunctionModule;
+import org.killbill.billing.osgi.api.Healthcheck;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.overdue.glue.DefaultOverdueModule;
import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.platform.api.KillbillConfigSource;
@@ -94,6 +96,7 @@ import org.skife.jdbi.v2.ResultSetMapperFactory;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import ch.qos.logback.classic.helpers.MDCInsertingServletFilter;
+import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
@@ -114,6 +117,8 @@ public class KillbillServerModule extends KillbillPlatformModule {
configureResources();
configureFilters();
configurePushNotification();
+
+ bind(new TypeLiteral<OSGIServiceRegistration<Healthcheck>>() {}).to(DefaultHealthcheckPluginRegistry.class).asEagerSingleton();
}
@Override