killbill-memoizeit

Details

diff --git a/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java b/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java
index 20027f2..a62fc85 100644
--- a/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java
+++ b/api/src/main/java/org/killbill/billing/entitlement/EventsStream.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -64,6 +64,8 @@ public interface EventsStream {
 
     boolean isBlockChange(final DateTime effectiveDate);
 
+    boolean isBlockEntitlement(final DateTime effectiveDate);
+
     int getDefaultBillCycleDayLocal();
 
     Collection<BlockingState> getPendingEntitlementCancellationEvents();
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 ac1df1a..0728a12 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
@@ -65,6 +65,8 @@ public interface SubscriptionBaseInternalApi {
 
     public Iterable<UUID> getNonAOSubscriptionIdsForKey(String bundleKey, InternalTenantContext context);
 
+    public SubscriptionBaseBundle getActiveBundleForKey(String bundleKey, Catalog catalog, InternalTenantContext context);
+
     public List<SubscriptionBase> getSubscriptionsForBundle(UUID bundleId, DryRunArguments dryRunArguments, InternalTenantContext context)
             throws SubscriptionBaseApiException;
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index 475527c..9c7423e 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -38,6 +38,9 @@ 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.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogInternalApi;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.ProductCategory;
@@ -72,12 +75,12 @@ import org.killbill.clock.Clock;
 import org.killbill.notificationq.api.NotificationQueueService;
 
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 
-import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logCreateEntitlement;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logCreateEntitlementsWithAOs;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logPauseResumeEntitlement;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logTransferEntitlement;
@@ -102,6 +105,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
     private final NotificationQueueService notificationQueueService;
     private final EntitlementPluginExecution pluginExecution;
     private final SecurityApi securityApi;
+    private final CatalogInternalApi catalogInternalApi;
 
     @Inject
     public DefaultEntitlementApi(final PersistentBus eventBus, final InternalCallContextFactory internalCallContextFactory,
@@ -110,6 +114,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                                  final BlockingChecker checker, final NotificationQueueService notificationQueueService,
                                  final EventsStreamBuilder eventsStreamBuilder, final EntitlementUtils entitlementUtils,
                                  final EntitlementPluginExecution pluginExecution,
+                                 final CatalogInternalApi catalogInternalApi,
                                  final SecurityApi securityApi) {
         super(eventBus, null, pluginExecution, internalCallContextFactory, subscriptionInternalApi, accountApi, blockingStateDao, clock, checker, notificationQueueService, eventsStreamBuilder, entitlementUtils, securityApi);
         this.internalCallContextFactory = internalCallContextFactory;
@@ -123,6 +128,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
         this.entitlementUtils = entitlementUtils;
         this.pluginExecution = pluginExecution;
         this.securityApi = securityApi;
+        this.catalogInternalApi = catalogInternalApi;
         this.dateHelper = new EntitlementDateHelper();
     }
 
@@ -137,7 +143,8 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                                                                                                                                     entitlementEffectiveDate,
                                                                                                                                     billingEffectiveDate,
                                                                                                                                     isMigrated);
-        final List<UUID> createdEntitlements = createBaseEntitlementsWithAddOns(accountId,
+        final List<UUID> createdEntitlements = createBaseEntitlementsWithAddOns(OperationType.CREATE_SUBSCRIPTION,
+                                                                                accountId,
                                                                                 ImmutableList.<BaseEntitlementWithAddOnsSpecifier>of(baseEntitlementWithAddOnsSpecifier),
                                                                                 renameCancelledBundleIfExist,
                                                                                 properties,
@@ -145,182 +152,41 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
         return createdEntitlements.get(0);
     }
 
-    private BaseEntitlementWithAddOnsSpecifier getFirstBaseEntitlementWithAddOnsSpecifier(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers) throws SubscriptionBaseApiException {
-        if (baseEntitlementWithAddOnsSpecifiers == null) {
-            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
-        }
-
-        final Iterator<BaseEntitlementWithAddOnsSpecifier> iterator = baseEntitlementWithAddOnsSpecifiers.iterator();
-        if (!iterator.hasNext()) {
-            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
-        }
-
-        return iterator.next();
-    }
-
     @Override
     public List<UUID> createBaseEntitlementsWithAddOns(final UUID accountId, final Iterable<BaseEntitlementWithAddOnsSpecifier> originalBaseEntitlementWithAddOnsSpecifiers, final boolean renameCancelledBundleIfExist, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
-        logCreateEntitlementsWithAOs(log, originalBaseEntitlementWithAddOnsSpecifiers);
-
-        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SHOPPING_CART_SUBSCRIPTIONS,
-                                                                               accountId,
-                                                                               null,
-                                                                               originalBaseEntitlementWithAddOnsSpecifiers,
-                                                                               null,
-                                                                               properties,
-                                                                               callContext);
-
-        final WithEntitlementPlugin<List<UUID>> createBaseEntitlementsWithAddOns = new WithEntitlementPlugin<List<UUID>>() {
-            @Override
-            public List<UUID> doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
-                final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
-
-                final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiersAfterPlugins = updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers();
-                final Collection<SubscriptionBaseWithAddOnsSpecifier> subscriptionBaseWithAddOnsSpecifiers = new LinkedList<SubscriptionBaseWithAddOnsSpecifier>();
-                DateTime upTo = null;
-                for (final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier : baseEntitlementWithAddOnsSpecifiersAfterPlugins) {
-                    // Entitlement
-                    final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(),
-                                                                                                       updatedPluginContext.getCreatedDate(),
-                                                                                                       contextWithValidAccountRecordId);
-                    upTo = upTo == null || upTo.compareTo(entitlementRequestedDate) < 0 ? entitlementRequestedDate : upTo;
-
-                    final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier = new SubscriptionBaseWithAddOnsSpecifier(baseEntitlementWithAddOnsSpecifier.getBundleId(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifier.getExternalKey(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifier.getEntitlementSpecifier(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifier.getBillingEffectiveDate(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifier.isMigrated());
-                    subscriptionBaseWithAddOnsSpecifiers.add(subscriptionBaseWithAddOnsSpecifier);
-                }
-
-                try {
-                    // Verify if operation is allowed by looking for is_block_change on Account
-                    // Note that to fully check for block_change we should also look for BlockingState at the BUNDLE/SUBSCRIPTION level in case some of the input contain a BP that already exists.
-                    checkForAccountBlockingChange(accountId, upTo, contextWithValidAccountRecordId);
-
-                    final List<SubscriptionBaseWithAddOns> subscriptionsWithAddOns = subscriptionBaseInternalApi.createBaseSubscriptionsWithAddOns(subscriptionBaseWithAddOnsSpecifiers,
-                                                                                                                            renameCancelledBundleIfExist,
-                                                                                                                            contextWithValidAccountRecordId);
-                    final List<UUID> createdSubscriptionIds = new LinkedList<UUID>();
-                    final Map<BlockingState, UUID> blockingStateMap = new HashMap<BlockingState, UUID>();
-                    int i = 0;
-                    for (final Iterator<BaseEntitlementWithAddOnsSpecifier> it = baseEntitlementWithAddOnsSpecifiersAfterPlugins.iterator(); it.hasNext(); i++) {
-                        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = it.next();
-                        for (final SubscriptionBase subscriptionBase : subscriptionsWithAddOns.get(i).getSubscriptionBaseList()) {
-                            final BlockingState blockingState = new DefaultBlockingState(subscriptionBase.getId(),
-                                                                                         BlockingStateType.SUBSCRIPTION,
-                                                                                         DefaultEntitlementApi.ENT_STATE_START,
-                                                                                         EntitlementService.ENTITLEMENT_SERVICE_NAME,
-                                                                                         false,
-                                                                                         false,
-                                                                                         false,
-                                                                                         dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), updatedPluginContext.getCreatedDate(), contextWithValidAccountRecordId));
-                            blockingStateMap.put(blockingState, subscriptionsWithAddOns.get(i).getBundle().getId());
-
-                            createdSubscriptionIds.add(subscriptionBase.getId());
-                        }
-                    }
-                    entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(blockingStateMap, contextWithValidAccountRecordId);
-                    return createdSubscriptionIds;
-                } catch (final SubscriptionBaseApiException e) {
-                    throw new EntitlementApiException(e);
-                }
-            }
-        };
-        return pluginExecution.executeWithPlugin(createBaseEntitlementsWithAddOns, pluginContext);
+        return createBaseEntitlementsWithAddOns(OperationType.CREATE_SHOPPING_CART_SUBSCRIPTIONS,
+                                                accountId,
+                                                originalBaseEntitlementWithAddOnsSpecifiers,
+                                                renameCancelledBundleIfExist,
+                                                properties,
+                                                callContext);
     }
 
     @Override
     public UUID addEntitlement(final UUID bundleId, final PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate entitlementEffectiveDate, @Nullable final LocalDate billingEffectiveDate,
-            final boolean isMigrated, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
-        logCreateEntitlement(log, bundleId, planPhaseSpecifier, overrides, entitlementEffectiveDate, billingEffectiveDate);
-
+                               final boolean isMigrated, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
         final EntitlementSpecifier entitlementSpecifier = new DefaultEntitlementSpecifier(planPhaseSpecifier, overrides);
-        final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
-        entitlementSpecifierList.add(entitlementSpecifier);
-        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = new DefaultBaseEntitlementWithAddOnsSpecifier(
-                bundleId,
-                null,
-                entitlementSpecifierList,
-                entitlementEffectiveDate,
-                billingEffectiveDate,
-                isMigrated);
-        final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
-        baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementWithAddOnsSpecifier);
-
-        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
-                                                                               null,
-                                                                               null,
-                                                                               baseEntitlementWithAddOnsSpecifierList,
-                                                                               null,
-                                                                               properties,
-                                                                               callContext);
-
-        final WithEntitlementPlugin<UUID> addEntitlementWithPlugin = new WithEntitlementPlugin<UUID>() {
-            @Override
-            public UUID doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
-                final InternalCallContext context = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, callContext);
-
-                final List<SubscriptionBase> subscriptionsByBundle;
-                try {
-                    subscriptionsByBundle = subscriptionBaseInternalApi.getSubscriptionsForBundle(bundleId, null, context);
-
-                    if (subscriptionsByBundle == null || subscriptionsByBundle.isEmpty()) {
-                        throw new EntitlementApiException(ErrorCode.SUB_NO_ACTIVE_SUBSCRIPTIONS, bundleId);
-                    }
-                } catch (final SubscriptionBaseApiException e) {
-                    throw new EntitlementApiException(e);
-                }
-                final boolean isStandalone = ProductCategory.STANDALONE.equals(subscriptionsByBundle.get(0).getCategory());
-
-                final EventsStream eventsStreamForBaseSubscription = isStandalone ? null : eventsStreamBuilder.buildForBaseSubscription(bundleId, callContext);
-
-                final DateTime entitlementRequestedDateRaw = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), updatedPluginContext.getCreatedDate(), context);
-                DateTime entitlementRequestedDate = entitlementRequestedDateRaw;
-                if (!isStandalone) {
-                    final DateTime baseEntitlementStartDate = eventsStreamForBaseSubscription.getEntitlementEffectiveStartDateTime();
-                    entitlementRequestedDate = entitlementRequestedDateRaw.isBefore(baseEntitlementStartDate) ? baseEntitlementStartDate : entitlementRequestedDateRaw;
-                    preCheckAddEntitlement(bundleId, entitlementRequestedDate, baseEntitlementWithAddOnsSpecifier, eventsStreamForBaseSubscription, callContext);
-                }
-
-                try {
-                    final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifierAfterPlugins = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
-                    final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier = new SubscriptionBaseWithAddOnsSpecifier(baseEntitlementWithAddOnsSpecifierAfterPlugins.getBundleId(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifierAfterPlugins.getExternalKey(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifierAfterPlugins.getEntitlementSpecifier(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifierAfterPlugins.getBillingEffectiveDate(),
-                                                                                                                                            baseEntitlementWithAddOnsSpecifierAfterPlugins.isMigrated());
-
-                    final List<SubscriptionBaseWithAddOns> subscriptionsWithAddOns = subscriptionBaseInternalApi.createBaseSubscriptionsWithAddOns(ImmutableList.<SubscriptionBaseWithAddOnsSpecifier>of(subscriptionBaseWithAddOnsSpecifier),
-                                                                                                                                                   false,
-                                                                                                                                                   context);
-                    final SubscriptionBaseWithAddOns subscriptionBaseWithAddOns = subscriptionsWithAddOns.get(0);
-                    final UUID subscriptionId = subscriptionBaseWithAddOns.getSubscriptionBaseList().get(0).getId();
-
-                    final BlockingState newBlockingState = new DefaultBlockingState(subscriptionId, BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_START, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, entitlementRequestedDate);
-                    entitlementUtils.setBlockingStatesAndPostBlockingTransitionEvent(ImmutableList.<BlockingState>of(newBlockingState), subscriptionBaseWithAddOns.getBundle().getId(), context);
-
-                    return subscriptionId;
-                } catch (final SubscriptionBaseApiException e) {
-                    throw new EntitlementApiException(e);
-                }
-            }
-        };
-        return pluginExecution.executeWithPlugin(addEntitlementWithPlugin, pluginContext);
-    }
-
-    private void preCheckAddEntitlement(final UUID bundleId, final DateTime entitlementRequestedDate, final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier, final EventsStream eventsStreamForBaseSubscription, final CallContext callContext) throws EntitlementApiException {
-        if (eventsStreamForBaseSubscription.isEntitlementCancelled() ||
-            (eventsStreamForBaseSubscription.isEntitlementPending() &&
-             (baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate() == null ||
-              baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate().compareTo(eventsStreamForBaseSubscription.getEntitlementEffectiveStartDate()) < 0))) {
-            throw new EntitlementApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
+        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = new DefaultBaseEntitlementWithAddOnsSpecifier(bundleId,
+                                                                                                                                    null,
+                                                                                                                                    ImmutableList.<EntitlementSpecifier>of(entitlementSpecifier),
+                                                                                                                                    entitlementEffectiveDate,
+                                                                                                                                    billingEffectiveDate,
+                                                                                                                                    isMigrated);
+        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, callContext);
+        final UUID accountId;
+        try {
+            accountId = subscriptionBaseInternalApi.getAccountIdFromBundleId(bundleId, context);
+        } catch (final SubscriptionBaseApiException e) {
+            throw new EntitlementApiException(e);
         }
 
-        // Check the base entitlement state is not blocked
-        if (eventsStreamForBaseSubscription.isBlockChange(entitlementRequestedDate)) {
-            throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
-        }
+        final List<UUID> createdEntitlements = createBaseEntitlementsWithAddOns(OperationType.CREATE_SUBSCRIPTION,
+                                                                                accountId,
+                                                                                ImmutableList.<BaseEntitlementWithAddOnsSpecifier>of(baseEntitlementWithAddOnsSpecifier),
+                                                                                false,
+                                                                                properties,
+                                                                                callContext);
+        return createdEntitlements.get(0);
     }
 
     @Override
@@ -506,8 +372,184 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
         return pluginExecution.executeWithPlugin(transferWithPlugin, pluginContext);
     }
 
-    private void checkForAccountBlockingChange(final UUID accountId, @Nullable final DateTime upTo, final InternalCallContext context) throws EntitlementApiException {
+    private List<UUID> createBaseEntitlementsWithAddOns(final OperationType operationType,
+                                                        final UUID accountId,
+                                                        final Iterable<BaseEntitlementWithAddOnsSpecifier> originalBaseEntitlementWithAddOnsSpecifiers,
+                                                        final boolean renameCancelledBundleIfExist,
+                                                        final Iterable<PluginProperty> properties,
+                                                        final CallContext callContext) throws EntitlementApiException {
+        logCreateEntitlementsWithAOs(log, originalBaseEntitlementWithAddOnsSpecifiers);
+
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(operationType,
+                                                                               accountId,
+                                                                               null,
+                                                                               originalBaseEntitlementWithAddOnsSpecifiers,
+                                                                               null,
+                                                                               properties,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<List<UUID>> createBaseEntitlementsWithAddOns = new WithEntitlementPlugin<List<UUID>>() {
+            @Override
+            public List<UUID> doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+                final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+                final Catalog catalog;
+                try {
+                    catalog = catalogInternalApi.getFullCatalog(true, true, contextWithValidAccountRecordId);
+                } catch (final CatalogApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+
+                final Map<UUID, Optional<EventsStream>> eventsStreamForBaseSubscriptionPerBundle = new HashMap<UUID, Optional<EventsStream>>();
+                final Map<String, Optional<UUID>> bundleKeyToIdMapping = new HashMap<String, Optional<UUID>>();
+                final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiersAfterPlugins = updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers();
+                final Collection<SubscriptionBaseWithAddOnsSpecifier> subscriptionBaseWithAddOnsSpecifiers = new LinkedList<SubscriptionBaseWithAddOnsSpecifier>();
+                DateTime upTo = null;
+                for (final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier : baseEntitlementWithAddOnsSpecifiersAfterPlugins) {
+                    // Entitlement
+                    final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(),
+                                                                                                       updatedPluginContext.getCreatedDate(),
+                                                                                                       contextWithValidAccountRecordId);
+                    upTo = upTo == null || upTo.compareTo(entitlementRequestedDate) < 0 ? entitlementRequestedDate : upTo;
+
+                    // Verify if the operation is valid for that bundle
+                    preCheckAddEntitlement(baseEntitlementWithAddOnsSpecifier,
+                                           entitlementRequestedDate,
+                                           eventsStreamForBaseSubscriptionPerBundle,
+                                           bundleKeyToIdMapping,
+                                           catalog,
+                                           callContext,
+                                           contextWithValidAccountRecordId);
+
+                    final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier = new SubscriptionBaseWithAddOnsSpecifier(baseEntitlementWithAddOnsSpecifier.getBundleId(),
+                                                                                                                                            baseEntitlementWithAddOnsSpecifier.getExternalKey(),
+                                                                                                                                            baseEntitlementWithAddOnsSpecifier.getEntitlementSpecifier(),
+                                                                                                                                            baseEntitlementWithAddOnsSpecifier.getBillingEffectiveDate(),
+                                                                                                                                            baseEntitlementWithAddOnsSpecifier.isMigrated());
+                    subscriptionBaseWithAddOnsSpecifiers.add(subscriptionBaseWithAddOnsSpecifier);
+                }
+
+                // Verify if operation is allowed by looking for is_block_change on Account
+                // Note that to fully check for block_change we should also look for BlockingState at the BUNDLE/SUBSCRIPTION level in case some of the input contain a BP that already exists.
+                checkForAccountBlockingChange(accountId, upTo, contextWithValidAccountRecordId);
+
+                final List<SubscriptionBaseWithAddOns> subscriptionsWithAddOns;
+                try {
+                    subscriptionsWithAddOns = subscriptionBaseInternalApi.createBaseSubscriptionsWithAddOns(subscriptionBaseWithAddOnsSpecifiers,
+                                                                                                            renameCancelledBundleIfExist,
+                                                                                                            contextWithValidAccountRecordId);
+                } catch (final SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+
+                return createEntitlementEvents(baseEntitlementWithAddOnsSpecifiersAfterPlugins, subscriptionsWithAddOns, updatedPluginContext, contextWithValidAccountRecordId);
+            }
+        };
+        return pluginExecution.executeWithPlugin(createBaseEntitlementsWithAddOns, pluginContext);
+    }
 
+    private BaseEntitlementWithAddOnsSpecifier getFirstBaseEntitlementWithAddOnsSpecifier(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers) throws SubscriptionBaseApiException {
+        if (baseEntitlementWithAddOnsSpecifiers == null) {
+            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+        }
+
+        final Iterator<BaseEntitlementWithAddOnsSpecifier> iterator = baseEntitlementWithAddOnsSpecifiers.iterator();
+        if (!iterator.hasNext()) {
+            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+        }
+
+        return iterator.next();
+    }
+
+    private void preCheckAddEntitlement(final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier,
+                                        final DateTime entitlementRequestedDate,
+                                        final Map<UUID, Optional<EventsStream>> eventsStreamForBaseSubscriptionPerBundle,
+                                        final Map<String, Optional<UUID>> bundleKeyToIdMapping,
+                                        final Catalog catalog,
+                                        final TenantContext callContext,
+                                        final InternalCallContext contextWithValidAccountRecordId) throws EntitlementApiException {
+        // In the addEntitlement codepath, bundleId is always set. But, technically, an existing bundle could be specified by externalKey in
+        // the createBaseEntitlementsWithAddOns codepath. In that case, we should also check if that bundle is blocked.
+        UUID bundleId = baseEntitlementWithAddOnsSpecifier.getBundleId();
+        if (bundleId == null && baseEntitlementWithAddOnsSpecifier.getExternalKey() != null) {
+            populateBundleKeyToIdMappingCache(baseEntitlementWithAddOnsSpecifier, bundleKeyToIdMapping, catalog, contextWithValidAccountRecordId);
+
+            final Optional<UUID> bundleIdForKey = bundleKeyToIdMapping.get(baseEntitlementWithAddOnsSpecifier.getExternalKey());
+            if (bundleIdForKey.isPresent()) {
+                bundleId = bundleIdForKey.get();
+            }
+        }
+
+        if (bundleId == null) {
+            return;
+        }
+
+        populateEventsStreamForBaseSubscriptionPerBundleCache(bundleId, eventsStreamForBaseSubscriptionPerBundle, callContext, contextWithValidAccountRecordId);
+
+        final Optional<EventsStream> eventsStreamForBaseSubscription = eventsStreamForBaseSubscriptionPerBundle.get(bundleId);
+        if (eventsStreamForBaseSubscription.isPresent()) {
+            preCheckAddEntitlement(bundleId, entitlementRequestedDate, baseEntitlementWithAddOnsSpecifier, eventsStreamForBaseSubscription.get());
+        }
+    }
+
+    private void populateBundleKeyToIdMappingCache(final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier, final Map<String, Optional<UUID>> bundleKeyToIdMapping, final Catalog catalog, final InternalCallContext contextWithValidAccountRecordId) throws EntitlementApiException {
+        if (bundleKeyToIdMapping.get(baseEntitlementWithAddOnsSpecifier.getExternalKey()) == null) {
+            final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getActiveBundleForKey(baseEntitlementWithAddOnsSpecifier.getExternalKey(), catalog, contextWithValidAccountRecordId);
+            if (bundle != null) {
+                bundleKeyToIdMapping.put(baseEntitlementWithAddOnsSpecifier.getExternalKey(), Optional.<UUID>of(bundle.getId()));
+            } else {
+                bundleKeyToIdMapping.put(baseEntitlementWithAddOnsSpecifier.getExternalKey(), Optional.<UUID>absent());
+            }
+        }
+    }
+
+    private void populateEventsStreamForBaseSubscriptionPerBundleCache(final UUID bundleId, final Map<UUID, Optional<EventsStream>> eventsStreamForBaseSubscriptionPerBundle, final TenantContext callContext, final InternalCallContext contextWithValidAccountRecordId) throws EntitlementApiException {
+        if (eventsStreamForBaseSubscriptionPerBundle.get(bundleId) == null) {
+            final List<SubscriptionBase> subscriptionsByBundle;
+            try {
+                subscriptionsByBundle = subscriptionBaseInternalApi.getSubscriptionsForBundle(bundleId, null, contextWithValidAccountRecordId);
+
+                if (subscriptionsByBundle == null || subscriptionsByBundle.isEmpty()) {
+                    throw new EntitlementApiException(ErrorCode.SUB_NO_ACTIVE_SUBSCRIPTIONS, bundleId);
+                }
+            } catch (final SubscriptionBaseApiException e) {
+                throw new EntitlementApiException(e);
+            }
+
+            final boolean isStandalone = Iterables.any(subscriptionsByBundle,
+                                                       new Predicate<SubscriptionBase>() {
+                                                           @Override
+                                                           public boolean apply(final SubscriptionBase input) {
+                                                               return ProductCategory.STANDALONE.equals(input.getCategory());
+                                                           }
+                                                       });
+
+            if (!isStandalone) {
+                final EventsStream eventsStreamForBaseSubscription = eventsStreamBuilder.buildForBaseSubscription(bundleId, callContext);
+                eventsStreamForBaseSubscriptionPerBundle.put(bundleId, Optional.<EventsStream>of(eventsStreamForBaseSubscription));
+            } else {
+                eventsStreamForBaseSubscriptionPerBundle.put(bundleId, Optional.<EventsStream>absent());
+            }
+        }
+    }
+
+    private void preCheckAddEntitlement(final UUID bundleId, final DateTime entitlementRequestedDate, final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier, final EventsStream eventsStreamForBaseSubscription) throws EntitlementApiException {
+        if (eventsStreamForBaseSubscription.isEntitlementCancelled() ||
+            (eventsStreamForBaseSubscription.isEntitlementPending() &&
+             (baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate() == null ||
+              baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate().compareTo(eventsStreamForBaseSubscription.getEntitlementEffectiveStartDate()) < 0))) {
+            throw new EntitlementApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
+        }
+
+        // Check the base entitlement state is not blocked
+        if (eventsStreamForBaseSubscription.isBlockChange(entitlementRequestedDate)) {
+            throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
+        } else if (eventsStreamForBaseSubscription.isBlockEntitlement(entitlementRequestedDate)) {
+            throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_ENTITLEMENT, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
+        }
+    }
+
+    private void checkForAccountBlockingChange(final UUID accountId, @Nullable final DateTime upTo, final InternalCallContext context) throws EntitlementApiException {
         try {
             final BlockingAggregator blockingAggregator = checker.getBlockedStatus(accountId, BlockingStateType.ACCOUNT, upTo, context);
             if (blockingAggregator.isBlockChange()) {
@@ -517,4 +559,31 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
             throw new EntitlementApiException(e);
         }
     }
+
+    private List<UUID> createEntitlementEvents(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiersAfterPlugins,
+                                               final List<SubscriptionBaseWithAddOns> subscriptionsWithAddOns,
+                                               final CallContext updatedPluginContext,
+                                               final InternalCallContext contextWithValidAccountRecordId) throws EntitlementApiException {
+        final List<UUID> createdSubscriptionIds = new LinkedList<UUID>();
+        final Map<BlockingState, UUID> blockingStateMap = new HashMap<BlockingState, UUID>();
+        int i = 0;
+        for (final Iterator<BaseEntitlementWithAddOnsSpecifier> it = baseEntitlementWithAddOnsSpecifiersAfterPlugins.iterator(); it.hasNext(); i++) {
+            final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = it.next();
+            for (final SubscriptionBase subscriptionBase : subscriptionsWithAddOns.get(i).getSubscriptionBaseList()) {
+                final BlockingState blockingState = new DefaultBlockingState(subscriptionBase.getId(),
+                                                                             BlockingStateType.SUBSCRIPTION,
+                                                                             DefaultEntitlementApi.ENT_STATE_START,
+                                                                             EntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                                             false,
+                                                                             false,
+                                                                             false,
+                                                                             dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), updatedPluginContext.getCreatedDate(), contextWithValidAccountRecordId));
+                blockingStateMap.put(blockingState, subscriptionsWithAddOns.get(i).getBundle().getId());
+
+                createdSubscriptionIds.add(subscriptionBase.getId());
+            }
+        }
+        entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(blockingStateMap, contextWithValidAccountRecordId);
+        return createdSubscriptionIds;
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
index dbf7876..0fc92b0 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
@@ -42,4 +42,16 @@ public class DefaultEntitlementSpecifier implements EntitlementSpecifier {
         return overrides;
     }
 
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultEntitlementSpecifier{");
+        sb.append("planName=").append(planPhaseSpecifier.getPlanName());
+        sb.append(", productName=").append(planPhaseSpecifier.getProductName());
+        sb.append(", billingPeriod=").append(planPhaseSpecifier.getBillingPeriod());
+        sb.append(", phaseType=").append(planPhaseSpecifier.getPhaseType());
+        sb.append(", priceListName=").append(planPhaseSpecifier.getPriceListName());
+        sb.append(", overrides=").append(overrides);
+        sb.append('}');
+        return sb.toString();
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
index 5ff2339..9c5a89b 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
@@ -226,6 +226,13 @@ public class DefaultEventsStream implements EventsStream {
     }
 
     @Override
+    public boolean isBlockEntitlement(final DateTime effectiveDate) {
+        Preconditions.checkState(effectiveDate != null);
+        final BlockingAggregator aggregator = getBlockingAggregator(effectiveDate);
+        return aggregator.isBlockEntitlement();
+    }
+
+    @Override
     public int getDefaultBillCycleDayLocal() {
         return defaultBillCycleDayLocal;
     }
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 10a2b1b..4734da9 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
@@ -295,8 +295,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                     }
                 } else if (subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey() != null &&
                            baseOrFirstStandalonePlanSpecifier == null) { // Skip the expensive checks if we are about to create the bundle (validation will be done in SubscriptionDao#createSubscriptionBundle)
-                    final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(), context);
-                    final SubscriptionBaseBundle tmp = getActiveBundleForKeyNotException(existingBundles, dao, clock, catalog, context);
+                    final SubscriptionBaseBundle tmp = getActiveBundleForKey(subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(), catalog, context);
                     if (tmp == null) {
                         throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey());
                     } else if (!tmp.getAccountId().equals(accountId)) {
@@ -474,7 +473,9 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
         return dao.getNonAOSubscriptionIdsForKey(bundleKey, context);
     }
 
-    public static SubscriptionBaseBundle getActiveBundleForKeyNotException(final Iterable<SubscriptionBaseBundle> existingBundles, final SubscriptionDao dao, final Clock clock, final Catalog catalog, final InternalTenantContext context) {
+    @Override
+    public SubscriptionBaseBundle getActiveBundleForKey(final String bundleKey, final Catalog catalog, final InternalTenantContext context) {
+        final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
         for (final SubscriptionBaseBundle cur : existingBundles) {
             final List<SubscriptionBase> subscriptions;
             try {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
index 2d73917..a075ae9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -35,6 +35,7 @@ import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.subscription.api.SubscriptionApiBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
 import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi;
 import org.killbill.billing.subscription.api.timeline.BundleBaseTimeline;
 import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException;
@@ -66,14 +67,16 @@ public class DefaultSubscriptionBaseTransferApi extends SubscriptionApiBase impl
 
     private final CatalogInternalApi catalogInternalApi;
     private final SubscriptionBaseTimelineApi timelineApi;
+    private final SubscriptionBaseInternalApi subscriptionBaseInternalApi;
     private final InternalCallContextFactory internalCallContextFactory;
 
     @Inject
     public DefaultSubscriptionBaseTransferApi(final Clock clock, final SubscriptionDao dao, final SubscriptionBaseTimelineApi timelineApi, final CatalogInternalApi catalogInternalApi,
-                                              final SubscriptionBaseApiService apiService, final InternalCallContextFactory internalCallContextFactory) {
+                                              final SubscriptionBaseInternalApi subscriptionBaseInternalApi, final SubscriptionBaseApiService apiService, final InternalCallContextFactory internalCallContextFactory) {
         super(dao, apiService, clock);
         this.catalogInternalApi = catalogInternalApi;
         this.timelineApi = timelineApi;
+        this.subscriptionBaseInternalApi = subscriptionBaseInternalApi;
         this.internalCallContextFactory = internalCallContextFactory;
     }
 
@@ -192,13 +195,11 @@ public class DefaultSubscriptionBaseTransferApi extends SubscriptionApiBase impl
                 throw new SubscriptionBaseTransferApiException(ErrorCode.SUB_TRANSFER_INVALID_EFF_DATE, effectiveTransferDate);
             }
 
-            final SubscriptionBaseBundle bundleForAccountAndKey = dao.getSubscriptionBundlesForAccountAndKey(sourceAccountId, bundleKey, fromInternalCallContext);
-            final SubscriptionBaseBundle bundle = DefaultSubscriptionInternalApi.getActiveBundleForKeyNotException(ImmutableList.of(bundleForAccountAndKey), dao, clock, catalog, fromInternalCallContext);
+            final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getActiveBundleForKey(bundleKey, catalog, fromInternalCallContext);
             if (bundle == null) {
                 throw new SubscriptionBaseTransferApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleKey);
             }
 
-
             // Get the bundle timeline for the old account
             final BundleBaseTimeline bundleBaseTimeline = timelineApi.getBundleTimeline(bundle, context);
 
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
index 95ce7e2..9c3818b 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
@@ -73,7 +73,7 @@ public class TestDefaultSubscriptionTransferApi extends SubscriptionTestSuiteNoD
         final CatalogInternalApi catalogInternalApiWithMockCatalogService = new DefaultCatalogInternalApi(catalogService, internalCallContextFactory);
         final SubscriptionBaseApiService apiService = Mockito.mock(SubscriptionBaseApiService.class);
         final SubscriptionBaseTimelineApi timelineApi = Mockito.mock(SubscriptionBaseTimelineApi.class);
-        transferApi = new DefaultSubscriptionBaseTransferApi(clock, dao, timelineApi, catalogInternalApiWithMockCatalogService, apiService, internalCallContextFactory);
+        transferApi = new DefaultSubscriptionBaseTransferApi(clock, dao, timelineApi, catalogInternalApiWithMockCatalogService, subscriptionInternalApi, apiService, internalCallContextFactory);
         // Overrride catalog with our Mock CatalogService
         this.catalog = catalogInternalApiWithMockCatalogService.getFullCatalog(true, true, internalCallContext);
     }