killbill-memoizeit

invoice: Fixes for invoice dry run. See #774 * Revert dryrun

12/13/2017 4:15:45 AM

Details

diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 8971143..7c4b710 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -93,8 +93,6 @@ public interface SubscriptionBaseInternalApi {
 
     public void updateExternalKey(UUID bundleId, String newExternalKey, InternalCallContext context);
 
-    public Iterable<DateTime> getFutureNotificationsForAccount(InternalCallContext context);
-
     public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final List<SubscriptionBaseTransitionType> eventTypes, final InternalCallContext internalCallContext);
 
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 4c3745f..af000cf 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -991,7 +991,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(internalCallContext).getMillis();
         for (final LocalDate notificationDate : callbackDateTimePerSubscriptions.getNotificationsForDryRun().keySet()) {
             final DateTime notificationDateTime = internalCallContext.toUTCDateTime(notificationDate);
-            final Set<UUID> subscriptionIds = callbackDateTimePerSubscriptions.getNotificationsForTrigger().get(notificationDate);
+            final Set<UUID> subscriptionIds = callbackDateTimePerSubscriptions.getNotificationsForDryRun().get(notificationDate);
             nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionIds, notificationDateTime, notificationDateTime.plusMillis((int) dryRunNotificationTime), internalCallContext);
         }
     }
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 {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index d4cfd26..b6ee8da 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -83,9 +83,6 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
                                            internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
                                            notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
 
-        Mockito.when(subscriptionApi.getFutureNotificationsForAccount(Mockito.<InternalCallContext>any())).thenReturn(ImmutableList.<DateTime>of());
-
-
     }
 
     @Test(groups = "slow")
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 f0cec38..448b395 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -219,7 +219,6 @@ public class TestInvoiceHelper {
                                                                    invoiceDao, internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
 
-        Mockito.when(subscriptionApi.getFutureNotificationsForAccount(Mockito.<InternalCallContext>any())).thenReturn(ImmutableList.<DateTime>of());
         Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
         Assert.assertNotNull(invoice);
 
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index 8cf40a2..95dbcc3 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -73,7 +73,6 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
 import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
 import org.killbill.billing.subscription.engine.addon.AddonUtils;
-import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
 import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
 import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
@@ -90,17 +89,12 @@ import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 import org.killbill.clock.Clock;
 import org.killbill.clock.DefaultClock;
-import org.killbill.notificationq.api.NotificationEvent;
-import org.killbill.notificationq.api.NotificationEventWithMetadata;
-import org.killbill.notificationq.api.NotificationQueue;
 import org.killbill.notificationq.api.NotificationQueueService;
-import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -758,24 +752,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
-    public Iterable<DateTime> getFutureNotificationsForAccount(final InternalCallContext internalCallContext) {
-        try {
-            final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultSubscriptionBaseService.SUBSCRIPTION_SERVICE_NAME,
-                                                                                                      DefaultSubscriptionBaseService.NOTIFICATION_QUEUE_NAME);
-            final Iterable<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
-            return Iterables.transform(futureNotifications, new Function<NotificationEventWithMetadata<NotificationEvent>, DateTime>() {
-                @Nullable
-                @Override
-                public DateTime apply(final NotificationEventWithMetadata<NotificationEvent> input) {
-                    return input.getEffectiveDate();
-                }
-            });
-        } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
-            throw new IllegalStateException(noSuchNotificationQueue);
-        }
-    }
-
-    @Override
     public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final List<SubscriptionBaseTransitionType> eventTypes, final InternalCallContext internalCallContext) {
         final Iterable<SubscriptionBaseEvent> filteredEvents = dao.getFutureEventsForAccount(eventTypes, internalCallContext);
         final Map<UUID, DateTime> result = filteredEvents.iterator().hasNext() ? new HashMap<UUID, DateTime>() : ImmutableMap.<UUID, DateTime>of();