killbill-aplcache
Changes
invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java 43(+40 -3)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 407(+310 -97)
Details
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
index acb4dd0..d519cc0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
@@ -53,6 +53,20 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
}
@Override
+ public boolean isSanitySafetyBoundEnabled() {
+ return staticConfig.isSanitySafetyBoundEnabled();
+ }
+
+ @Override
+ public boolean isSanitySafetyBoundEnabled(final InternalTenantContext tenantContext) {
+ final String result = getStringTenantConfig("isSanitySafetyBoundEnabled", tenantContext);
+ if (result != null) {
+ return Boolean.parseBoolean(result);
+ }
+ return isSanitySafetyBoundEnabled();
+ }
+
+ @Override
public int getMaxDailyNumberOfItemsSafetyBound() {
return staticConfig.getMaxDailyNumberOfItemsSafetyBound();
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
index 6dc31d6..eaa69f8 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -20,6 +20,7 @@ package org.killbill.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -40,6 +41,7 @@ import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvalidDateSequenceException;
@@ -59,6 +61,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
+import com.google.common.collect.Range;
import com.google.inject.Inject;
import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
@@ -107,7 +110,7 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
accountItemTree.mergeWithProposedItems(proposedItems);
final List<InvoiceItem> resultingItems = accountItemTree.getResultingItemList();
- safetyBound(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
return resultingItems;
}
@@ -403,8 +406,42 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
}
}
- // Trigger an exception if we create too many subscriptions for a subscription on a given day
- private void safetyBound(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+ @VisibleForTesting
+ void safetyBounds(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+ // Trigger an exception if we detect the creation of similar items for a given subscription
+ // See https://github.com/killbill/killbill/issues/664
+ if (config.isSanitySafetyBoundEnabled(internalCallContext)) {
+ final Map<UUID, Multimap<LocalDate, InvoiceItem>> fixedItemsPerDateAndSubscription = new HashMap<UUID, Multimap<LocalDate, InvoiceItem>>();
+ final Map<UUID, Multimap<Range<LocalDate>, InvoiceItem>> recurringItemsPerServicePeriodAndSubscription = new HashMap<UUID, Multimap<Range<LocalDate>, InvoiceItem>>();
+ for (final InvoiceItem resultingItem : resultingItems) {
+ if (resultingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
+ if (fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()) == null) {
+ fixedItemsPerDateAndSubscription.put(resultingItem.getSubscriptionId(), LinkedListMultimap.<LocalDate, InvoiceItem>create());
+ }
+ fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()).put(resultingItem.getStartDate(), resultingItem);
+
+ final Collection<InvoiceItem> resultingInvoiceItems = fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()).get(resultingItem.getStartDate());
+ if (resultingInvoiceItems.size() > 1) {
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED Multiple FIXED items for subscriptionId='%s', startDate='%s', resultingItems=%s",
+ resultingItem.getSubscriptionId(), resultingItem.getStartDate(), resultingInvoiceItems));
+ }
+ } else if (resultingItem.getInvoiceItemType() == InvoiceItemType.RECURRING) {
+ if (recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()) == null) {
+ recurringItemsPerServicePeriodAndSubscription.put(resultingItem.getSubscriptionId(), LinkedListMultimap.<Range<LocalDate>, InvoiceItem>create());
+ }
+ final Range<LocalDate> interval = Range.<LocalDate>closedOpen(resultingItem.getStartDate(), resultingItem.getEndDate());
+ recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()).put(interval, resultingItem);
+
+ final Collection<InvoiceItem> resultingInvoiceItems = recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()).get(interval);
+ if (resultingInvoiceItems.size() > 1) {
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED Multiple RECURRING items for subscriptionId='%s', startDate='%s', endDate='%s', resultingItems=%s",
+ resultingItem.getSubscriptionId(), resultingItem.getStartDate(), resultingItem.getEndDate(), resultingInvoiceItems));
+ }
+ }
+ }
+ }
+
+ // Trigger an exception if we create too many invoice items for a subscription on a given day
if (config.getMaxDailyNumberOfItemsSafetyBound(internalCallContext) == -1) {
// Safety bound disabled
return;
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index e104024..e6eb41f 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -123,6 +123,16 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
}
@Override
+ public boolean isSanitySafetyBoundEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isSanitySafetyBoundEnabled(final InternalTenantContext tenantContext) {
+ return true;
+ }
+
+ @Override
public int getMaxDailyNumberOfItemsSafetyBound() {
return 10;
}
@@ -252,18 +262,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 10, 31));
}
- private SubscriptionBase createSubscription() {
- return createSubscription(UUID.randomUUID(), UUID.randomUUID());
- }
-
- private SubscriptionBase createSubscription(final UUID subscriptionId, final UUID bundleId) {
- final SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
- Mockito.when(sub.getId()).thenReturn(subscriptionId);
- Mockito.when(sub.getBundleId()).thenReturn(bundleId);
-
- return sub;
- }
-
@Test(groups = "fast")
public void testSimpleWithTimeZone() throws InvoiceApiException, CatalogApiException {
final SubscriptionBase sub = createSubscription();
@@ -842,65 +840,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
}
- private MockPlanPhase createMockThirtyDaysPlanPhase(@Nullable final BigDecimal recurringRate) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.THIRTY_DAYS);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase() {
- return new MockPlanPhase(null, null, BillingPeriod.MONTHLY);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.MONTHLY);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.MONTHLY, phaseType);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
- @Nullable final BigDecimal fixedCost,
- final PhaseType phaseType) {
- final MockInternationalPrice recurringPrice = (recurringRate == null) ? null : new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD));
- final MockInternationalPrice fixedPrice = (fixedCost == null) ? null : new MockInternationalPrice(new DefaultPrice(fixedCost, Currency.USD));
-
- return new MockPlanPhase(recurringPrice, fixedPrice, BillingPeriod.MONTHLY, phaseType);
- }
-
- private MockPlanPhase createMockAnnualPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.ANNUAL, phaseType);
- }
-
- private BillingEvent createBillingEvent(final UUID subscriptionId, final UUID bundleId, final LocalDate startDate,
- final Plan plan, final PlanPhase planPhase, final int billCycleDayLocal) throws CatalogApiException {
- final SubscriptionBase sub = createSubscription(subscriptionId, bundleId);
- final Currency currency = Currency.USD;
-
- return invoiceUtil.createMockBillingEvent(null, sub, startDate.toDateTimeAtStartOfDay(), plan, planPhase,
- planPhase.getFixed().getPrice() == null ? null : planPhase.getFixed().getPrice().getPrice(currency),
- planPhase.getRecurring().getRecurringPrice() == null ? null : planPhase.getRecurring().getRecurringPrice().getPrice(currency),
- currency, planPhase.getRecurring().getBillingPeriod(),
- billCycleDayLocal, BillingMode.IN_ADVANCE, "Test", 1L, SubscriptionBaseTransitionType.CREATE);
- }
-
- private void testInvoiceGeneration(final UUID accountId, final BillingEventSet events, final List<Invoice> existingInvoices,
- final LocalDate targetDate, final int expectedNumberOfItems,
- final BigDecimal expectedAmount) throws InvoiceApiException {
- final Currency currency = Currency.USD;
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
- final Invoice invoice = invoiceWithMetadata.getInvoice();
- assertNotNull(invoice);
- assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
- existingInvoices.add(invoice);
-
- distributeItems(existingInvoices);
- assertEquals(invoice.getBalance(), KillBillMoney.of(expectedAmount, invoice.getCurrency()));
- }
-
@Test(groups = "fast")
public void testWithFullRepairInvoiceGeneration() throws CatalogApiException, InvoiceApiException {
final LocalDate april25 = new LocalDate(2012, 4, 25);
@@ -1110,32 +1049,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(newInvoice);
}
- private void distributeItems(final List<Invoice> invoices) {
- final Map<UUID, Invoice> invoiceMap = new HashMap<UUID, Invoice>();
-
- for (final Invoice invoice : invoices) {
- invoiceMap.put(invoice.getId(), invoice);
- }
-
- for (final Invoice invoice : invoices) {
- final Iterator<InvoiceItem> itemIterator = invoice.getInvoiceItems().iterator();
- final UUID invoiceId = invoice.getId();
-
- while (itemIterator.hasNext()) {
- final InvoiceItem item = itemIterator.next();
-
- if (!item.getInvoiceId().equals(invoiceId)) {
- final Invoice thisInvoice = invoiceMap.get(item.getInvoiceId());
- if (thisInvoice == null) {
- throw new NullPointerException();
- }
- thisInvoice.addInvoiceItem(item);
- itemIterator.remove();
- }
- }
- }
- }
-
@Test(groups = "fast")
public void testAutoInvoiceOffAccount() throws Exception {
final MockBillingEventSet events = new MockBillingEventSet();
@@ -1158,6 +1071,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(invoiceWithMetadata.getInvoice());
}
+ @Test(groups = "fast")
public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceApiException {
final Currency currency = Currency.USD;
final List<Invoice> invoices = new ArrayList<Invoice>();
@@ -1257,6 +1171,305 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(invoice2);
}
+ // Complex but plausible scenario, with multiple same-day changes, to verify bounds are not triggered
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testMultipleDailyChangesDoNotTriggerBounds() throws InvoiceApiException, CatalogApiException {
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId1 = UUID.randomUUID();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ Invoice invoice;
+
+ final Plan plan1 = new MockPlan("plan1");
+ final PlanPhase plan1Phase1 = createMockMonthlyPlanPhase(null, EIGHT, PhaseType.TRIAL);
+ final PlanPhase plan1Phase2 = createMockMonthlyPlanPhase(TWELVE, PhaseType.DISCOUNT);
+ final LocalDate plan1StartDate = invoiceUtil.buildDate(2011, 1, 5);
+ final LocalDate plan1PhaseChangeDate = invoiceUtil.buildDate(2011, 4, 5);
+
+ final Plan plan2 = new MockPlan("plan2");
+ final PlanPhase plan2Phase1 = createMockMonthlyPlanPhase(null, TWENTY, PhaseType.TRIAL);
+ final PlanPhase plan2Phase2 = createMockMonthlyPlanPhase(THIRTY, PhaseType.DISCOUNT);
+ final PlanPhase plan2Phase3 = createMockMonthlyPlanPhase(FORTY, PhaseType.EVERGREEN);
+ final PlanPhase plan2Phase4 = createMockMonthlyPlanPhase();
+ final LocalDate plan2PhaseChangeToEvergreenDate = invoiceUtil.buildDate(2011, 6, 5);
+ final LocalDate plan2CancelDate = invoiceUtil.buildDate(2011, 6, 5);
+
+ // On 1/5/2011, start TRIAL on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, EIGHT);
+ invoice = invoices.get(0);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(EIGHT), 0);
+
+ // On 1/5/2011, change to TRIAL on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan2, plan2Phase1, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, TWENTY);
+ assertEquals(invoices.get(0), invoice);
+ invoice = invoices.get(1);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWENTY), 0);
+
+ // On 1/5/2011, change back to TRIAL on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+
+ // We don't repair FIXED items and one already exists for that date - nothing to generate
+ testNullInvoiceGeneration(events, invoices, plan1StartDate);
+
+ // On 4/5/2011, phase change to DISCOUNT on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 1, TWELVE);
+ assertEquals(invoices.get(1), invoice);
+ invoice = invoices.get(2);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+
+ // On 4/5/2011, change to DISCOUNT on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan2, plan2Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("18"));
+ assertEquals(invoices.get(2), invoice);
+ invoice = invoices.get(3);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(2).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(TWELVE.negate()), 0);
+
+ // On 4/5/2011, change back to DISCOUNT on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("-18"));
+ assertEquals(invoices.get(3), invoice);
+ invoice = invoices.get(4);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(3).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(THIRTY.negate()), 0);
+
+ // On 4/5/2011, change back to DISCOUNT on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan2, plan2Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("18"));
+ assertEquals(invoices.get(4), invoice);
+ invoice = invoices.get(5);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(4).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(TWELVE.negate()), 0);
+
+ // On 6/5/2011, phase change to EVERGREEN on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan2PhaseChangeToEvergreenDate, plan2, plan2Phase3, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan2PhaseChangeToEvergreenDate, 2, new BigDecimal("70"));
+ assertEquals(invoices.get(5), invoice);
+ invoice = invoices.get(6);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 7, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(FORTY), 0);
+
+ // On 6/5/2011, cancel subscription
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan2CancelDate, plan2, plan2Phase4, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan2PhaseChangeToEvergreenDate, 1, FORTY.negate());
+ assertEquals(invoices.get(6), invoice);
+ invoice = invoices.get(7);
+ assertEquals(invoice.getInvoiceItems().get(0).getLinkedItemId(), invoices.get(6).getInvoiceItems().get(1).getId());
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 7, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(FORTY.negate()), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testBuggyBillingEventsDoNotImpactInvoicing() throws InvoiceApiException, CatalogApiException {
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId1 = UUID.randomUUID();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ Invoice invoice;
+
+ final Plan plan1 = new MockPlan("plan1");
+ final PlanPhase plan1Phase1 = createMockMonthlyPlanPhase(null, EIGHT, PhaseType.TRIAL);
+ final PlanPhase plan1Phase2 = createMockMonthlyPlanPhase(TWELVE, PhaseType.EVERGREEN);
+ final LocalDate plan1StartDate = invoiceUtil.buildDate(2011, 1, 5);
+ final LocalDate plan1PhaseChangeDate = invoiceUtil.buildDate(2011, 2, 5);
+
+ // To simulate a bug, duplicate the billing events
+ for (int i = 0; i < 10; i++) {
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+ }
+ assertEquals(events.size(), 20);
+
+ // Fix for https://github.com/killbill/killbill/issues/467 will prevent duplicate fixed items
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, EIGHT);
+ invoice = invoices.get(0);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(EIGHT), 0);
+
+ // Intermediate billing intervals associated with recurring items will be less than a day, so only one recurring item will be generated
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 1, TWELVE);
+ invoice = invoices.get(1);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 2, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 3, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+ }
+
+ private Long totalOrdering = 1L;
+
+ private MockPlanPhase createMockThirtyDaysPlanPhase(@Nullable final BigDecimal recurringRate) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.THIRTY_DAYS);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase() {
+ return new MockPlanPhase(null, null, BillingPeriod.MONTHLY);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.MONTHLY);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
+ @Nullable final BigDecimal fixedCost,
+ final PhaseType phaseType) {
+ final MockInternationalPrice recurringPrice = (recurringRate == null) ? null : new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD));
+ final MockInternationalPrice fixedPrice = (fixedCost == null) ? null : new MockInternationalPrice(new DefaultPrice(fixedCost, Currency.USD));
+
+ return new MockPlanPhase(recurringPrice, fixedPrice, BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private MockPlanPhase createMockAnnualPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.ANNUAL, phaseType);
+ }
+
+ private SubscriptionBase createSubscription() {
+ return createSubscription(UUID.randomUUID(), UUID.randomUUID());
+ }
+
+ private SubscriptionBase createSubscription(final UUID subscriptionId, final UUID bundleId) {
+ final SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
+ Mockito.when(sub.getId()).thenReturn(subscriptionId);
+ Mockito.when(sub.getBundleId()).thenReturn(bundleId);
+
+ return sub;
+ }
+
+ private BillingEvent createBillingEvent(final UUID subscriptionId, final UUID bundleId, final LocalDate startDate,
+ final Plan plan, final PlanPhase planPhase, final int billCycleDayLocal) throws CatalogApiException {
+ final SubscriptionBase sub = createSubscription(subscriptionId, bundleId);
+ final Currency currency = Currency.USD;
+
+ return invoiceUtil.createMockBillingEvent(null, sub, startDate.toDateTimeAtStartOfDay(), plan, planPhase,
+ planPhase.getFixed().getPrice() == null ? null : planPhase.getFixed().getPrice().getPrice(currency),
+ planPhase.getRecurring().getRecurringPrice() == null ? null : planPhase.getRecurring().getRecurringPrice().getPrice(currency),
+ currency, planPhase.getRecurring().getBillingPeriod(),
+ billCycleDayLocal, BillingMode.IN_ADVANCE, "Test", totalOrdering++, SubscriptionBaseTransitionType.CREATE);
+ }
+
+ private void testInvoiceGeneration(final UUID accountId, final BillingEventSet events, final List<Invoice> existingInvoices,
+ final LocalDate targetDate, final int expectedNumberOfItems,
+ final BigDecimal expectedAmount) throws InvoiceApiException {
+ final Currency currency = Currency.USD;
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+ assertNotNull(invoice);
+ assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
+ existingInvoices.add(invoice);
+
+ distributeItems(existingInvoices);
+ assertEquals(invoice.getBalance(), KillBillMoney.of(expectedAmount, invoice.getCurrency()));
+ }
+
+ private void testNullInvoiceGeneration(final BillingEventSet events, final List<Invoice> existingInvoices, final LocalDate targetDate) throws InvoiceApiException {
+ final Currency currency = Currency.USD;
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+ assertNull(invoice);
+ }
+
+ private void distributeItems(final List<Invoice> invoices) {
+ final Map<UUID, Invoice> invoiceMap = new HashMap<UUID, Invoice>();
+
+ for (final Invoice invoice : invoices) {
+ invoiceMap.put(invoice.getId(), invoice);
+ }
+
+ for (final Invoice invoice : invoices) {
+ final Iterator<InvoiceItem> itemIterator = invoice.getInvoiceItems().iterator();
+ final UUID invoiceId = invoice.getId();
+
+ while (itemIterator.hasNext()) {
+ final InvoiceItem item = itemIterator.next();
+
+ if (!item.getInvoiceId().equals(invoiceId)) {
+ final Invoice thisInvoice = invoiceMap.get(item.getInvoiceId());
+ if (thisInvoice == null) {
+ throw new NullPointerException();
+ }
+ thisInvoice.addInvoiceItem(item);
+ itemIterator.remove();
+ }
+ }
+ }
+ }
+
private void printDetailInvoice(final Invoice invoice) {
log.info("-------------------- START DETAIL ----------------------");
log.info("Invoice " + invoice.getId() + ": BALANCE = " + invoice.getBalance()
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 5d81097..7f19ff7 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -19,6 +19,7 @@ package org.killbill.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -55,6 +56,9 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
@@ -78,7 +82,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
}
}
-
@Test(groups = "fast")
public void testIsSameDayAndSameSubscriptionWithNullPrevEvent() {
@@ -286,7 +289,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
}
@Test(groups = "fast")
- public void testSafetyBounds() throws InvoiceApiException {
+ public void testSafetyBoundsTooManyInvoiceItemsForGivenSubscriptionAndInvoiceDate() throws InvoiceApiException {
final int threshold = 15;
final LocalDate startDate = new LocalDate("2016-01-01");
@@ -373,4 +376,232 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
}
}
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDate() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(null, price, BillingPeriod.NO_BILLING_PERIOD, PhaseType.TRIAL);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ amount,
+ null,
+ account.getCurrency(),
+ BillingPeriod.NO_BILLING_PERIOD,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a bunch of fixed items for that subscription and start date (simulate bad data on disk)
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ for (int i = 0; i < 20; i++) {
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ "Buggy fixed item",
+ startDate,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+ }
+
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ // There will be one proposed, but because it will match one of ones in the existing list and we don't repair, it won't be returned
+ assertEquals(generatedItems.size(), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testSubscriptionAlreadyDoubleBilledForServicePeriod() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a bunch of recurring items for that subscription and service period (bad data on disk leading to double billing)
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ for (int i = 0; i < 20; i++) {
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate.plusMonths(i), account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ // Set random dates to verify it doesn't impact double billing detection
+ startDate.plusMonths(i).toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusMonths(1),
+ amount,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+ }
+
+ try {
+ // There will be one proposed item but the tree will refuse the merge because of the bad state on disk
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final IllegalStateException e) {
+ assertTrue(e.getMessage().startsWith("Double billing detected"));
+ }
+ }
+
+ // Simulate a bug in the generator where two fixed items for the same day and subscription end up in the resulting items
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDatePostMerge() throws InvoiceApiException {
+ final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final Collection<InvoiceItem> resultingItems = new LinkedList<InvoiceItem>();
+ final InvoiceItem fixedPriceInvoiceItem = new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ "description",
+ startDate,
+ BigDecimal.ONE,
+ account.getCurrency());
+ resultingItems.add(fixedPriceInvoiceItem);
+ resultingItems.add(fixedPriceInvoiceItem);
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+
+ resultingItems.clear();
+ for (int i = 0; i < 2; i++) {
+ resultingItems.add(new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ "description",
+ startDate,
+ // Amount shouldn't have any effect
+ BigDecimal.ONE.add(new BigDecimal(i)),
+ account.getCurrency()));
+ }
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+ }
+
+ // Simulate a bug in the generator where two recurring items for the same service period and subscription end up in the resulting items
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyRecurringInvoiceItemsForGivenSubscriptionAndServicePeriodPostMerge() throws InvoiceApiException {
+ final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final Collection<InvoiceItem> resultingItems = new LinkedList<InvoiceItem>();
+ final InvoiceItem recurringInvoiceItem = new RecurringInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ startDate,
+ startDate.plusMonths(1),
+ BigDecimal.ONE,
+ BigDecimal.ONE,
+ account.getCurrency());
+ resultingItems.add(recurringInvoiceItem);
+ resultingItems.add(recurringInvoiceItem);
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+
+ resultingItems.clear();
+ for (int i = 0; i < 2; i++) {
+ resultingItems.add(new RecurringInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ startDate,
+ startDate.plusMonths(1),
+ // Amount shouldn't have any effect
+ BigDecimal.TEN,
+ BigDecimal.ONE,
+ account.getCurrency()));
+ }
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+ }
}
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
index 776621b..b2ac2b0 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
@@ -36,6 +36,16 @@ public interface InvoiceConfig extends KillbillConfig {
@Description("Maximum target date to consider when generating an invoice")
int getNumberOfMonthsInFuture(@Param("dummy") final InternalTenantContext tenantContext);
+ @Config("org.killbill.invoice.sanitySafetyBoundEnabled")
+ @Default("true")
+ @Description("Whether internal sanity checks to prevent mis- and double-billing are enabled")
+ boolean isSanitySafetyBoundEnabled();
+
+ @Config("org.killbill.invoice.sanitySafetyBoundEnabled")
+ @Default("true")
+ @Description("Whether internal sanity checks to prevent mis- and double-billing are enabled")
+ boolean isSanitySafetyBoundEnabled(@Param("dummy") final InternalTenantContext tenantContext);
+
@Config("org.killbill.invoice.maxDailyNumberOfItemsSafetyBound")
@Default("15")
@Description("Maximum daily number of invoice items to generate for a subscription id")