diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 63bd632..feb4604 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -306,18 +306,39 @@ public class InvoiceDispatcher {
}
}
} else /* Dry run use cases */ {
+
+ final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+ final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+
+ // Events that are prone to trigger an invoice
+ // TODO we can't specify START_BILLING_DISABLED and END_BILLING_DISABLED as those are generated by junction
+ final ImmutableList<SubscriptionBaseTransitionType> eventTypes = ImmutableList.<SubscriptionBaseTransitionType>of(SubscriptionBaseTransitionType.CREATE,
+ SubscriptionBaseTransitionType.CHANGE,
+ SubscriptionBaseTransitionType.CANCEL,
+ SubscriptionBaseTransitionType.PHASE,
+ SubscriptionBaseTransitionType.BCD_CHANGE);
+ final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap = subscriptionApi.getNextFutureEventForSubscriptions(eventTypes, context);
+
+ // List of all existing invoice notifications
+ final List<LocalDate> allCandidateTargetDates = getUpcomingInvoiceCandidateDates(futureNotifications, nextScheduledSubscriptionsEventMap, ImmutableList.<UUID>of(), context);
+
if (dryRunArguments.getDryRunType() == DryRunType.UPCOMING_INVOICE) {
final Iterable<UUID> filteredSubscriptionIdsForDryRun = getFilteredSubscriptionIdsFor_UPCOMING_INVOICE_DryRun(dryRunArguments, billingEvents);
- final List<LocalDate> candidateTargetDates = getUpcomingInvoiceCandidateDates(filteredSubscriptionIdsForDryRun, context);
+
+ // List of existing invoice notifications associated to the filter set of subscriptionIds
+ final List<LocalDate> filteredCandidateTargetDates = Iterables.isEmpty(filteredSubscriptionIdsForDryRun) ?
+ allCandidateTargetDates :
+ getUpcomingInvoiceCandidateDates(futureNotifications, nextScheduledSubscriptionsEventMap, filteredSubscriptionIdsForDryRun, context);
if (Iterables.isEmpty(filteredSubscriptionIdsForDryRun)) {
- invoice = processDryRun_UPCOMING_INVOICE_Invoice(accountId, candidateTargetDates, billingEvents, existingInvoices, context);
+ invoice = processDryRun_UPCOMING_INVOICE_Invoice(accountId, allCandidateTargetDates, billingEvents, existingInvoices, context);
} else {
- invoice = processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(accountId, candidateTargetDates, billingEvents, existingInvoices, context);
+ invoice = processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(accountId, filteredCandidateTargetDates, allCandidateTargetDates, billingEvents, existingInvoices, context);
}
- } else /* DryRunType.TARGET_DATE */ {
- invoice = processDryRun_TARGET_DATE_Invoice(accountId, inputTargetDate, billingEvents, existingInvoices, context);
+ } else /* DryRunType.TARGET_DATE, SUBSCRIPTION_ACTION */ {
+ invoice = processDryRun_TARGET_DATE_Invoice(accountId, inputTargetDate, allCandidateTargetDates, billingEvents, existingInvoices, context);
}
}
return invoice;
@@ -336,11 +357,18 @@ public class InvoiceDispatcher {
parkAccount(accountId, context);
}
throw e;
+ } catch (final NoSuchNotificationQueue e) {
+ // Should not happen, notificationQ is only used for dry run mode
+ if (!isDryRun) {
+ log.warn("Illegal invoicing state detected for accountId='{}', dryRunArguments='{}', parking account", accountId, dryRunArguments, e);
+ parkAccount(accountId, context);
+ }
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, "Failed to retrieve future notifications from notificationQ");
}
}
- private Invoice processDryRun_UPCOMING_INVOICE_Invoice(final UUID accountId, final List<LocalDate> candidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
- for (final LocalDate curTargetDate : candidateTargetDates) {
+ private Invoice processDryRun_UPCOMING_INVOICE_Invoice(final UUID accountId, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+ for (final LocalDate curTargetDate : allCandidateTargetDates) {
final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDate, billingEvents, existingInvoices, true, context);
if (invoice != null) {
return invoice;
@@ -350,9 +378,9 @@ public class InvoiceDispatcher {
}
- private Invoice processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(final UUID accountId, final List<LocalDate> candidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
- for (final LocalDate curTargetDate : candidateTargetDates) {
- final Invoice invoice = processDryRun_TARGET_DATE_Invoice(accountId, curTargetDate, billingEvents, existingInvoices, context);
+ private Invoice processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(final UUID accountId, final List<LocalDate> filteringCandidateTargetDates, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+ for (final LocalDate curTargetDate : filteringCandidateTargetDates) {
+ final Invoice invoice = processDryRun_TARGET_DATE_Invoice(accountId, curTargetDate, allCandidateTargetDates, billingEvents, existingInvoices, context);
if (invoice != null) {
return invoice;
}
@@ -361,11 +389,19 @@ public class InvoiceDispatcher {
}
- private Invoice processDryRun_TARGET_DATE_Invoice(final UUID accountId, final LocalDate targetDate, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+ private Invoice processDryRun_TARGET_DATE_Invoice(final UUID accountId, final LocalDate targetDate, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+ LocalDate prevLocalDate = null;
+ for (final LocalDate cur : allCandidateTargetDates) {
+ if (cur.compareTo(targetDate) < 0) {
+ prevLocalDate = cur;
+ }
+ }
// Generate a dryRun invoice for such date if required in such a way that dryRun invoice on our targetDate only contains items that we expect to see
- final Invoice additionalInvoice = processAccountWithLockAndInputTargetDate(accountId, targetDate.minusDays(1), billingEvents, existingInvoices, true, context);
+ final Invoice additionalInvoice = prevLocalDate != null ?
+ processAccountWithLockAndInputTargetDate(accountId, prevLocalDate, billingEvents, existingInvoices, true, context) :
+ null;
final List<Invoice> augmentedExistingInvoices = additionalInvoice != null ?
new ImmutableList.Builder().addAll(existingInvoices).add(additionalInvoice).build() :
@@ -566,7 +602,7 @@ public class InvoiceDispatcher {
subscriptionsForDryRunDates = new HashSet<UUID>();
notificationListForDryRun.put(curDryRunDate, subscriptionsForDryRunDates);
}
- subscriptionsForDryRunDates.addAll(notificationListForDryRun.get(curDate));
+ subscriptionsForDryRunDates.addAll(notificationListForTrigger.get(curDate));
}
@@ -742,18 +778,12 @@ public class InvoiceDispatcher {
- private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<UUID> filteredSubscriptionIds, final InternalCallContext internalCallContext) {
- final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(filteredSubscriptionIds, internalCallContext);
-
- // Events that are prone to trigger an invoice
- // TODO we can't specify START_BILLING_DISABLED and END_BILLING_DISABLED as those are generated by junction
- final ImmutableList<SubscriptionBaseTransitionType> eventTypes = ImmutableList.<SubscriptionBaseTransitionType>of(SubscriptionBaseTransitionType.CREATE,
- SubscriptionBaseTransitionType.CHANGE,
- SubscriptionBaseTransitionType.CANCEL,
- SubscriptionBaseTransitionType.PHASE,
- SubscriptionBaseTransitionType.BCD_CHANGE);
+ private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
+ final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap,
+ final Iterable<UUID> filteredSubscriptionIds,
+ final InternalCallContext internalCallContext) {
- final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap = subscriptionApi.getNextFutureEventForSubscriptions(eventTypes, internalCallContext);
+ final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(futureNotifications, filteredSubscriptionIds);
final Iterable<DateTime> nextScheduledSubscriptionsEvents;
if (!Iterables.isEmpty(filteredSubscriptionIds)) {
@@ -777,38 +807,30 @@ public class InvoiceDispatcher {
});
}
- private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final Iterable<UUID> filteredSubscriptionIds, final InternalCallContext internalCallContext) {
- try {
- final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
- DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
- final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
-
- final Collection<DateTime> effectiveDates = new LinkedList<DateTime>();
- for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
-
- // If we don't specify a filter list of subscriptionIds, we look at all events.
- boolean isEventForSubscription = Iterables.isEmpty(filteredSubscriptionIds);
- // If we specify a filter, we keep the date if at least one of the subscriptions from the event list matches one of the subscription from our filter list
- if (!Iterables.isEmpty(filteredSubscriptionIds)) {
- for (final UUID curSubscriptionId : filteredSubscriptionIds) {
- if (Iterables.contains(input.getEvent().getUuidKeys(), curSubscriptionId)) {
- isEventForSubscription = true;
- break;
- }
+ private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
+ final Iterable<UUID> filteredSubscriptionIds) {
+ final Collection<DateTime> effectiveDates = new LinkedList<DateTime>();
+ for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
+
+ // If we don't specify a filter list of subscriptionIds, we look at all events.
+ boolean isEventForSubscription = Iterables.isEmpty(filteredSubscriptionIds);
+ // If we specify a filter, we keep the date if at least one of the subscriptions from the event list matches one of the subscription from our filter list
+ if (!Iterables.isEmpty(filteredSubscriptionIds)) {
+ for (final UUID curSubscriptionId : filteredSubscriptionIds) {
+ if (Iterables.contains(input.getEvent().getUuidKeys(), curSubscriptionId)) {
+ isEventForSubscription = true;
+ break;
}
}
+ }
-
- final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
- input.getEvent().isDryRunForInvoiceNotification() : false;
- if (isEventForSubscription && !isEventDryRunForNotifications) {
- effectiveDates.add(input.getEffectiveDate());
- }
+ final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
+ input.getEvent().isDryRunForInvoiceNotification() : false;
+ if (isEventForSubscription && !isEventDryRunForNotifications) {
+ effectiveDates.add(input.getEffectiveDate());
}
- return effectiveDates;
- } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
- throw new IllegalStateException(noSuchNotificationQueue);
}
+ return effectiveDates;
}
private static final class TargetDateDryRunArguments implements DryRunArguments {