killbill-aplcache

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 5679190..f6a6c8f 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
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 import org.joda.time.DateTime;
 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.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
@@ -44,6 +45,8 @@ public interface SubscriptionBaseInternalApi {
     public SubscriptionBase createBaseSubscriptionWithAddOns(UUID bundleId, Iterable<EntitlementSpecifier> entitlements, DateTime requestedDateWithMs,
                                                              InternalCallContext context) throws SubscriptionBaseApiException;
 
+    public void cancelBaseSubscriptions(Iterable<SubscriptionBase> subscriptions, BillingActionPolicy policy, InternalCallContext context) throws SubscriptionBaseApiException;
+
     public SubscriptionBaseBundle createBundleForAccount(UUID accountId, String bundleName, InternalCallContext context)
             throws SubscriptionBaseApiException;
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
index 61f6307..82d9278 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * 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
@@ -17,6 +17,10 @@
 
 package org.killbill.billing.entitlement.api;
 
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
 import javax.inject.Inject;
 
 import org.killbill.billing.ErrorCode;
@@ -41,15 +45,48 @@ public class EntitlementPluginExecution {
         T doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException;
     }
 
-
     @Inject
     public EntitlementPluginExecution(final EntitlementApi entitlementApi, final OSGIServiceRegistration<EntitlementPluginApi> pluginRegistry) {
         this.entitlementApi = entitlementApi;
         this.pluginRegistry = pluginRegistry;
     }
 
-    public <T> T executeWithPlugin(final WithEntitlementPlugin<T> callback, final EntitlementContext pluginContext) throws EntitlementApiException {
+    public void executeWithPlugin(final Callable<Void> preCallbacksCallback, final List<WithEntitlementPlugin> callbacks, final Iterable<EntitlementContext> pluginContexts) throws EntitlementApiException {
+        final List<EntitlementContext> updatedPluginContexts = new LinkedList<EntitlementContext>();
+
+        try {
+            for (final EntitlementContext pluginContext : pluginContexts) {
+                final PriorEntitlementResult priorEntitlementResult = executePluginPriorCalls(pluginContext);
+                if (priorEntitlementResult != null && priorEntitlementResult.isAborted()) {
+                    throw new EntitlementApiException(ErrorCode.ENT_PLUGIN_API_ABORTED);
+                }
+                updatedPluginContexts.add(new DefaultEntitlementContext(pluginContext, priorEntitlementResult));
+            }
+
+            preCallbacksCallback.call();
 
+            try {
+                for (int i = 0; i < updatedPluginContexts.size(); i++) {
+                    final EntitlementContext updatedPluginContext = updatedPluginContexts.get(i);
+                    final WithEntitlementPlugin callback = callbacks.get(i);
+
+                    callback.doCall(entitlementApi, updatedPluginContext);
+                    executePluginOnSuccessCalls(updatedPluginContext);
+                }
+            } catch (final EntitlementApiException e) {
+                for (final EntitlementContext updatedPluginContext : updatedPluginContexts) {
+                    executePluginOnFailureCalls(updatedPluginContext);
+                }
+                throw e;
+            }
+        } catch (final EntitlementPluginApiException e) {
+            throw new EntitlementApiException(ErrorCode.ENT_PLUGIN_API_ABORTED, e.getMessage());
+        } catch (final Exception e) {
+            throw new EntitlementApiException(ErrorCode.ENT_PLUGIN_API_ABORTED, e.getMessage());
+        }
+    }
+
+    public <T> T executeWithPlugin(final WithEntitlementPlugin<T> callback, final EntitlementContext pluginContext) throws EntitlementApiException {
         try {
             final PriorEntitlementResult priorEntitlementResult = executePluginPriorCalls(pluginContext);
             if (priorEntitlementResult != null && priorEntitlementResult.isAborted()) {
@@ -57,7 +94,7 @@ public class EntitlementPluginExecution {
             }
             final EntitlementContext updatedPluginContext = new DefaultEntitlementContext(pluginContext, priorEntitlementResult);
             try {
-                T result = callback.doCall(entitlementApi, updatedPluginContext);
+                final T result = callback.doCall(entitlementApi, updatedPluginContext);
                 executePluginOnSuccessCalls(updatedPluginContext);
                 return result;
             } catch (final EntitlementApiException e) {
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 69ad149..3c48358 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
@@ -22,14 +22,16 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.Callable;
 
 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;
@@ -57,6 +59,7 @@ 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.SubscriptionBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.util.callcontext.CallContext;
@@ -91,28 +94,48 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
     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 ImmutableMap.Builder<BlockingState, Optional<UUID>> blockingStates = 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>();
+        final Collection<EntitlementContext> pluginContexts = new LinkedList<EntitlementContext>();
+        final List<WithEntitlementPlugin> callbacks = new LinkedList<WithEntitlementPlugin>();
+        final List<SubscriptionBase> subscriptions = new LinkedList<SubscriptionBase>();
 
-            try {
-                cancelEntitlementWithDateOverrideBillingPolicy(defaultEntitlement, effectiveDate, billingPolicy, blockingStates, notificationEvents, properties, callContext, internalCallContext);
-            } catch (final EntitlementApiException e) {
+        for (final Entitlement entitlement : entitlements) {
+            if (entitlement.getState() == EntitlementState.CANCELLED) {
                 // If subscription has already been cancelled, we ignore and carry on
-                if (e.getCode() != ErrorCode.SUB_CANCEL_BAD_STATE.getCode()) {
-                    throw e;
-                }
+                continue;
             }
 
-            for (final BlockingState blockingState : blockingStates) {
-                states.put(blockingState, Optional.<UUID>fromNullable(entitlement.getBundleId()));
-            }
+            final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CANCEL_SUBSCRIPTION,
+                                                                                   entitlement.getAccountId(),
+                                                                                   null,
+                                                                                   entitlement.getBundleId(),
+                                                                                   entitlement.getExternalKey(),
+                                                                                   null,
+                                                                                   effectiveDate,
+                                                                                   properties,
+                                                                                   callContext);
+            pluginContexts.add(pluginContext);
+
+            final DefaultEntitlement defaultEntitlement = getDefaultEntitlement(entitlement, internalCallContext);
+            final WithEntitlementPlugin<Entitlement> cancelEntitlementWithPlugin = new WithDateOverrideBillingPolicyEntitlementCanceler(defaultEntitlement,
+                                                                                                                                        blockingStates,
+                                                                                                                                        notificationEvents,
+                                                                                                                                        callContext,
+                                                                                                                                        internalCallContext);
+            callbacks.add(cancelEntitlementWithPlugin);
+
+            subscriptions.add(defaultEntitlement.getSubscriptionBase());
         }
 
+        final Callable<Void> preCallbacksCallback = new BulkSubscriptionBaseCancellation(subscriptions,
+                                                                                         billingPolicy,
+                                                                                         internalCallContext);
+
+        pluginExecution.executeWithPlugin(preCallbacksCallback, callbacks, pluginContexts);
+
         // Record the new states first, then insert the notifications to avoid race conditions
-        blockingStateDao.setBlockingStatesAndPostBlockingTransitionEvent(states.build(), internalCallContext);
+        blockingStateDao.setBlockingStatesAndPostBlockingTransitionEvent(blockingStates.build(), internalCallContext);
         for (final DateTime effectiveDateForNotification : notificationEvents.keySet()) {
             for (final NotificationEvent notificationEvent : notificationEvents.get(effectiveDateForNotification)) {
                 recordFutureNotification(effectiveDateForNotification, notificationEvent, internalCallContext);
@@ -120,65 +143,6 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
         }
     }
 
-    // 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) {
@@ -202,4 +166,80 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
             return (DefaultEntitlement) getEntitlementForId(entitlement.getId(), context);
         }
     }
+
+    private class BulkSubscriptionBaseCancellation implements Callable<Void> {
+
+        private final Iterable<SubscriptionBase> subscriptions;
+        private final BillingActionPolicy billingPolicy;
+        private final InternalCallContext callContext;
+
+        public BulkSubscriptionBaseCancellation(final Iterable<SubscriptionBase> subscriptions,
+                                                final BillingActionPolicy billingPolicy,
+                                                final InternalCallContext callContext) {
+            this.subscriptions = subscriptions;
+            this.billingPolicy = billingPolicy;
+            this.callContext = callContext;
+        }
+
+        @Override
+        public Void call() throws Exception {
+            try {
+                subscriptionInternalApi.cancelBaseSubscriptions(subscriptions, billingPolicy, callContext);
+            } catch (final SubscriptionBaseApiException e) {
+                throw new EntitlementApiException(e);
+            }
+
+            return null;
+        }
+    }
+
+    // Note that the implementation is similar to DefaultEntitlement#cancelEntitlementWithDateOverrideBillingPolicy but state isn't persisted on disk
+    private class WithDateOverrideBillingPolicyEntitlementCanceler implements WithEntitlementPlugin<Entitlement> {
+
+        private final DefaultEntitlement entitlement;
+        private final ImmutableMap.Builder<BlockingState, Optional<UUID>> blockingStates;
+        private final Map<DateTime, Collection<NotificationEvent>> notificationEventsWithEffectiveDate;
+        private final CallContext callContext;
+        private final InternalCallContext internalCallContext;
+
+        public WithDateOverrideBillingPolicyEntitlementCanceler(final DefaultEntitlement entitlement,
+                                                                final ImmutableMap.Builder<BlockingState, Optional<UUID>> blockingStates,
+                                                                final Map<DateTime, Collection<NotificationEvent>> notificationEventsWithEffectiveDate,
+                                                                final CallContext callContext,
+                                                                final InternalCallContext internalCallContext) {
+            this.entitlement = entitlement;
+            this.blockingStates = blockingStates;
+            this.notificationEventsWithEffectiveDate = notificationEventsWithEffectiveDate;
+            this.callContext = callContext;
+            this.internalCallContext = internalCallContext;
+        }
+
+        @Override
+        public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            // 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);
+
+            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);
+
+            final Optional<UUID> bundleIdOptional = Optional.<UUID>fromNullable(entitlement.getBundleId());
+            blockingStates.put(newBlockingState, bundleIdOptional);
+            for (final BlockingState blockingState : addOnsBlockingStates) {
+                blockingStates.put(blockingState, bundleIdOptional);
+            }
+
+            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;
+        }
+    }
 }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index 200a0fc..9b3d03d 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+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.BillingPeriod;
@@ -31,7 +32,6 @@ import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanChangeResult;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
@@ -63,6 +63,9 @@ public interface SubscriptionBaseApiService {
     public boolean cancelWithPolicy(DefaultSubscriptionBase subscription, BillingActionPolicy policy, CallContext context)
             throws SubscriptionBaseApiException;
 
+    public boolean cancelWithPolicyNoValidation(Iterable<DefaultSubscriptionBase> subscriptions, BillingActionPolicy policy, InternalCallContext context)
+            throws SubscriptionBaseApiException;
+
     public boolean uncancel(DefaultSubscriptionBase subscription, CallContext context)
             throws SubscriptionBaseApiException;
 
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 64c4d39..5d97842 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
@@ -215,6 +215,23 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
+    public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
+        apiService.cancelWithPolicyNoValidation(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(subscriptions,
+                                                                                                               new Function<SubscriptionBase, DefaultSubscriptionBase>() {
+                                                                                                                   @Override
+                                                                                                                   public DefaultSubscriptionBase apply(final SubscriptionBase subscriptionBase) {
+                                                                                                                       try {
+                                                                                                                           return getDefaultSubscriptionBase(subscriptionBase, context);
+                                                                                                                       } catch (final CatalogApiException e) {
+                                                                                                                           throw new RuntimeException(e);
+                                                                                                                       }
+                                                                                                                   }
+                                                                                                               }),
+                                                policy,
+                                                context);
+    }
+
+    @Override
     public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final InternalCallContext context) throws SubscriptionBaseApiException {
 
         final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
@@ -627,4 +644,14 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             }
         }));
     }
+
+    // For forward-compatibility
+    private DefaultSubscriptionBase getDefaultSubscriptionBase(final SubscriptionBase subscriptionBase, final InternalTenantContext context) throws CatalogApiException {
+        if (subscriptionBase instanceof DefaultSubscriptionBase) {
+            return (DefaultSubscriptionBase) subscriptionBase;
+        } else {
+            // Safe cast, see above
+            return (DefaultSubscriptionBase) dao.getSubscriptionFromId(subscriptionBase.getId(), context);
+        }
+    }
 }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index cb7804e..3a079aa 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -75,6 +75,7 @@ import org.killbill.clock.DefaultClock;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
@@ -221,11 +222,11 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
                                                                     subscription.getCurrentPhase().getPhaseType());
 
         try {
-            final InternalTenantContext internalCallContext = createTenantContextFromBundleId(subscription.getBundleId(), context);
+            final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
             final BillingActionPolicy policy = catalogService.getFullCatalog(internalCallContext).planCancelPolicy(planPhase, now);
             final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
 
-            return doCancelPlan(subscription, now, effectiveDate, context);
+            return doCancelPlan(ImmutableMap.<DefaultSubscriptionBase, DateTime>of(subscription, effectiveDate), now, internalCallContext);
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
         }
@@ -239,7 +240,9 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         }
         final DateTime now = clock.getUTCNow();
         final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
-        return doCancelPlan(subscription, now, effectiveDate, context);
+
+        final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+        return doCancelPlan(ImmutableMap.<DefaultSubscriptionBase, DateTime>of(subscription, effectiveDate), now, internalCallContext);
     }
 
     @Override
@@ -248,33 +251,51 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         if (currentState != null && currentState != EntitlementState.ACTIVE) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
         }
-        final DateTime now = clock.getUTCNow();
-        final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
 
-        return doCancelPlan(subscription, now, effectiveDate, context);
+        final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+        return cancelWithPolicyNoValidation(ImmutableList.<DefaultSubscriptionBase>of(subscription), policy, internalCallContext);
     }
 
-    private boolean doCancelPlan(final DefaultSubscriptionBase subscription, final DateTime now, final DateTime effectiveDate, final CallContext context) throws SubscriptionBaseApiException {
-        validateEffectiveDate(subscription, effectiveDate);
+    @Override
+    public boolean cancelWithPolicyNoValidation(final Iterable<DefaultSubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
+        final Map<DefaultSubscriptionBase, DateTime> subscriptionsWithEffectiveDate = new HashMap<DefaultSubscriptionBase, DateTime>();
+        final DateTime now = clock.getUTCNow();
 
-        final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+        for (final DefaultSubscriptionBase subscription : subscriptions) {
+            final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
+            subscriptionsWithEffectiveDate.put(subscription, effectiveDate);
+        }
+
+        return doCancelPlan(subscriptionsWithEffectiveDate, now, context);
+    }
+
+    private boolean doCancelPlan(final Map<DefaultSubscriptionBase, DateTime> subscriptions, final DateTime now, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
         final List<DefaultSubscriptionBase> subscriptionsToBeCancelled = new LinkedList<DefaultSubscriptionBase>();
         final List<SubscriptionBaseEvent> cancelEvents = new LinkedList<SubscriptionBaseEvent>();
 
         try {
-            subscriptionsToBeCancelled.add(subscription);
-            cancelEvents.addAll(getEventsOnCancelPlan(subscription, effectiveDate, now, false, internalCallContext));
+            for (final DefaultSubscriptionBase subscription : subscriptions.keySet()) {
+                final DateTime effectiveDate = subscriptions.get(subscription);
+                validateEffectiveDate(subscription, effectiveDate);
 
-            if (subscription.getCategory() == ProductCategory.BASE) {
-                subscriptionsToBeCancelled.addAll(computeAddOnsToCancel(cancelEvents, null, subscription.getBundleId(), effectiveDate, internalCallContext));
+                subscriptionsToBeCancelled.add(subscription);
+                cancelEvents.addAll(getEventsOnCancelPlan(subscription, effectiveDate, now, false, internalCallContext));
+
+                if (subscription.getCategory() == ProductCategory.BASE) {
+                    subscriptionsToBeCancelled.addAll(computeAddOnsToCancel(cancelEvents, null, subscription.getBundleId(), effectiveDate, internalCallContext));
+                }
             }
 
             dao.cancelSubscriptions(subscriptionsToBeCancelled, cancelEvents, internalCallContext);
 
-            final Catalog fullCatalog = catalogService.getFullCatalog(internalCallContext);
-            subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
+            boolean allSubscriptionsCancelled = true;
+            for (final DefaultSubscriptionBase subscription : subscriptions.keySet()) {
+                final Catalog fullCatalog = catalogService.getFullCatalog(internalCallContext);
+                subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
+                allSubscriptionsCancelled = allSubscriptionsCancelled && (subscription.getState() == EntitlementState.CANCELLED);
+            }
 
-            return subscription.getState() == EntitlementState.CANCELLED;
+            return allSubscriptionsCancelled;
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
         }