killbill-aplcache

Details

diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 6b4146b..6d0260b 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -106,6 +106,10 @@
         </dependency>
         <dependency>
             <groupId>org.kill-bill.billing</groupId>
+            <artifactId>killbill-platform-osgi-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.kill-bill.billing</groupId>
             <artifactId>killbill-platform-test</artifactId>
             <scope>test</scope>
         </dependency>
@@ -125,6 +129,10 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.kill-bill.billing.plugin</groupId>
+            <artifactId>killbill-plugin-api-entitlement</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.kill-bill.commons</groupId>
             <artifactId>killbill-clock</artifactId>
         </dependency>
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 3874218..6736e28 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
@@ -24,39 +24,36 @@ import javax.inject.Inject;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
-import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountInternalApi;
-import org.killbill.bus.api.PersistentBus;
-import org.killbill.bus.api.PersistentBus.EventBusException;
 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.clock.Clock;
 import org.killbill.billing.entitlement.AccountEventsStreams;
 import org.killbill.billing.entitlement.DefaultEntitlementService;
 import org.killbill.billing.entitlement.EntitlementService;
 import org.killbill.billing.entitlement.EntitlementTransitionType;
 import org.killbill.billing.entitlement.EventsStream;
-import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.block.BlockingChecker;
 import org.killbill.billing.entitlement.dao.BlockingStateDao;
 import org.killbill.billing.entitlement.engine.core.EntitlementNotificationKey;
 import org.killbill.billing.entitlement.engine.core.EntitlementNotificationKeyAction;
 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.EntitlementPluginApi;
+import org.killbill.billing.entitlement.plugin.api.EntitlementPluginApiException;
+import org.killbill.billing.entitlement.plugin.api.OnFailureEntitlementResult;
+import org.killbill.billing.entitlement.plugin.api.OnSuccessEntitlementResult;
+import org.killbill.billing.entitlement.plugin.api.OperationType;
+import org.killbill.billing.entitlement.plugin.api.PriorEntitlementResult;
 import org.killbill.billing.junction.DefaultBlockingState;
-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 org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
 import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
@@ -66,6 +63,15 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
@@ -93,13 +99,15 @@ public class DefaultEntitlementApi implements EntitlementApi {
     private final EventsStreamBuilder eventsStreamBuilder;
     private final EntitlementUtils entitlementUtils;
     private final NotificationQueueService notificationQueueService;
+    private final OSGIServiceRegistration<EntitlementPluginApi> pluginRegistry;
 
     @Inject
     public DefaultEntitlementApi(final PersistentBus eventBus, final InternalCallContextFactory internalCallContextFactory,
                                  final SubscriptionBaseTransferApi subscriptionTransferApi, final SubscriptionBaseInternalApi subscriptionInternalApi,
                                  final AccountInternalApi accountApi, final BlockingStateDao blockingStateDao, final Clock clock,
                                  final BlockingChecker checker, final NotificationQueueService notificationQueueService,
-                                 final EventsStreamBuilder eventsStreamBuilder, final EntitlementUtils entitlementUtils) {
+                                 final EventsStreamBuilder eventsStreamBuilder, final EntitlementUtils entitlementUtils,
+                                 final OSGIServiceRegistration<EntitlementPluginApi> pluginRegistry) {
         this.eventBus = eventBus;
         this.internalCallContextFactory = internalCallContextFactory;
         this.subscriptionBaseInternalApi = subscriptionInternalApi;
@@ -111,58 +119,119 @@ public class DefaultEntitlementApi implements EntitlementApi {
         this.notificationQueueService = notificationQueueService;
         this.eventsStreamBuilder = eventsStreamBuilder;
         this.entitlementUtils = entitlementUtils;
+        this.pluginRegistry = pluginRegistry;
         this.dateHelper = new EntitlementDateHelper(accountApi, clock);
     }
 
-    @Override
-    public Entitlement createBaseEntitlement(final UUID accountId, final PlanPhaseSpecifier planPhaseSpecifier, final String externalKey, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final CallContext callContext) throws EntitlementApiException {
-        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
-        try {
-
-            if (entitlementUtils.getFirstActiveSubscriptionIdForKeyOrNull(externalKey, contextWithValidAccountRecordId) != null) {
-                throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, externalKey));
-            }
-
-            final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.createBundleForAccount(accountId, externalKey, contextWithValidAccountRecordId);
+    public interface WithEntitlementPlugin<T> {
+        T doCall(final EntitlementApi entitlementApi) throws EntitlementApiException;
+    }
 
-            final DateTime referenceTime = clock.getUTCNow();
-            final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(effectiveDate, referenceTime, contextWithValidAccountRecordId);
-            final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundle.getId(), planPhaseSpecifier, overrides, requestedDate, contextWithValidAccountRecordId);
+    private <T> T executeWithPlugin(final WithEntitlementPlugin<T> callback, final EntitlementApi entitlementApi, final EntitlementContext pluginContext) throws EntitlementApiException {
 
-            return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, this,
-                                          blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
-                                          entitlementUtils, dateHelper, clock, internalCallContextFactory, callContext);
-        } catch (SubscriptionBaseApiException e) {
-            throw new EntitlementApiException(e);
+        try {
+            final PriorEntitlementResult priorEntitlementResult = executePluginPriorCalls(pluginContext);
+            if (priorEntitlementResult != null && priorEntitlementResult.isAborted()) {
+                throw new EntitlementApiException(ErrorCode.ENT_PLUGIN_API_ABORTED);
+            }
+            final EntitlementContext updatedPluginContext = new DefaultEntitlementContext(pluginContext, priorEntitlementResult);
+            try {
+                T result = callback.doCall(entitlementApi);
+                executePluginOnSuccessCalls(updatedPluginContext);
+                return result;
+            } catch (final EntitlementApiException e) {
+                executePluginOnFailureCalls(updatedPluginContext);
+                throw e;
+            }
+        } catch (final EntitlementPluginApiException e) {
+            throw new EntitlementApiException(ErrorCode.ENT_PLUGIN_API_ABORTED, e.getMessage());
         }
     }
 
     @Override
-    public Entitlement addEntitlement(final UUID bundleId, final PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final CallContext callContext) throws EntitlementApiException {
-        final EventsStream eventsStreamForBaseSubscription = eventsStreamBuilder.buildForBaseSubscription(bundleId, callContext);
-
-        // Check the base entitlement state is active
-        if (!eventsStreamForBaseSubscription.isEntitlementActive()) {
-            throw new EntitlementApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
-        }
-
-        // Check the base entitlement state is not blocked
-        if (eventsStreamForBaseSubscription.isBlockChange()) {
-            throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
-        }
+    public Entitlement createBaseEntitlement(final UUID accountId, final PlanPhaseSpecifier planPhaseSpecifier, final String externalKey, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final CallContext callContext) throws EntitlementApiException {
 
-        final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(effectiveDate, eventsStreamForBaseSubscription.getSubscriptionBase().getStartDate(), eventsStreamForBaseSubscription.getInternalTenantContext());
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
+                                                                               accountId,
+                                                                               null,
+                                                                               planPhaseSpecifier,
+                                                                               externalKey,
+                                                                               overrides,
+                                                                               effectiveDate,
+                                                                               null,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<Entitlement> createBaseEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
+            @Override
+            public Entitlement doCall(final EntitlementApi entitlementApi) throws EntitlementApiException {
+                final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+                try {
+
+                    if (entitlementUtils.getFirstActiveSubscriptionIdForKeyOrNull(externalKey, contextWithValidAccountRecordId) != null) {
+                        throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, externalKey));
+                    }
+
+
+                    final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.createBundleForAccount(accountId, externalKey, contextWithValidAccountRecordId);
+
+                    final DateTime referenceTime = clock.getUTCNow();
+                    final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(effectiveDate, referenceTime, contextWithValidAccountRecordId);
+                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundle.getId(), planPhaseSpecifier, overrides, requestedDate, contextWithValidAccountRecordId);
+
+                    return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, entitlementApi,
+                                                  blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
+                                                  entitlementUtils, dateHelper, clock, internalCallContextFactory, callContext);
+                } catch (SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+            }
+        };
+        return executeWithPlugin(createBaseEntitlementWithPlugin, this, pluginContext);
+    }
 
-        try {
-            final InternalCallContext context = internalCallContextFactory.createInternalCallContext(callContext);
-            final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundleId, planPhaseSpecifier, overrides, requestedDate, context);
+    @Override
+    public Entitlement addEntitlement(final UUID bundleId, final PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final CallContext callContext) throws EntitlementApiException {
 
-            return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, this,
-                                          blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
-                                          entitlementUtils, dateHelper, clock, internalCallContextFactory, callContext);
-        } catch (SubscriptionBaseApiException e) {
-            throw new EntitlementApiException(e);
-        }
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
+                                                                               null,
+                                                                               bundleId,
+                                                                               planPhaseSpecifier,
+                                                                               null,
+                                                                               overrides,
+                                                                               effectiveDate,
+                                                                               null,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<Entitlement> addEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
+            @Override
+            public Entitlement doCall(final EntitlementApi entitlementApi) throws EntitlementApiException {
+                final EventsStream eventsStreamForBaseSubscription = eventsStreamBuilder.buildForBaseSubscription(bundleId, callContext);
+
+                // Check the base entitlement state is active
+                if (!eventsStreamForBaseSubscription.isEntitlementActive()) {
+                    throw new EntitlementApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
+                }
+
+                // Check the base entitlement state is not blocked
+                if (eventsStreamForBaseSubscription.isBlockChange()) {
+                    throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
+                }
+
+                final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(effectiveDate, eventsStreamForBaseSubscription.getSubscriptionBase().getStartDate(), eventsStreamForBaseSubscription.getInternalTenantContext());
+
+                try {
+                    final InternalCallContext context = internalCallContextFactory.createInternalCallContext(callContext);
+                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundleId, planPhaseSpecifier, overrides, requestedDate, context);
+
+                    return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, entitlementApi,
+                                                  blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
+                                                  entitlementUtils, dateHelper, clock, internalCallContextFactory, callContext);
+                } catch (SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+            }
+        };
+        return executeWithPlugin(addEntitlementWithPlugin, this, pluginContext);
     }
 
     @Override
@@ -239,89 +308,124 @@ public class DefaultEntitlementApi implements EntitlementApi {
 
     @Override
     public void pause(final UUID bundleId, final LocalDate localEffectiveDate, final CallContext context) throws EntitlementApiException {
-        try {
-            final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
-            final BlockingState currentState = blockingStateDao.getBlockingStateForService(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, EntitlementService.ENTITLEMENT_SERVICE_NAME, contextWithValidAccountRecordId);
-            if (currentState != null && currentState.getStateName().equals(ENT_STATE_BLOCKED)) {
-                throw new EntitlementApiException(ErrorCode.ENT_ALREADY_BLOCKED, bundleId);
-            }
-
-            final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, contextWithValidAccountRecordId);
-            final Account account = accountApi.getAccountById(bundle.getAccountId(), contextWithValidAccountRecordId);
-            final SubscriptionBase baseSubscription = subscriptionBaseInternalApi.getBaseSubscription(bundleId, contextWithValidAccountRecordId);
-            final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(localEffectiveDate, baseSubscription.getStartDate(), contextWithValidAccountRecordId);
 
-            if (!dateHelper.isBeforeOrEqualsToday(effectiveDate, account.getTimeZone())) {
-                recordPauseResumeNotificationEntry(baseSubscription.getId(), bundleId, effectiveDate, true, contextWithValidAccountRecordId);
-                return;
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.PAUSE_SUBSCRIPTION,
+                                                                               null,
+                                                                               bundleId,
+                                                                               null,
+                                                                               null,
+                                                                               null,
+                                                                               localEffectiveDate,
+                                                                               null,
+                                                                               context);
+
+        final WithEntitlementPlugin<Void> pauseWithPlugin = new WithEntitlementPlugin<Void>() {
+            @Override
+            public Void doCall(final EntitlementApi entitlementApi) throws EntitlementApiException {
+                try {
+                    final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
+                    final BlockingState currentState = blockingStateDao.getBlockingStateForService(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, EntitlementService.ENTITLEMENT_SERVICE_NAME, contextWithValidAccountRecordId);
+                    if (currentState != null && currentState.getStateName().equals(ENT_STATE_BLOCKED)) {
+                        throw new EntitlementApiException(ErrorCode.ENT_ALREADY_BLOCKED, bundleId);
+                    }
+
+                    final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, contextWithValidAccountRecordId);
+                    final Account account = accountApi.getAccountById(bundle.getAccountId(), contextWithValidAccountRecordId);
+                    final SubscriptionBase baseSubscription = subscriptionBaseInternalApi.getBaseSubscription(bundleId, contextWithValidAccountRecordId);
+                    final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(localEffectiveDate, baseSubscription.getStartDate(), contextWithValidAccountRecordId);
+
+                    if (!dateHelper.isBeforeOrEqualsToday(effectiveDate, account.getTimeZone())) {
+                        recordPauseResumeNotificationEntry(baseSubscription.getId(), bundleId, effectiveDate, true, contextWithValidAccountRecordId);
+                        return null;
+                    }
+
+                    final BlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_BLOCKED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, true, effectiveDate);
+                    entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, contextWithValidAccountRecordId);
+
+                    // Should we send one event per entitlement in the bundle?
+                    // Code below only sends one event for the bundle and use the base entitlementId
+                    final DefaultEffectiveEntitlementEvent event = new DefaultEffectiveEntitlementEvent(state.getId(), baseSubscription.getId(), bundleId, bundle.getAccountId(), EntitlementTransitionType.BLOCK_BUNDLE,
+                                                                                                        effectiveDate, clock.getUTCNow(),
+                                                                                                        contextWithValidAccountRecordId.getAccountRecordId(), contextWithValidAccountRecordId.getTenantRecordId(),
+                                                                                                        contextWithValidAccountRecordId.getUserToken());
+
+                    try {
+                        eventBus.post(event);
+                    } catch (EventBusException e) {
+                        log.warn("Failed to post bus event for pause operation on bundle " + bundleId);
+                    }
+
+                } catch (SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                } catch (AccountApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+                return null;
             }
-
-            final BlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_BLOCKED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, true, effectiveDate);
-            entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, contextWithValidAccountRecordId);
-
-            // Should we send one event per entitlement in the bundle?
-            // Code below only sends one event for the bundle and use the base entitlementId
-            final DefaultEffectiveEntitlementEvent event = new DefaultEffectiveEntitlementEvent(state.getId(), baseSubscription.getId(), bundleId, bundle.getAccountId(), EntitlementTransitionType.BLOCK_BUNDLE,
-                                                                                                effectiveDate, clock.getUTCNow(),
-                                                                                                contextWithValidAccountRecordId.getAccountRecordId(), contextWithValidAccountRecordId.getTenantRecordId(),
-                                                                                                contextWithValidAccountRecordId.getUserToken());
-
-            try {
-                eventBus.post(event);
-            } catch (EventBusException e) {
-                log.warn("Failed to post bus event for pause operation on bundle " + bundleId);
-            }
-
-        } catch (SubscriptionBaseApiException e) {
-            throw new EntitlementApiException(e);
-        } catch (AccountApiException e) {
-            throw new EntitlementApiException(e);
-        }
+        };
+        executeWithPlugin(pauseWithPlugin, this, pluginContext);
     }
 
     @Override
     public void resume(final UUID bundleId, final LocalDate localEffectiveDate, final CallContext context) throws EntitlementApiException {
-        try {
-            final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
-            final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, contextWithValidAccountRecordId);
-            final Account account = accountApi.getAccountById(bundle.getAccountId(), contextWithValidAccountRecordId);
-            final SubscriptionBase baseSubscription = subscriptionBaseInternalApi.getBaseSubscription(bundleId, contextWithValidAccountRecordId);
-
-            final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(localEffectiveDate, baseSubscription.getStartDate(), contextWithValidAccountRecordId);
 
-            if (!dateHelper.isBeforeOrEqualsToday(effectiveDate, account.getTimeZone())) {
-                recordPauseResumeNotificationEntry(baseSubscription.getId(), bundleId, effectiveDate, false, contextWithValidAccountRecordId);
-                return;
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.RESUME_SUBSCRIPTION,
+                                                                               null,
+                                                                               bundleId,
+                                                                               null,
+                                                                               null,
+                                                                               null,
+                                                                               localEffectiveDate,
+                                                                               null,
+                                                                               context);
+        final WithEntitlementPlugin<Void> resumeWithPlugin = new WithEntitlementPlugin<Void>() {
+            @Override
+            public Void doCall(final EntitlementApi entitlementApi) throws EntitlementApiException {
+                try {
+                    final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
+                    final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, contextWithValidAccountRecordId);
+                    final Account account = accountApi.getAccountById(bundle.getAccountId(), contextWithValidAccountRecordId);
+                    final SubscriptionBase baseSubscription = subscriptionBaseInternalApi.getBaseSubscription(bundleId, contextWithValidAccountRecordId);
+
+                    final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(localEffectiveDate, baseSubscription.getStartDate(), contextWithValidAccountRecordId);
+
+                    if (!dateHelper.isBeforeOrEqualsToday(effectiveDate, account.getTimeZone())) {
+                        recordPauseResumeNotificationEntry(baseSubscription.getId(), bundleId, effectiveDate, false, contextWithValidAccountRecordId);
+                        return null;
+                    }
+
+                    final BlockingState currentState = blockingStateDao.getBlockingStateForService(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, EntitlementService.ENTITLEMENT_SERVICE_NAME, contextWithValidAccountRecordId);
+                    if (currentState == null || currentState.getStateName().equals(ENT_STATE_CLEAR)) {
+                        // Nothing to do.
+                        log.warn("Current state is {}, nothing to resume", currentState);
+                        return null;
+                    }
+
+                    final BlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_CLEAR, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, effectiveDate);
+                    entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, contextWithValidAccountRecordId);
+
+                    // Should we send one event per entitlement in the bundle?
+                    // Code below only sends one event for the bundle and use the base entitlementId
+                    final DefaultEffectiveEntitlementEvent event = new DefaultEffectiveEntitlementEvent(state.getId(), baseSubscription.getId(), bundleId, bundle.getAccountId(), EntitlementTransitionType.UNBLOCK_BUNDLE,
+                                                                                                        effectiveDate, clock.getUTCNow(),
+                                                                                                        contextWithValidAccountRecordId.getAccountRecordId(), contextWithValidAccountRecordId.getTenantRecordId(),
+                                                                                                        contextWithValidAccountRecordId.getUserToken());
+
+                    try {
+                        eventBus.post(event);
+                    } catch (EventBusException e) {
+                        log.warn("Failed to post bus event for resume operation on bundle " + bundleId);
+                    }
+
+                } catch (SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                } catch (AccountApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+                return null;
             }
-
-            final BlockingState currentState = blockingStateDao.getBlockingStateForService(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, EntitlementService.ENTITLEMENT_SERVICE_NAME, contextWithValidAccountRecordId);
-            if (currentState == null || currentState.getStateName().equals(ENT_STATE_CLEAR)) {
-                // Nothing to do.
-                log.warn("Current state is {}, nothing to resume", currentState);
-                return;
-            }
-
-            final BlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_CLEAR, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, effectiveDate);
-            entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, contextWithValidAccountRecordId);
-
-            // Should we send one event per entitlement in the bundle?
-            // Code below only sends one event for the bundle and use the base entitlementId
-            final DefaultEffectiveEntitlementEvent event = new DefaultEffectiveEntitlementEvent(state.getId(), baseSubscription.getId(), bundleId, bundle.getAccountId(), EntitlementTransitionType.UNBLOCK_BUNDLE,
-                                                                                                effectiveDate, clock.getUTCNow(),
-                                                                                                contextWithValidAccountRecordId.getAccountRecordId(), contextWithValidAccountRecordId.getTenantRecordId(),
-                                                                                                contextWithValidAccountRecordId.getUserToken());
-
-            try {
-                eventBus.post(event);
-            } catch (EventBusException e) {
-                log.warn("Failed to post bus event for resume operation on bundle " + bundleId);
-            }
-
-        } catch (SubscriptionBaseApiException e) {
-            throw new EntitlementApiException(e);
-        } catch (AccountApiException e) {
-            throw new EntitlementApiException(e);
-        }
+        };
+        executeWithPlugin(resumeWithPlugin, this, pluginContext);
     }
 
     @Override
@@ -352,7 +456,7 @@ public class DefaultEntitlementApi implements EntitlementApi {
             final SubscriptionBaseBundle baseBundle = baseSubscription != null ?
                                                       subscriptionBaseInternalApi.getBundleFromId(baseSubscription.getBundleId(), contextWithValidAccountRecordId) : null;
 
-            if (baseBundle == null || ! baseBundle.getAccountId().equals(sourceAccountId)) {
+            if (baseBundle == null || !baseBundle.getAccountId().equals(sourceAccountId)) {
                 throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_KEY, externalKey));
             }
 
@@ -391,4 +495,45 @@ public class DefaultEntitlementApi implements EntitlementApi {
         }
     }
 
+    private PriorEntitlementResult executePluginPriorCalls(final EntitlementContext entitlementContextArg) throws EntitlementPluginApiException {
+
+        // Return as soon as the first plugin aborts, or the last result for the last plugin
+        PriorEntitlementResult prevResult = null;
+
+        EntitlementContext currentContext = entitlementContextArg;
+        for (final String pluginName : pluginRegistry.getAllServices()) {
+            final EntitlementPluginApi plugin = pluginRegistry.getServiceForName(pluginName);
+            if (plugin == null) {
+                // First call to plugin, we log warn, if plugin is not registered
+                log.warn("Skipping unknown entitlement control plugin {} when fetching results", pluginName);
+                continue;
+            }
+            prevResult = plugin.priorCall(currentContext, currentContext.getPluginProperties());
+            if (prevResult.isAborted()) {
+                break;
+            }
+            currentContext = new DefaultEntitlementContext(currentContext, prevResult);
+        }
+        return prevResult;
+    }
+
+    private OnSuccessEntitlementResult executePluginOnSuccessCalls(final EntitlementContext context) throws EntitlementPluginApiException {
+        for (final String pluginName : pluginRegistry.getAllServices()) {
+            final EntitlementPluginApi plugin = pluginRegistry.getServiceForName(pluginName);
+            if (plugin != null) {
+                plugin.onSuccessCall(context, context.getPluginProperties());
+            }
+        }
+        return null;
+    }
+
+    private OnFailureEntitlementResult executePluginOnFailureCalls(final EntitlementContext context) throws EntitlementPluginApiException {
+        for (final String pluginName : pluginRegistry.getAllServices()) {
+            final EntitlementPluginApi plugin = pluginRegistry.getServiceForName(pluginName);
+            if (plugin != null) {
+                plugin.onFailureCall(context, context.getPluginProperties());
+            }
+        }
+        return null;
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
new file mode 100644
index 0000000..eb7c293
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.entitlement.plugin.api.EntitlementContext;
+import org.killbill.billing.entitlement.plugin.api.OperationType;
+import org.killbill.billing.entitlement.plugin.api.PriorEntitlementResult;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.UserType;
+
+import com.google.common.base.MoreObjects;
+
+public class DefaultEntitlementContext implements EntitlementContext {
+
+    private final OperationType operationType;
+    private final UUID accountId;
+    private final UUID bundleId;
+    private final PlanPhaseSpecifier spec;
+    private final String externalKey;
+    private final List<PlanPhasePriceOverride> planPhasePriceOverrides;
+    private final LocalDate effectiveDate;
+    private final Iterable<PluginProperty> pluginProperties;
+    private final UUID userToken;
+    private final String userName;
+    private final CallOrigin callOrigin;
+    private final UserType userType;
+    private final String reasonCode;
+    private final String comments;
+    private final DateTime createdDate;
+    private final DateTime updatedDate;
+    private final UUID tenantId;
+
+
+    public DefaultEntitlementContext(final EntitlementContext prev,
+                                     @Nullable final PriorEntitlementResult pluginResult) {
+        this(prev.getOperationType(),
+             prev.getAccountId(),
+             prev.getBundleId(),
+             pluginResult != null && pluginResult.getAdjustedPlanPhaseSpecifier() != null ? pluginResult.getAdjustedPlanPhaseSpecifier() : prev.getPlanPhaseSpecifier(),
+             prev.getExternalKey(),
+             pluginResult != null && pluginResult.getAdjustedPlanPhasePriceOverride() != null ? pluginResult.getAdjustedPlanPhasePriceOverride() : prev.getPlanPhasePriceOverride(),
+             pluginResult != null && pluginResult.getAdjustedEffectiveDate() != null ? pluginResult.getAdjustedEffectiveDate() : prev.getEffectiveDate(),
+             pluginResult != null && pluginResult.getAdjustedPluginProperties() != null ? pluginResult.getAdjustedPluginProperties() : prev.getPluginProperties(),
+             prev);
+    }
+
+    public DefaultEntitlementContext(final OperationType operationType,
+                                     final UUID accountId,
+                                     final UUID bundleId,
+                                     final PlanPhaseSpecifier spec,
+                                     final String externalKey,
+                                     final List<PlanPhasePriceOverride> planPhasePriceOverrides,
+                                     final LocalDate effectiveDate,
+                                     final Iterable<PluginProperty> pluginProperties,
+                                     final CallContext callContext) {
+        this(operationType, accountId, bundleId, spec, externalKey, planPhasePriceOverrides, effectiveDate, pluginProperties,
+             callContext.getUserToken(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(),
+             callContext.getComments(), callContext.getCreatedDate(), callContext.getUpdatedDate(), callContext.getTenantId());
+    }
+
+
+    public DefaultEntitlementContext(final OperationType operationType,
+                                     final UUID accountId,
+                                     final UUID bundleId,
+                                     final PlanPhaseSpecifier spec,
+                                     final String externalKey,
+                                     final List<PlanPhasePriceOverride> planPhasePriceOverrides,
+                                     final LocalDate effectiveDate,
+                                     final Iterable<PluginProperty> pluginProperties,
+                                     final UUID userToken,
+                                     final String userName,
+                                     final CallOrigin callOrigin,
+                                     final UserType userType,
+                                     final String reasonCode,
+                                     final String comments,
+                                     final DateTime createdDate,
+                                     final DateTime updatedDate,
+                                     final UUID tenantId) {
+        this.operationType = operationType;
+        this.accountId = accountId;
+        this.bundleId = bundleId;
+        this.spec = spec;
+        this.externalKey = externalKey;
+        this.planPhasePriceOverrides = planPhasePriceOverrides;
+        this.effectiveDate = effectiveDate;
+        this.pluginProperties = pluginProperties;
+        this.userToken = userToken;
+        this.userName = userName;
+        this.callOrigin = callOrigin;
+        this.userType = userType;
+        this.reasonCode = reasonCode;
+        this.comments = comments;
+        this.createdDate = createdDate;
+        this.updatedDate = updatedDate;
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public OperationType getOperationType() {
+        return operationType;
+    }
+
+    @Override
+    public UUID getAccountId() {
+        return accountId;
+    }
+
+    @Override
+    public UUID getBundleId() {
+        return bundleId;
+    }
+
+    @Override
+    public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+        return spec;
+    }
+
+    @Override
+    public String getExternalKey() {
+        return externalKey;
+    }
+
+    @Override
+    public List<PlanPhasePriceOverride> getPlanPhasePriceOverride() {
+        return planPhasePriceOverrides;
+    }
+
+    @Override
+    public LocalDate getEffectiveDate() {
+        return effectiveDate;
+    }
+
+    @Override
+    public Iterable<PluginProperty> getPluginProperties() {
+        return pluginProperties;
+    }
+
+    @Override
+    public UUID getUserToken() {
+        return userToken;
+    }
+
+    @Override
+    public String getUserName() {
+        return userName;
+    }
+
+    @Override
+    public CallOrigin getCallOrigin() {
+        return callOrigin;
+    }
+
+    @Override
+    public UserType getUserType() {
+        return userType;
+    }
+
+    @Override
+    public String getReasonCode() {
+        return reasonCode;
+    }
+
+    @Override
+    public String getComments() {
+        return comments;
+    }
+
+    @Override
+    public DateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    @Override
+    public DateTime getUpdatedDate() {
+        return updatedDate;
+    }
+
+    @Override
+    public UUID getTenantId() {
+        return tenantId;
+    }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java b/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java
index 420073f..852dbd6 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java
@@ -33,17 +33,25 @@ import org.killbill.billing.entitlement.dao.BlockingStateDao;
 import org.killbill.billing.entitlement.dao.ProxyBlockingStateDao;
 import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
 import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
+import org.killbill.billing.entitlement.plugin.api.EntitlementPluginApi;
 import org.killbill.billing.glue.EntitlementModule;
 import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.platform.api.KillbillConfigSource;
 import org.killbill.billing.util.glue.KillBillModule;
 
+import com.google.inject.TypeLiteral;
+
 public class DefaultEntitlementModule extends KillBillModule implements EntitlementModule {
 
     public DefaultEntitlementModule(final KillbillConfigSource configSource) {
         super(configSource);
     }
 
+    protected void installEntitlementPluginApi() {
+        bind(new TypeLiteral<OSGIServiceRegistration<EntitlementPluginApi>>() {}).toProvider(DefaultEntitlementProviderPluginRegistryProvider.class).asEagerSingleton();
+    }
+
     @Override
     protected void configure() {
         installBlockingStateDao();
@@ -55,6 +63,7 @@ public class DefaultEntitlementModule extends KillBillModule implements Entitlem
         bind(EntitlementService.class).to(DefaultEntitlementService.class).asEagerSingleton();
         bind(EntitlementUtils.class).asEagerSingleton();
         bind(EventsStreamBuilder.class).asEagerSingleton();
+        installEntitlementPluginApi();
     }
 
     @Override
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementProviderPluginRegistryProvider.java b/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementProviderPluginRegistryProvider.java
new file mode 100644
index 0000000..19a7e39
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementProviderPluginRegistryProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.glue;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.entitlement.plugin.api.EntitlementPluginApi;
+import org.killbill.billing.entitlement.provider.DefaultEntitlementProviderPluginRegistry;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+
+import com.google.inject.Provider;
+
+public class DefaultEntitlementProviderPluginRegistryProvider implements Provider<OSGIServiceRegistration<EntitlementPluginApi>> {
+
+    @Inject
+    public DefaultEntitlementProviderPluginRegistryProvider() {
+    }
+
+    @Override
+    public OSGIServiceRegistration<EntitlementPluginApi> get() {
+        final DefaultEntitlementProviderPluginRegistry pluginRegistry = new DefaultEntitlementProviderPluginRegistry();
+        return pluginRegistry;
+    }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java b/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java
new file mode 100644
index 0000000..54b1f19
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/provider/DefaultEntitlementProviderPluginRegistry.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.provider;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.entitlement.plugin.api.EntitlementPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultEntitlementProviderPluginRegistry implements OSGIServiceRegistration<EntitlementPluginApi> {
+
+    private final static Logger log = LoggerFactory.getLogger(DefaultEntitlementProviderPluginRegistry.class);
+
+    private final Map<String, EntitlementPluginApi> pluginsByName = new ConcurrentHashMap<String, EntitlementPluginApi>();
+
+    @Inject
+    public DefaultEntitlementProviderPluginRegistry() {
+    }
+
+    @Override
+    public void registerService(final OSGIServiceDescriptor desc, final EntitlementPluginApi service) {
+        log.info("DefaultEntitlementProviderPluginRegistry registering service " + desc.getRegistrationName());
+        pluginsByName.put(desc.getRegistrationName(), service);
+    }
+
+    @Override
+    public void unregisterService(final String serviceName) {
+        log.info("DefaultEntitlementProviderPluginRegistry unregistering service " + serviceName);
+        pluginsByName.remove(serviceName);
+    }
+
+    @Override
+    public EntitlementPluginApi getServiceForName(final String serviceName) {
+        if (serviceName == null) {
+            throw new IllegalArgumentException("Null entitlement plugin API name");
+        }
+        final EntitlementPluginApi plugin = pluginsByName.get(serviceName);
+        return plugin;
+    }
+
+    @Override
+    public Set<String> getAllServices() {
+        return pluginsByName.keySet();    }
+
+    @Override
+    public Class<EntitlementPluginApi> getServiceType() {
+        return EntitlementPluginApi.class;
+    }
+}

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 3d22ed7..9d12dd9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.23</version>
+        <version>0.24-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.15.1-SNAPSHOT</version>