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 {