killbill-memoizeit

invoice: missing Invoice Notification when we have future

1/30/2018 10:04:41 PM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
index c51b21e..1b8b3ed 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
@@ -22,11 +22,16 @@ 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.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.Entitlement;
+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 TestInvoiceNotifications extends TestIntegrationBase {
@@ -63,4 +68,44 @@ public class TestInvoiceNotifications extends TestIntegrationBase {
         // And then verify the invoice is correctly generated
         addDaysAndCheckForCompletion(7, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
     }
+
+
+    @Test(groups = "slow")
+    public void testInvoiceNotificationWithFutureSubscriptionEvents() throws Exception {
+        clock.setDay(new LocalDate(2018, 1, 31));
+
+        final AccountData accountData = getAccountData(28);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+
+        final LocalDate billingDate = new LocalDate(2018, 2, 28);
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial");
+
+
+        busHandler.pushExpectedEvents(NextEvent.BLOCK);
+        final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleKey", null, null, billingDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
+        busHandler.assertListenerStatus();
+
+        // Move to the notification before the start date =>  2018, 2, 21
+        addDaysAndCheckForCompletion(21, NextEvent.INVOICE_NOTIFICATION);
+
+        // Move to the start date => 2018, 2, 28
+        addDaysAndCheckForCompletion(7, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+        final LocalDate futureChangeDate = new LocalDate(2018, 3, 28);
+
+        entitlement.changePlanWithDate(new PlanPhaseSpecifier("shotgun-monthly"), null, futureChangeDate, null, callContext);
+        assertListenerStatus();
+
+        // Move to the notification before the start date =>  2018, 3, 21
+        addDaysAndCheckForCompletion(21, NextEvent.INVOICE_NOTIFICATION);
+
+
+        // Move to the change date => 2018, 3, 28
+        addDaysAndCheckForCompletion(7, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+    }
+
+
 }
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 fabf9c7..808e0c5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -48,10 +48,13 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.SubscriptionEventType;
 import org.killbill.billing.events.BusInternalEvent;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.events.InvoiceNotificationInternalEvent;
+import org.killbill.billing.events.RequestedSubscriptionInternalEvent;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.FutureAccountNotificationsBuilder;
 import org.killbill.billing.invoice.api.DefaultInvoiceService;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.invoice.api.DryRunType;
@@ -107,6 +110,7 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
@@ -168,6 +172,56 @@ public class InvoiceDispatcher {
         this.parkedAccountsManager = parkedAccountsManager;
     }
 
+
+    public void processSubscriptionStartRequestedDate(final RequestedSubscriptionInternalEvent transition, final InternalCallContext context) {
+
+        final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
+        final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
+        if (!isInvoiceNotificationEnabled) {
+            return;
+        }
+
+        final UUID accountId;
+        try {
+            accountId = subscriptionApi.getAccountIdFromSubscriptionId(transition.getSubscriptionId(), context);
+
+
+        } catch (final SubscriptionBaseApiException e) {
+            log.warn("Failed handling SubscriptionBase change.",
+                     new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, transition.getSubscriptionId().toString()));
+            return;
+        }
+
+        try {
+
+            final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, null, context);
+            if (billingEvents.isEmpty()) {
+                return;
+            }
+
+            final FutureAccountNotificationsBuilder notificationsBuilder = new FutureAccountNotificationsBuilder();
+            populateNextFutureDryRunNotificationDate(billingEvents, notificationsBuilder, context);
+
+            final ImmutableAccountData account = accountApi.getImmutableAccountDataById(accountId, context);
+
+            commitInvoiceAndSetFutureNotifications(account, null, notificationsBuilder.build(), context);
+
+        } catch (final SubscriptionBaseApiException e) {
+            log.warn("Failed handling SubscriptionBase change.",
+                     new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, transition.getSubscriptionId().toString()));
+        } catch (final AccountApiException e) {
+            log.warn("Failed to retrieve BillingEvents for accountId='{}'", accountId, e);
+        } catch (final CatalogApiException e) {
+            log.warn("Failed to retrieve BillingEvents for accountId='{}'", accountId, e);
+        }
+
+
+
+
+    }
+
+
+
     public void processSubscriptionForInvoiceGeneration(final EffectiveSubscriptionInternalEvent transition,
                                                         final InternalCallContext context) throws InvoiceApiException {
         final UUID subscriptionId = transition.getSubscriptionId();
@@ -579,8 +633,18 @@ public class InvoiceDispatcher {
         return generator.generateInvoice(account, billingEvents, existingInvoices, targetInvoiceId, targetDate, account.getCurrency(), context);
     }
 
+
+
     private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final InternalCallContext context) {
 
+        final FutureAccountNotificationsBuilder notificationsBuilder = new FutureAccountNotificationsBuilder();
+        populateNextFutureNotificationDate(invoiceWithMetadata, notificationsBuilder);
+        populateNextFutureDryRunNotificationDate(billingEvents, notificationsBuilder, context);
+        return notificationsBuilder.build();
+    }
+
+
+    private void populateNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final FutureAccountNotificationsBuilder notificationsBuilder) {
         final Map<LocalDate, Set<UUID>> notificationListForTrigger = new HashMap<LocalDate, Set<UUID>>();
 
         for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
@@ -609,6 +673,13 @@ public class InvoiceDispatcher {
                 }
             }
         }
+        notificationsBuilder.setNotificationListForTrigger(notificationListForTrigger);
+    }
+
+    private void populateNextFutureDryRunNotificationDate(final BillingEventSet billingEvents, final FutureAccountNotificationsBuilder notificationsBuilder, final InternalCallContext context) {
+
+
+        final Map<LocalDate, Set<UUID>> notificationListForTrigger = notificationsBuilder.getNotificationListForTrigger();
 
         final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
         final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
@@ -625,12 +696,12 @@ public class InvoiceDispatcher {
                 subscriptionsForDryRunDates.addAll(notificationListForTrigger.get(curDate));
             }
 
-            final Map<UUID, DateTime> upcomingPhasesForSubscriptions = isInvoiceNotificationEnabled ?
+            final Map<UUID, DateTime> upcomingTransitionsForSubscriptions = isInvoiceNotificationEnabled ?
                                                                        getNextTransitionsForSubscriptions(billingEvents) :
                                                                        ImmutableMap.<UUID, DateTime>of();
 
-            for (UUID curId : upcomingPhasesForSubscriptions.keySet()) {
-                final LocalDate curDryRunDate = context.toLocalDate(upcomingPhasesForSubscriptions.get(curId).minus(dryRunNotificationTime));
+            for (UUID curId : upcomingTransitionsForSubscriptions.keySet()) {
+                final LocalDate curDryRunDate = context.toLocalDate(upcomingTransitionsForSubscriptions.get(curId).minus(dryRunNotificationTime));
                 Set<UUID> subscriptionsForDryRunDates = notificationListForDryRun.get(curDryRunDate);
                 if (subscriptionsForDryRunDates == null) {
                     subscriptionsForDryRunDates = new HashSet<UUID>();
@@ -639,10 +710,11 @@ public class InvoiceDispatcher {
                 subscriptionsForDryRunDates.add(curId);
             }
         }
-
-        return new FutureAccountNotifications(notificationListForTrigger, notificationListForDryRun);
+        notificationsBuilder.setNotificationListForDryRun(notificationListForDryRun);
     }
 
+
+
     private List<InvoiceItemModelDao> transformToInvoiceModelDao(final List<InvoiceItem> invoiceItems) {
         return Lists.transform(invoiceItems,
                                new Function<InvoiceItem, InvoiceItemModelDao>() {
@@ -778,6 +850,37 @@ public class InvoiceDispatcher {
         public Map<LocalDate, Set<UUID>> getNotificationsForDryRun() {
             return notificationListForDryRun;
         }
+
+
+
+        public static class FutureAccountNotificationsBuilder {
+
+            private Map<LocalDate, Set<UUID>> notificationListForTrigger;
+            private Map<LocalDate, Set<UUID>> notificationListForDryRun;
+
+            public FutureAccountNotificationsBuilder() {
+            }
+
+            public void setNotificationListForTrigger(final Map<LocalDate, Set<UUID>> notificationListForTrigger) {
+                this.notificationListForTrigger = notificationListForTrigger;
+            }
+
+            public void setNotificationListForDryRun(final Map<LocalDate, Set<UUID>> notificationListForDryRun) {
+                this.notificationListForDryRun = notificationListForDryRun;
+            }
+
+            public Map<LocalDate, Set<UUID>> getNotificationListForTrigger() {
+                return MoreObjects.firstNonNull(notificationListForTrigger, ImmutableMap.<LocalDate, Set<UUID>>of());
+            }
+
+            public Map<LocalDate, Set<UUID>> getNotificationListForDryRun() {
+                return MoreObjects.firstNonNull(notificationListForDryRun, ImmutableMap.<LocalDate, Set<UUID>>of());
+            }
+
+            public FutureAccountNotifications build() {
+                return new FutureAccountNotifications(getNotificationListForTrigger(), getNotificationListForDryRun());
+            }
+        }
     }
 
     private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index bf7e879..eee8749 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -29,6 +29,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.events.BlockingTransitionInternalEvent;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.RequestedSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.invoice.api.InvoiceListenerService;
@@ -155,6 +156,15 @@ public class InvoiceListener extends RetryableService implements InvoiceListener
                                                  }
                                              }
                                          });
+        subscriberQueueHandler.subscribe(RequestedSubscriptionInternalEvent.class,
+                                         new SubscriberAction<RequestedSubscriptionInternalEvent>() {
+                                             @Override
+                                             public void run(final RequestedSubscriptionInternalEvent event) {
+                                                 final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+                                                 dispatcher.processSubscriptionStartRequestedDate(event, context);
+                                             }
+                                         });
+
         this.retryableSubscriber = new RetryableSubscriber(clock, this, subscriberQueueHandler);
     }
 
@@ -190,6 +200,14 @@ public class InvoiceListener extends RetryableService implements InvoiceListener
         retryableSubscriber.handleEvent(event);
     }
 
+    @AllowConcurrentEvents
+    @Subscribe
+    public void handleSubscriptionTransition(final RequestedSubscriptionInternalEvent event) {
+        retryableSubscriber.handleEvent(event);
+    }
+
+
+
     public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
         final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
         try {