Commit
Add various tests to verify the current behavior under atypical scenarios, namely: * Complex timelines with various same-day changes do not trigger any bounds * Duplicate (buggy) billing events do not lead to invoicing errors * If multiple fixed items for a given subscription and start date already exist on disk, the next run will not re-generate one * If multiple recurring items for a given subscription and service period already exist on disk, the next run will not complete (the merge logic will detect the existing bad state, i.e. double billing) While the system can already handle these bad cases, it's still possible that either our items generation code (proposed items) or the merge algorithm have issues in even more complex setups (repairs, adjustments, etc.). To pro-actively detect these, this patch introduces new internal safety checks on the resulting list: * At most one FIXED item should be present for a given subscription id and start date * At most one RECURRING item should be present for a given subscription id and service period The behavior can be disabled by setting org.killbill.invoice.sanitySafetyBoundEnabled=false. See #664. Signed-off-by: Pierre-Alexandre Meyer <pierre@mouraf.org>
- Loading branch information
There are no files selected for viewing
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
|
@@ -20,6 +20,7 @@ | ||
import java.math.BigDecimal; | import java.math.BigDecimal; | ||
import java.util.ArrayList; | import java.util.ArrayList; | ||
import java.util.Collection; | import java.util.Collection; | ||
import java.util.HashMap; | |||
import java.util.Iterator; | import java.util.Iterator; | ||
import java.util.List; | import java.util.List; | ||
import java.util.Map; | import java.util.Map; | ||
|
@@ -40,6 +41,7 @@ | ||
import org.killbill.billing.invoice.api.Invoice; | import org.killbill.billing.invoice.api.Invoice; | ||
import org.killbill.billing.invoice.api.InvoiceApiException; | import org.killbill.billing.invoice.api.InvoiceApiException; | ||
import org.killbill.billing.invoice.api.InvoiceItem; | 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.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates; | ||
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem; | import org.killbill.billing.invoice.model.FixedPriceInvoiceItem; | ||
import org.killbill.billing.invoice.model.InvalidDateSequenceException; | import org.killbill.billing.invoice.model.InvalidDateSequenceException; | ||
|
@@ -59,6 +61,7 @@ | ||
import com.google.common.base.MoreObjects; | import com.google.common.base.MoreObjects; | ||
import com.google.common.collect.LinkedListMultimap; | import com.google.common.collect.LinkedListMultimap; | ||
import com.google.common.collect.Multimap; | import com.google.common.collect.Multimap; | ||
import com.google.common.collect.Range; | |||
import com.google.inject.Inject; | import com.google.inject.Inject; | ||
|
|
||
import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods; | import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods; | ||
|
@@ -107,7 +110,7 @@ public List<InvoiceItem> generateItems(final ImmutableAccountData account, final | ||
accountItemTree.mergeWithProposedItems(proposedItems); | accountItemTree.mergeWithProposedItems(proposedItems); | ||
|
|
||
final List<InvoiceItem> resultingItems = accountItemTree.getResultingItemList(); | final List<InvoiceItem> resultingItems = accountItemTree.getResultingItemList(); | ||
safetyBound(resultingItems, createdItemsPerDayPerSubscription, internalCallContext); | safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext); | ||
|
|
||
return resultingItems; | return resultingItems; | ||
} | } | ||
|
@@ -403,8 +406,42 @@ private InvoiceItem generateFixedPriceItem(final UUID invoiceId, final UUID acco | ||
} | } | ||
} | } | ||
|
|
||
// Trigger an exception if we create too many subscriptions for a subscription on a given day | @VisibleForTesting | ||
private void safetyBound(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException { | void safetyBounds(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException { | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
sbrossie
Member
|
|||
// 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()); | |||
This comment has been minimized.
Sorry, something went wrong.
sbrossie
Member
|
|||
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) { | if (config.getMaxDailyNumberOfItemsSafetyBound(internalCallContext) == -1) { | ||
// Safety bound disabled | // Safety bound disabled | ||
return; | return; | ||
|
1 comment
on commit f6ef852
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
The diff is weird. It seems like the
safetyBounds
did not change, just the removal ofprivate
and the new@VisibleForTesting
annotation, yet the whole section is green. Am i missing something?