killbill-memoizeit

overdue: cancel entitlements in bulk This fixes the entitlement

1/12/2016 5:14:20 PM

Details

diff --git a/api/src/main/java/org/killbill/billing/entitlement/EntitlementInternalApi.java b/api/src/main/java/org/killbill/billing/entitlement/EntitlementInternalApi.java
index cf812c5..f2eef61 100644
--- a/api/src/main/java/org/killbill/billing/entitlement/EntitlementInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/entitlement/EntitlementInternalApi.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -21,6 +23,7 @@ import java.util.UUID;
 import org.joda.time.LocalDate;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.entitlement.api.Entitlement;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
 import org.killbill.billing.payment.api.PluginProperty;
@@ -34,4 +37,6 @@ public interface EntitlementInternalApi {
     void pause(UUID bundleId, LocalDate effectiveDate, Iterable<PluginProperty> properties, InternalCallContext context) throws EntitlementApiException;
 
     void resume(UUID bundleId, LocalDate localEffectiveDate, Iterable<PluginProperty> properties, InternalCallContext context) throws EntitlementApiException;
+
+    void cancel(Iterable<Entitlement> entitlements, LocalDate effectiveDate, BillingActionPolicy billingPolicy, Iterable<PluginProperty> properties, InternalCallContext context) throws EntitlementApiException;
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 29400c3..7e8a3dd 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -368,6 +368,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         }
     }
 
+    // See also EntitlementInternalApi#cancel for the bulk API
     @Override
     public Entitlement cancelEntitlementWithDateOverrideBillingPolicy(final LocalDate localCancelDate, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
index 52e03f4..69ad149 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -16,25 +18,63 @@
 
 package org.killbill.billing.entitlement.api.svcs;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
 import javax.inject.Inject;
 
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.entitlement.DefaultEntitlementService;
 import org.killbill.billing.entitlement.EntitlementInternalApi;
+import org.killbill.billing.entitlement.EntitlementService;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.DefaultEntitlementApi;
+import org.killbill.billing.entitlement.api.DefaultEntitlementContext;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
 import org.killbill.billing.entitlement.api.EntitlementPluginExecution;
+import org.killbill.billing.entitlement.api.EntitlementPluginExecution.WithEntitlementPlugin;
 import org.killbill.billing.entitlement.block.BlockingChecker;
 import org.killbill.billing.entitlement.dao.BlockingStateDao;
 import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
 import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
+import org.killbill.billing.entitlement.plugin.api.EntitlementContext;
+import org.killbill.billing.entitlement.plugin.api.OperationType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.security.api.SecurityApi;
 import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.bus.api.PersistentBus;
 import org.killbill.clock.Clock;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
 import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
 
 public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase implements EntitlementInternalApi {
 
+    private final BlockingStateDao blockingStateDao;
+
     @Inject
     public DefaultEntitlementInternalApi(final PersistentBus eventBus,
                                          final EntitlementApi entitlementApi, final EntitlementPluginExecution pluginExecution,
@@ -44,6 +84,122 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
                                          final BlockingChecker checker, final NotificationQueueService notificationQueueService,
                                          final EventsStreamBuilder eventsStreamBuilder, final EntitlementUtils entitlementUtils, final SecurityApi securityApi) {
         super(eventBus, entitlementApi, pluginExecution, internalCallContextFactory, subscriptionInternalApi, accountApi, blockingStateDao, clock, checker, notificationQueueService, eventsStreamBuilder, entitlementUtils, securityApi);
+        this.blockingStateDao = blockingStateDao;
     }
 
+    @Override
+    public void cancel(final Iterable<Entitlement> entitlements, final LocalDate effectiveDate, final BillingActionPolicy billingPolicy, final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext) throws EntitlementApiException {
+        final CallContext callContext = internalCallContextFactory.createCallContext(internalCallContext);
+
+        final ImmutableMap.Builder<BlockingState, Optional<UUID>> states = new ImmutableMap.Builder<BlockingState, Optional<UUID>>();
+        final Map<DateTime, Collection<NotificationEvent>> notificationEvents = new HashMap<DateTime, Collection<NotificationEvent>>();
+        for (final Entitlement entitlement : entitlements) {
+            final DefaultEntitlement defaultEntitlement = getDefaultEntitlement(entitlement, internalCallContext);
+            final Collection<BlockingState> blockingStates = new ArrayList<BlockingState>();
+
+            try {
+                cancelEntitlementWithDateOverrideBillingPolicy(defaultEntitlement, effectiveDate, billingPolicy, blockingStates, notificationEvents, properties, callContext, internalCallContext);
+            } catch (final EntitlementApiException e) {
+                // If subscription has already been cancelled, we ignore and carry on
+                if (e.getCode() != ErrorCode.SUB_CANCEL_BAD_STATE.getCode()) {
+                    throw e;
+                }
+            }
+
+            for (final BlockingState blockingState : blockingStates) {
+                states.put(blockingState, Optional.<UUID>fromNullable(entitlement.getBundleId()));
+            }
+        }
+
+        // Record the new states first, then insert the notifications to avoid race conditions
+        blockingStateDao.setBlockingStatesAndPostBlockingTransitionEvent(states.build(), internalCallContext);
+        for (final DateTime effectiveDateForNotification : notificationEvents.keySet()) {
+            for (final NotificationEvent notificationEvent : notificationEvents.get(effectiveDateForNotification)) {
+                recordFutureNotification(effectiveDateForNotification, notificationEvent, internalCallContext);
+            }
+        }
+    }
+
+    // Note that the implementation is similar to DefaultEntitlement#cancelEntitlementWithDateOverrideBillingPolicy but state isn't persisted on disk
+    private void cancelEntitlementWithDateOverrideBillingPolicy(final DefaultEntitlement entitlement,
+                                                                final LocalDate localCancelDate,
+                                                                final BillingActionPolicy billingPolicy,
+                                                                final Collection<BlockingState> blockingStates,
+                                                                final Map<DateTime, Collection<NotificationEvent>> notificationEventsWithEffectiveDate,
+                                                                final Iterable<PluginProperty> properties,
+                                                                final CallContext callContext,
+                                                                final InternalCallContext internalCallContext) throws EntitlementApiException {
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CANCEL_SUBSCRIPTION,
+                                                                               entitlement.getAccountId(),
+                                                                               null,
+                                                                               entitlement.getBundleId(),
+                                                                               entitlement.getExternalKey(),
+                                                                               null,
+                                                                               localCancelDate,
+                                                                               properties,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<Entitlement> cancelEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
+            @Override
+            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+                if (entitlement.getState() == EntitlementState.CANCELLED) {
+                    throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, entitlement.getId(), EntitlementState.CANCELLED);
+                }
+
+                // Make sure to compute the entitlement effective date first to avoid timing issues for IMM cancellations
+                // (we don't want an entitlement cancel date one second or so after the subscription cancel date or add-ons cancellations
+                // computations won't work).
+                final LocalDate effectiveLocalDate = new LocalDate(updatedPluginContext.getEffectiveDate(), entitlement.getAccountTimeZone());
+                final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(effectiveLocalDate, entitlement.getSubscriptionBase().getStartDate(), internalCallContext);
+
+                try {
+                    // Cancel subscription base first, to correctly compute the add-ons entitlements we need to cancel (see below)
+                    entitlement.getSubscriptionBase().cancelWithPolicy(billingPolicy, callContext);
+                } catch (final SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+
+                final BlockingState newBlockingState = new DefaultBlockingState(entitlement.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveDate);
+                final Collection<NotificationEvent> notificationEvents = new ArrayList<NotificationEvent>();
+                final Collection<BlockingState> addOnsBlockingStates = entitlement.computeAddOnBlockingStates(effectiveDate, notificationEvents, callContext, internalCallContext);
+
+                blockingStates.add(newBlockingState);
+                blockingStates.addAll(addOnsBlockingStates);
+
+                if (notificationEventsWithEffectiveDate.get(effectiveDate) == null) {
+                    notificationEventsWithEffectiveDate.put(effectiveDate, notificationEvents);
+                } else {
+                    notificationEventsWithEffectiveDate.get(effectiveDate).addAll(notificationEvents);
+                }
+
+                // Unable to return the new state (not on disk yet)
+                return null;
+            }
+        };
+        pluginExecution.executeWithPlugin(cancelEntitlementWithPlugin, pluginContext);
+    }
+
+    private void recordFutureNotification(final DateTime effectiveDate,
+                                          final NotificationEvent notificationEvent,
+                                          final InternalCallContext context) {
+        try {
+            final NotificationQueue subscriptionEventQueue = notificationQueueService.getNotificationQueue(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                                                                           DefaultEntitlementService.NOTIFICATION_QUEUE_NAME);
+            subscriptionEventQueue.recordFutureNotification(effectiveDate, notificationEvent, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+        } catch (final NoSuchNotificationQueue e) {
+            throw new RuntimeException(e);
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // For forward-compatibility
+    private DefaultEntitlement getDefaultEntitlement(final Entitlement entitlement, final InternalTenantContext context) throws EntitlementApiException {
+        if (entitlement instanceof DefaultEntitlement) {
+            return (DefaultEntitlement) entitlement;
+        } else {
+            // Safe cast
+            return (DefaultEntitlement) getEntitlementForId(entitlement.getId(), context);
+        }
+    }
 }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
index e2b3604..492d548 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
@@ -39,6 +39,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.EntitlementInternalApi;
 import org.killbill.billing.entitlement.api.BlockingApiException;
 import org.killbill.billing.entitlement.api.BlockingStateType;
 import org.killbill.billing.entitlement.api.Entitlement;
@@ -91,6 +92,7 @@ public class OverdueStateApplicator {
     private final PersistentBus bus;
     private final AccountInternalApi accountApi;
     private final EntitlementApi entitlementApi;
+    private final EntitlementInternalApi entitlementInternalApi;
     private final OverdueEmailGenerator overdueEmailGenerator;
     private final TagInternalApi tagApi;
     private final EmailSender emailSender;
@@ -100,6 +102,7 @@ public class OverdueStateApplicator {
     public OverdueStateApplicator(final BlockingInternalApi accessApi,
                                   final AccountInternalApi accountApi,
                                   final EntitlementApi entitlementApi,
+                                  final EntitlementInternalApi entitlementInternalApi,
                                   final Clock clock,
                                   @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED) final OverduePoster checkPoster,
                                   final OverdueEmailGenerator overdueEmailGenerator,
@@ -111,6 +114,7 @@ public class OverdueStateApplicator {
         this.blockingApi = accessApi;
         this.accountApi = accountApi;
         this.entitlementApi = entitlementApi;
+        this.entitlementInternalApi = entitlementInternalApi;
         this.clock = clock;
         this.checkPoster = checkPoster;
         this.overdueEmailGenerator = overdueEmailGenerator;
@@ -314,15 +318,10 @@ public class OverdueStateApplicator {
             final List<Entitlement> toBeCancelled = new LinkedList<Entitlement>();
             computeEntitlementsToCancel(account, toBeCancelled, callContext);
 
-            for (final Entitlement cur : toBeCancelled) {
-                try {
-                    cur.cancelEntitlementWithDateOverrideBillingPolicy(new LocalDate(clock.getUTCNow(), account.getTimeZone()), actionPolicy, ImmutableList.<PluginProperty>of(), callContext);
-                } catch (final EntitlementApiException e) {
-                    // If subscription has already been cancelled, there is nothing to do so we can ignore
-                    if (e.getCode() != ErrorCode.SUB_CANCEL_BAD_STATE.getCode()) {
-                        throw new OverdueException(e);
-                    }
-                }
+            try {
+                entitlementInternalApi.cancel(toBeCancelled, new LocalDate(clock.getUTCNow(), account.getTimeZone()), actionPolicy, ImmutableList.<PluginProperty>of(), context);
+            } catch (final EntitlementApiException e) {
+                throw new OverdueException(e);
             }
         } catch (final EntitlementApiException e) {
             throw new OverdueException(e);
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java
index 7387ab3..79fd448 100644
--- a/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java
@@ -43,6 +43,7 @@ public class MockEntitlementModule extends KillBillModule implements Entitlement
         installBlockingStateDao();
         installBlockingApi();
         installEntitlementApi();
+        installEntitlementInternalApi();
         installBlockingChecker();
     }