killbill-memoizeit

Changes

pom.xml 2(+1 -1)

profiles/killbill/src/main/resources/killbill-server.properties.mos 88(+0 -88)

Details

diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java
new file mode 100644
index 0000000..cd916d9
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultApiBase.java
@@ -0,0 +1,116 @@
+/*
+ * 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.payment.api;
+
+import java.math.BigDecimal;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultApiBase {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultApiBase.class);
+
+    private final PaymentConfig paymentConfig;
+
+    public DefaultApiBase(final PaymentConfig paymentConfig) {
+        this.paymentConfig = paymentConfig;
+    }
+
+    protected void logAPICall(final String transactionType, final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey) {
+        if (log.isInfoEnabled()) {
+            final StringBuilder logLine = new StringBuilder();
+            logLine.append("PaymentApi : ")
+                   .append(transactionType)
+                   .append(", account = ")
+                   .append(account.getId());
+            if (paymentMethodId != null) {
+                logLine.append(", paymentMethodId = ")
+                       .append(paymentMethodId);
+            }
+            if (paymentExternalKey != null) {
+                logLine.append(", paymentExternalKey = ")
+                       .append(paymentExternalKey);
+            }
+            if (paymentTransactionExternalKey != null) {
+                logLine.append(", paymentTransactionExternalKey = ")
+                       .append(paymentTransactionExternalKey);
+            }
+            if (paymentId != null) {
+                logLine.append(", paymentId = ")
+                       .append(paymentId);
+            }
+            if (transactionId != null) {
+                logLine.append(", transactionId = ")
+                       .append(transactionId);
+            }
+            if (amount != null) {
+                logLine.append(", amount = ")
+                       .append(amount);
+            }
+            if (currency != null) {
+                logLine.append(", currency = ")
+                       .append(currency);
+            }
+            log.info(logLine.toString());
+        }
+    }
+
+    protected List<String> toPaymentControlPluginNames(final PaymentOptions paymentOptions) {
+        // Special path for JAX-RS InvoicePayment endpoints (see JaxRsResourceBase)
+        if (paymentConfig.getPaymentControlPluginNames() != null &&
+            paymentOptions.getPaymentControlPluginNames() != null &&
+            paymentOptions.getPaymentControlPluginNames().size() == 1 &&
+            InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentOptions.getPaymentControlPluginNames().get(0))) {
+            final List<String> paymentControlPluginNames = new LinkedList<String>(paymentOptions.getPaymentControlPluginNames());
+            paymentControlPluginNames.addAll(paymentConfig.getPaymentControlPluginNames());
+            return paymentControlPluginNames;
+        } else if (paymentOptions.getPaymentControlPluginNames() != null && !paymentOptions.getPaymentControlPluginNames().isEmpty()) {
+            return paymentOptions.getPaymentControlPluginNames();
+        } else if (paymentConfig.getPaymentControlPluginNames() != null && !paymentConfig.getPaymentControlPluginNames().isEmpty()) {
+            return paymentConfig.getPaymentControlPluginNames();
+        } else {
+            return ImmutableList.<String>of();
+        }
+    }
+
+    protected void checkNotNullParameter(final Object parameter, final String parameterName) throws PaymentApiException {
+        if (parameter == null) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, parameterName, "should not be null");
+        }
+    }
+
+    protected void checkPositiveAmount(final BigDecimal amount) throws PaymentApiException {
+        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "amount", "should be greater than 0");
+        }
+    }
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index 1e2d91d..3da498d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -44,7 +44,7 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.ImmutableList;
 
-public class DefaultPaymentApi implements PaymentApi {
+public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
 
     private static final boolean SHOULD_LOCK_ACCOUNT = true;
     private static final boolean IS_API_PAYMENT = true;
@@ -52,7 +52,6 @@ public class DefaultPaymentApi implements PaymentApi {
 
     private static final Logger log = LoggerFactory.getLogger(DefaultPaymentApi.class);
 
-    private final PaymentConfig paymentConfig;
     private final PaymentProcessor paymentProcessor;
     private final PaymentMethodProcessor paymentMethodProcessor;
     private final PluginControlPaymentProcessor pluginControlPaymentProcessor;
@@ -60,7 +59,7 @@ public class DefaultPaymentApi implements PaymentApi {
 
     @Inject
     public DefaultPaymentApi(final PaymentConfig paymentConfig, final PaymentProcessor paymentProcessor, final PaymentMethodProcessor paymentMethodProcessor, final PluginControlPaymentProcessor pluginControlPaymentProcessor, final InternalCallContextFactory internalCallContextFactory) {
-        this.paymentConfig = paymentConfig;
+        super(paymentConfig);
         this.paymentProcessor = paymentProcessor;
         this.paymentMethodProcessor = paymentMethodProcessor;
         this.pluginControlPaymentProcessor = pluginControlPaymentProcessor;
@@ -167,7 +166,7 @@ public class DefaultPaymentApi implements PaymentApi {
     }
 
     @Override
-    public Payment createPurchaseWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, final String paymentExternalKey, final String paymentTransactionExternalKey,
+    public Payment createPurchaseWithPaymentControl(final Account account, @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, @Nullable  final String paymentExternalKey, final String paymentTransactionExternalKey,
                                                     final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
         final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions);
         if (paymentControlPluginNames.isEmpty()) {
@@ -180,7 +179,6 @@ public class DefaultPaymentApi implements PaymentApi {
             checkPositiveAmount(amount);
             checkNotNullParameter(currency, "currency");
         }
-        checkNotNullParameter(paymentExternalKey, "paymentExternalKey");
         checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
         checkNotNullParameter(properties, "plugin properties");
 
@@ -498,73 +496,4 @@ public class DefaultPaymentApi implements PaymentApi {
 
         return paymentMethods;
     }
-
-    private void logAPICall(final String transactionType, final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey) {
-        if (log.isInfoEnabled()) {
-            final StringBuilder logLine = new StringBuilder();
-            logLine.append("PaymentApi : ")
-                   .append(transactionType)
-                   .append(", account = ")
-                   .append(account.getId());
-            if (paymentMethodId != null) {
-                logLine.append(", paymentMethodId = ")
-                       .append(paymentMethodId);
-            }
-            if (paymentExternalKey != null) {
-                logLine.append(", paymentExternalKey = ")
-                       .append(paymentExternalKey);
-            }
-            if (paymentTransactionExternalKey != null) {
-                logLine.append(", paymentTransactionExternalKey = ")
-                       .append(paymentTransactionExternalKey);
-            }
-            if (paymentId != null) {
-                logLine.append(", paymentId = ")
-                       .append(paymentId);
-            }
-            if (transactionId != null) {
-                logLine.append(", transactionId = ")
-                       .append(transactionId);
-            }
-            if (amount != null) {
-                logLine.append(", amount = ")
-                       .append(amount);
-            }
-            if (currency != null) {
-                logLine.append(", currency = ")
-                       .append(currency);
-            }
-            log.info(logLine.toString());
-        }
-    }
-
-    private List<String> toPaymentControlPluginNames(final PaymentOptions paymentOptions) {
-        // Special path for JAX-RS InvoicePayment endpoints (see JaxRsResourceBase)
-        if (paymentConfig.getPaymentControlPluginNames() != null &&
-            paymentOptions.getPaymentControlPluginNames() != null &&
-            paymentOptions.getPaymentControlPluginNames().size() == 1 &&
-            InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentOptions.getPaymentControlPluginNames().get(0))) {
-            final List<String> paymentControlPluginNames = new LinkedList<String>(paymentOptions.getPaymentControlPluginNames());
-            paymentControlPluginNames.addAll(paymentConfig.getPaymentControlPluginNames());
-            return paymentControlPluginNames;
-        } else if (paymentOptions.getPaymentControlPluginNames() != null && !paymentOptions.getPaymentControlPluginNames().isEmpty()) {
-            return paymentOptions.getPaymentControlPluginNames();
-        } else if (paymentConfig.getPaymentControlPluginNames() != null && !paymentConfig.getPaymentControlPluginNames().isEmpty()) {
-            return paymentConfig.getPaymentControlPluginNames();
-        } else {
-            return ImmutableList.<String>of();
-        }
-    }
-
-    private void checkNotNullParameter(final Object parameter, final String parameterName) throws PaymentApiException {
-        if (parameter == null) {
-            throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, parameterName, "should not be null");
-        }
-    }
-
-    private void checkPositiveAmount(final BigDecimal amount) throws PaymentApiException {
-        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
-            throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "amount", "should be greater than 0");
-        }
-    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
index c9adbaf..39117a5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
@@ -17,6 +17,7 @@
 
 package org.killbill.billing.payment.api;
 
+import java.util.List;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
@@ -24,25 +25,37 @@ import javax.inject.Inject;
 
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.control.plugin.api.HPPType;
+import org.killbill.billing.control.plugin.api.PaymentApiType;
+import org.killbill.billing.control.plugin.api.PaymentControlApiException;
+import org.killbill.billing.control.plugin.api.PriorPaymentControlResult;
 import org.killbill.billing.payment.core.PaymentGatewayProcessor;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.PaymentConfig;
 
-public class DefaultPaymentGatewayApi implements PaymentGatewayApi {
+public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentGatewayApi {
 
     private final PaymentGatewayProcessor paymentGatewayProcessor;
+    private final ControlPluginRunner controlPluginRunner;
     private final InternalCallContextFactory internalCallContextFactory;
 
     @Inject
-    public DefaultPaymentGatewayApi(final PaymentGatewayProcessor paymentGatewayProcessor, final InternalCallContextFactory internalCallContextFactory) {
+    public DefaultPaymentGatewayApi(final PaymentConfig paymentConfig,
+                                    final PaymentGatewayProcessor paymentGatewayProcessor,
+                                    final ControlPluginRunner controlPluginRunner,
+                                    final InternalCallContextFactory internalCallContextFactory) {
+        super(paymentConfig);
         this.paymentGatewayProcessor = paymentGatewayProcessor;
+        this.controlPluginRunner = controlPluginRunner;
         this.internalCallContextFactory = internalCallContextFactory;
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account, @Nullable final UUID paymentMethodId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentApiException {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account, final UUID paymentMethodId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentApiException {
         final UUID paymentMethodIdToUse = paymentMethodId != null ? paymentMethodId : account.getPaymentMethodId();
 
         if (paymentMethodId == null) {
@@ -53,8 +66,14 @@ public class DefaultPaymentGatewayApi implements PaymentGatewayApi {
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptorWithPaymentControl(final Account account, final UUID uuid, final Iterable<PluginProperty> iterable, final Iterable<PluginProperty> iterable1, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
-        throw new IllegalStateException("Not implemented");
+    public HostedPaymentPageFormDescriptor buildFormDescriptorWithPaymentControl(final Account account, final UUID paymentMethodId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
+
+        return executeWithPaymentControl(account, paymentMethodId, properties, paymentOptions, callContext, new WithPaymentControlCallback<HostedPaymentPageFormDescriptor>() {
+            @Override
+            public HostedPaymentPageFormDescriptor doPaymentGatewayApiOperation(final Iterable<PluginProperty> adjustedPluginProperties) throws PaymentApiException {
+                return buildFormDescriptor(account, paymentMethodId, customFields, adjustedPluginProperties, callContext);
+            }
+        });
     }
 
     @Override
@@ -63,7 +82,61 @@ public class DefaultPaymentGatewayApi implements PaymentGatewayApi {
     }
 
     @Override
-    public GatewayNotification processNotificationWithPaymentControl(final String s, final String s1, final Iterable<PluginProperty> iterable, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
-        throw new IllegalStateException("Not implemented");
+    public GatewayNotification processNotificationWithPaymentControl(final String notification, final String pluginName, final Iterable<PluginProperty> properties, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
+        return executeWithPaymentControl(null, null, properties, paymentOptions, callContext, new WithPaymentControlCallback<GatewayNotification>() {
+            @Override
+            public GatewayNotification doPaymentGatewayApiOperation(final Iterable<PluginProperty> adjustedPluginProperties) throws PaymentApiException {
+                return processNotification(notification, pluginName, adjustedPluginProperties, callContext);
+            }
+        });
+    }
+
+
+    private interface WithPaymentControlCallback<T> {
+        T doPaymentGatewayApiOperation(final Iterable<PluginProperty> adjustedPluginProperties) throws PaymentApiException;
+    }
+
+    private <T> T executeWithPaymentControl(@Nullable final Account account,
+                                            @Nullable final UUID paymentMethodId,
+                                            final Iterable<PluginProperty> properties,
+                                            final PaymentOptions paymentOptions,
+                                            final CallContext callContext,
+                                            final WithPaymentControlCallback<T> callback) throws PaymentApiException {
+
+        final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions);
+        if (paymentControlPluginNames.isEmpty()) {
+            return callback.doPaymentGatewayApiOperation(properties);
+        }
+
+        final PriorPaymentControlResult priorCallResult;
+        try {
+            priorCallResult = controlPluginRunner.executePluginPriorCalls(account,
+                                                                          paymentMethodId,
+                                                                          null, null, null, null,
+                                                                          PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
+                                                                          null, null, true, paymentControlPluginNames, properties, callContext);
+
+        } catch (final PaymentControlApiException e) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION, e);
+        }
+
+        try {
+            final T result = callback.doPaymentGatewayApiOperation(priorCallResult.getAdjustedPluginProperties());
+            controlPluginRunner.executePluginOnSuccessCalls(account,
+                                                            paymentMethodId,
+                                                            null, null, null, null, null,
+                                                            PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
+                                                            null, null, null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext);
+            return result;
+        } catch (final PaymentApiException e) {
+            controlPluginRunner.executePluginOnFailureCalls(account,
+                                                            paymentMethodId,
+                                                            null, null, null, null,
+                                                            PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
+                                                            null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext);
+
+            throw e;
+
+        }
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java
index 9634461..0459ad2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/AuthorizeControlOperation.java
@@ -27,8 +27,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class AuthorizeControlOperation extends OperationControlCallback {
 
-    public AuthorizeControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+    public AuthorizeControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java
index 2d325c4..2b76885 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CaptureControlOperation.java
@@ -27,8 +27,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class CaptureControlOperation extends OperationControlCallback {
 
-    public CaptureControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+    public CaptureControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java
index 3f47fdc..07b5344 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ChargebackControlOperation.java
@@ -28,8 +28,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class ChargebackControlOperation extends OperationControlCallback {
 
-    public ChargebackControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+    public ChargebackControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
index d6369e7..6f3e300 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
@@ -19,11 +19,13 @@ package org.killbill.billing.payment.core.sm.control;
 
 import org.killbill.automaton.OperationException;
 import org.killbill.automaton.OperationResult;
+import org.killbill.billing.control.plugin.api.PaymentApiType;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner.DefaultPaymentControlContext;
 import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
 import org.killbill.billing.payment.dispatcher.PluginDispatcher;
 import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType;
@@ -36,8 +38,11 @@ import org.killbill.commons.locker.GlobalLocker;
 //
 public class CompletionControlOperation extends OperationControlCallback {
 
-    public CompletionControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> retryPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, retryPluginRegistry);
+    public CompletionControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher,
+                                      final PaymentStateControlContext paymentStateContext,
+                                      final PaymentProcessor paymentProcessor,
+                                      final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
@@ -54,7 +59,9 @@ public class CompletionControlOperation extends OperationControlCallback {
                                                                                                             paymentStateContext.getPaymentExternalKey(),
                                                                                                             transaction.getId(),
                                                                                                             paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                                                            PaymentApiType.PAYMENT_TRANSACTION,
                                                                                                             paymentStateContext.getTransactionType(),
+                                                                                                            null,
                                                                                                             transaction.getAmount(),
                                                                                                             transaction.getCurrency(),
                                                                                                             transaction.getProcessedAmount(),
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
new file mode 100644
index 0000000..41c37a2
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
@@ -0,0 +1,380 @@
+/*
+ * 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.payment.core.sm.control;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.control.plugin.api.HPPType;
+import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
+import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult;
+import org.killbill.billing.control.plugin.api.PaymentApiType;
+import org.killbill.billing.control.plugin.api.PaymentControlApiException;
+import org.killbill.billing.control.plugin.api.PaymentControlContext;
+import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
+import org.killbill.billing.control.plugin.api.PriorPaymentControlResult;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.retry.DefaultFailureCallResult;
+import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
+import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ControlPluginRunner {
+
+    private static final Logger log = LoggerFactory.getLogger(ControlPluginRunner.class);
+
+    private final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry;
+
+    @Inject
+    public ControlPluginRunner(final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
+        this.paymentControlPluginRegistry = paymentControlPluginRegistry;
+    }
+
+    public PriorPaymentControlResult executePluginPriorCalls(final Account account,
+                                                             final UUID paymentMethodId,
+                                                             final UUID paymentAttemptId,
+                                                             final UUID paymentId,
+                                                             final String paymentExternalKey,
+                                                             final String paymentTransactionExternalKey,
+                                                             final PaymentApiType paymentApiType,
+                                                             final TransactionType transactionType,
+                                                             final HPPType hppType,
+                                                             final BigDecimal amount,
+                                                             final Currency currency,
+                                                             final boolean isApiPayment,
+                                                             final List<String> paymentControlPluginNames,
+                                                             final Iterable<PluginProperty> pluginProperties,
+                                                             final CallContext callContext) throws PaymentControlApiException {
+        // Return as soon as the first plugin aborts, or the last result for the last plugin
+        PriorPaymentControlResult prevResult = null;
+
+        // Those values are adjusted prior each call with the result of what previous call to plugin returned
+        Iterable<PluginProperty> inputPluginProperties = pluginProperties;
+        PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account,
+                                                                                            paymentMethodId,
+                                                                                            paymentAttemptId,
+                                                                                            paymentId,
+                                                                                            paymentExternalKey,
+                                                                                            paymentTransactionExternalKey,
+                                                                                            paymentApiType,
+                                                                                            transactionType,
+                                                                                            hppType,
+                                                                                            amount,
+                                                                                            currency,
+                                                                                            isApiPayment,
+                                                                                            callContext);
+
+        for (final String pluginName : paymentControlPluginNames) {
+            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+            if (plugin == null) {
+                // First call to plugin, we log warn, if plugin is not registered
+                log.warn("Skipping unknown payment control plugin {} when fetching results", pluginName);
+                continue;
+            }
+            prevResult = plugin.priorCall(inputPaymentControlContext, inputPluginProperties);
+            if (prevResult.getAdjustedPluginProperties() != null) {
+                inputPluginProperties = prevResult.getAdjustedPluginProperties();
+            }
+            if (prevResult.isAborted()) {
+                break;
+            }
+            inputPaymentControlContext = new DefaultPaymentControlContext(account,
+                                                                          prevResult.getAdjustedPaymentMethodId() != null ? prevResult.getAdjustedPaymentMethodId() : paymentMethodId,
+                                                                          paymentAttemptId,
+                                                                          paymentId,
+                                                                          paymentExternalKey,
+                                                                          paymentTransactionExternalKey,
+                                                                          paymentApiType,
+                                                                          transactionType,
+                                                                          hppType,
+                                                                          prevResult.getAdjustedAmount() != null ? prevResult.getAdjustedAmount() : amount,
+                                                                          prevResult.getAdjustedCurrency() != null ? prevResult.getAdjustedCurrency() : currency,
+                                                                          isApiPayment,
+                                                                          callContext);
+        }
+        // Rebuild latest result to include inputPluginProperties
+        prevResult = new DefaultPriorPaymentControlResult(prevResult, inputPluginProperties);
+        return prevResult;
+    }
+
+    public OnSuccessPaymentControlResult executePluginOnSuccessCalls(final Account account,
+                                                                     final UUID paymentMethodId,
+                                                                     final UUID paymentAttemptId,
+                                                                     final UUID paymentId,
+                                                                     final String paymentExternalKey,
+                                                                     final UUID transactionId,
+                                                                     final String paymentTransactionExternalKey,
+                                                                     final PaymentApiType paymentApiType,
+                                                                     final TransactionType transactionType,
+                                                                     final HPPType hppType,
+                                                                     final BigDecimal amount,
+                                                                     final Currency currency,
+                                                                     final BigDecimal processedAmount,
+                                                                     final Currency processedCurrency,
+                                                                     final boolean isApiPayment,
+                                                                     final List<String> paymentControlPluginNames,
+                                                                     final Iterable<PluginProperty> pluginProperties,
+                                                                     final CallContext callContext) {
+
+        final PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account,
+                                                                                                  paymentMethodId,
+                                                                                                  paymentAttemptId,
+                                                                                                  paymentId,
+                                                                                                  paymentExternalKey,
+                                                                                                  transactionId,
+                                                                                                  paymentTransactionExternalKey,
+                                                                                                  paymentApiType,
+                                                                                                  transactionType,
+                                                                                                  hppType,
+                                                                                                  amount,
+                                                                                                  currency,
+                                                                                                  processedAmount,
+                                                                                                  processedCurrency,
+                                                                                                  isApiPayment,
+                                                                                                  callContext);
+        Iterable<PluginProperty> inputPluginProperties = pluginProperties;
+        for (final String pluginName : paymentControlPluginNames) {
+            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+            if (plugin != null) {
+                try {
+                    final OnSuccessPaymentControlResult result = plugin.onSuccessCall(inputPaymentControlContext, inputPluginProperties);
+                    if (result.getAdjustedPluginProperties() != null) {
+                        inputPluginProperties = result.getAdjustedPluginProperties();
+                    }
+                    // Exceptions from the control plugins are ignored (and logged) because the semantics on what to do are undefined.
+                } catch (final PaymentControlApiException e) {
+                    log.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + inputPaymentControlContext.getPaymentExternalKey(), e);
+                } catch (final RuntimeException e) {
+                    log.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + inputPaymentControlContext.getPaymentExternalKey(), e);
+                }
+            }
+        }
+        return new DefaultOnSuccessPaymentControlResult(inputPluginProperties);
+    }
+
+    public OnFailurePaymentControlResult executePluginOnFailureCalls(final Account account,
+                                                                     final UUID paymentMethodId,
+                                                                     final UUID paymentAttemptId,
+                                                                     final UUID paymentId,
+                                                                     final String paymentExternalKey,
+                                                                     final String paymentTransactionExternalKey,
+                                                                     final PaymentApiType paymentApiType,
+                                                                     final TransactionType transactionType,
+                                                                     final HPPType hppType,
+                                                                     final BigDecimal amount,
+                                                                     final Currency currency,
+                                                                     final boolean isApiPayment,
+                                                                     final List<String> paymentControlPluginNames,
+                                                                     final Iterable<PluginProperty> pluginProperties,
+                                                                     final CallContext callContext) {
+
+        final PaymentControlContext inputPaymentControlContext = new DefaultPaymentControlContext(account,
+                                                                                                  paymentMethodId,
+                                                                                                  paymentAttemptId,
+                                                                                                  paymentId,
+                                                                                                  paymentExternalKey,
+                                                                                                  paymentTransactionExternalKey,
+                                                                                                  paymentApiType,
+                                                                                                  transactionType,
+                                                                                                  hppType,
+                                                                                                  amount,
+                                                                                                  currency,
+                                                                                                  isApiPayment,
+                                                                                                  callContext);
+
+        DateTime candidate = null;
+        Iterable<PluginProperty> inputPluginProperties = pluginProperties;
+
+        for (final String pluginName : paymentControlPluginNames) {
+            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+            if (plugin != null) {
+                try {
+                    final OnFailurePaymentControlResult result = plugin.onFailureCall(inputPaymentControlContext, inputPluginProperties);
+                    if (candidate == null) {
+                        candidate = result.getNextRetryDate();
+                    } else if (result.getNextRetryDate() != null) {
+                        candidate = candidate.compareTo(result.getNextRetryDate()) > 0 ? result.getNextRetryDate() : candidate;
+                    }
+
+                    if (result.getAdjustedPluginProperties() != null) {
+                        inputPluginProperties = result.getAdjustedPluginProperties();
+                    }
+
+                } catch (final PaymentControlApiException e) {
+                    log.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + inputPaymentControlContext.getPaymentExternalKey(), e);
+                    return new DefaultFailureCallResult(candidate, inputPluginProperties);
+                }
+            }
+        }
+        return new DefaultFailureCallResult(candidate, inputPluginProperties);
+    }
+
+    public static class DefaultPaymentControlContext extends DefaultCallContext implements PaymentControlContext {
+
+        private final Account account;
+        private final UUID paymentMethodId;
+        private final UUID attemptId;
+        private final UUID paymentId;
+        private final String paymentExternalKey;
+        private final UUID transactionId;
+        private final String transactionExternalKey;
+        private final PaymentApiType paymentApiType;
+        private final HPPType hppType;
+        private final TransactionType transactionType;
+        private final BigDecimal amount;
+        private final Currency currency;
+        private final BigDecimal processedAmount;
+        private final Currency processedCurrency;
+        private final boolean isApiPayment;
+
+        public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, final String transactionExternalKey,
+                                            final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType, final BigDecimal amount, final Currency currency,
+                                            final boolean isApiPayment, final CallContext callContext) {
+            this(account, paymentMethodId, attemptId, paymentId, paymentExternalKey, null, transactionExternalKey, paymentApiType, transactionType, hppType, amount, currency, null, null, isApiPayment, callContext);
+        }
+
+        public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, @Nullable final UUID transactionId, final String transactionExternalKey,
+                                            final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType,
+                                            final BigDecimal amount, final Currency currency, @Nullable final BigDecimal processedAmount, @Nullable final Currency processedCurrency, final boolean isApiPayment, final CallContext callContext) {
+            super(callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(), callContext.getComments(), callContext.getUserToken(), callContext.getCreatedDate(), callContext.getUpdatedDate());
+            this.account = account;
+            this.paymentMethodId = paymentMethodId;
+            this.attemptId = attemptId;
+            this.paymentId = paymentId;
+            this.paymentExternalKey = paymentExternalKey;
+            this.transactionId = transactionId;
+            this.transactionExternalKey = transactionExternalKey;
+            this.paymentApiType = paymentApiType;
+            this.hppType = hppType;
+            this.transactionType = transactionType;
+            this.amount = amount;
+            this.currency = currency;
+            this.processedAmount = processedAmount;
+            this.processedCurrency = processedCurrency;
+            this.isApiPayment = isApiPayment;
+        }
+
+        @Override
+        public UUID getAccountId() {
+            return account.getId();
+        }
+
+        @Override
+        public String getPaymentExternalKey() {
+            return paymentExternalKey;
+        }
+
+        @Override
+        public String getTransactionExternalKey() {
+            return transactionExternalKey;
+        }
+
+        @Override
+        public PaymentApiType getPaymentApiType() {
+            return paymentApiType;
+        }
+
+        @Override
+        public TransactionType getTransactionType() {
+            return transactionType;
+        }
+
+        @Override
+        public HPPType getHPPType() {
+            return hppType;
+        }
+
+        @Override
+        public BigDecimal getAmount() {
+            return amount;
+        }
+
+        @Override
+        public Currency getCurrency() {
+            return currency;
+        }
+
+        @Override
+        public UUID getPaymentMethodId() {
+            return paymentMethodId;
+        }
+
+        @Override
+        public UUID getPaymentId() {
+            return paymentId;
+        }
+
+        @Override
+        public UUID getAttemptPaymentId() {
+            return attemptId;
+        }
+
+        @Override
+        public BigDecimal getProcessedAmount() {
+            return processedAmount;
+        }
+
+        @Override
+        public Currency getProcessedCurrency() {
+            return processedCurrency;
+        }
+
+        @Override
+        public boolean isApiPayment() {
+            return isApiPayment;
+        }
+
+        public UUID getTransactionId() {
+            return transactionId;
+        }
+
+        @Override
+        public String toString() {
+            return "DefaultPaymentControlContext{" +
+                   "account=" + account +
+                   ", paymentMethodId=" + paymentMethodId +
+                   ", attemptId=" + attemptId +
+                   ", paymentId=" + paymentId +
+                   ", paymentExternalKey='" + paymentExternalKey + '\'' +
+                   ", transactionId=" + transactionId +
+                   ", transactionExternalKey='" + transactionExternalKey + '\'' +
+                   ", paymentApiType=" + paymentApiType +
+                   ", hppType=" + hppType +
+                   ", transactionType=" + transactionType +
+                   ", amount=" + amount +
+                   ", currency=" + currency +
+                   ", processedAmount=" + processedAmount +
+                   ", processedCurrency=" + processedCurrency +
+                   ", isApiPayment=" + isApiPayment +
+                   '}';
+        }
+    }
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java
index d0db210..1a071fa 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CreditControlOperation.java
@@ -27,8 +27,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class CreditControlOperation extends OperationControlCallback {
 
-    public CreditControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+    public CreditControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
index e1c632f..a1e8928 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
@@ -17,9 +17,7 @@
 
 package org.killbill.billing.payment.core.sm.control;
 
-import java.math.BigDecimal;
 import java.util.List;
-import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
@@ -29,30 +27,24 @@ import org.joda.time.DateTime;
 import org.killbill.automaton.Operation.OperationCallback;
 import org.killbill.automaton.OperationException;
 import org.killbill.automaton.OperationResult;
-import org.killbill.billing.account.api.Account;
-import org.killbill.billing.callcontext.DefaultCallContext;
-import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
 import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult;
+import org.killbill.billing.control.plugin.api.PaymentApiType;
 import org.killbill.billing.control.plugin.api.PaymentControlApiException;
 import org.killbill.billing.control.plugin.api.PaymentControlContext;
-import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
 import org.killbill.billing.control.plugin.api.PriorPaymentControlResult;
-import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PaymentTransaction;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
-import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.ProcessorBase.WithAccountLockCallback;
 import org.killbill.billing.payment.core.sm.OperationCallbackBase;
 import org.killbill.billing.payment.core.sm.PaymentStateContext;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner.DefaultPaymentControlContext;
 import org.killbill.billing.payment.dispatcher.PluginDispatcher;
 import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType;
-import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult;
-import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.commons.locker.GlobalLocker;
 import org.killbill.commons.locker.LockFailedException;
 import org.slf4j.Logger;
@@ -62,15 +54,20 @@ import com.google.common.base.MoreObjects;
 
 public abstract class OperationControlCallback extends OperationCallbackBase<Payment, PaymentApiException> implements OperationCallback {
 
+    private static final Logger logger = LoggerFactory.getLogger(OperationControlCallback.class);
+
     protected final PaymentProcessor paymentProcessor;
     protected final PaymentStateControlContext paymentStateControlContext;
-    private final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry;
-    private final Logger logger = LoggerFactory.getLogger(OperationControlCallback.class);
+    private final ControlPluginRunner controlPluginRunner;
 
-    protected OperationControlCallback(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> retryPluginRegistry) {
+    protected OperationControlCallback(final GlobalLocker locker,
+                                       final PluginDispatcher<OperationResult> paymentPluginDispatcher,
+                                       final PaymentStateControlContext paymentStateContext,
+                                       final PaymentProcessor paymentProcessor,
+                                       final ControlPluginRunner controlPluginRunner) {
         super(locker, paymentPluginDispatcher, paymentStateContext);
         this.paymentProcessor = paymentProcessor;
-        this.paymentControlPluginRegistry = retryPluginRegistry;
+        this.controlPluginRunner = controlPluginRunner;
         this.paymentStateControlContext = paymentStateContext;
     }
 
@@ -91,7 +88,9 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
                                                                                                      paymentStateContext.getPaymentId(),
                                                                                                      paymentStateContext.getPaymentExternalKey(),
                                                                                                      paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                                                     PaymentApiType.PAYMENT_TRANSACTION,
                                                                                                      paymentStateContext.getTransactionType(),
+                                                                                                     null,
                                                                                                      paymentStateContext.getAmount(),
                                                                                                      paymentStateContext.getCurrency(),
                                                                                                      paymentStateControlContext.isApiPayment(),
@@ -124,7 +123,9 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
                                                                                                                     result.getExternalKey(),
                                                                                                                     transaction.getId(),
                                                                                                                     paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                                                                    PaymentApiType.PAYMENT_TRANSACTION,
                                                                                                                     paymentStateContext.getTransactionType(),
+                                                                                                                    null,
                                                                                                                     transaction.getAmount(),
                                                                                                                     transaction.getCurrency(),
                                                                                                                     transaction.getProcessedAmount(),
@@ -177,67 +178,49 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
     }
 
     private PriorPaymentControlResult executePluginPriorCalls(final List<String> paymentControlPluginNames, final PaymentControlContext paymentControlContextArg) throws PaymentControlApiException {
-        // Return as soon as the first plugin aborts, or the last result for the last plugin
-        PriorPaymentControlResult prevResult = null;
-
-        // Those values are adjusted prior each call with the result of what previous call to plugin returned
-        PaymentControlContext inputPaymentControlContext = paymentControlContextArg;
-        Iterable<PluginProperty> inputPluginProperties = paymentStateContext.getProperties();
-
-        for (final String pluginName : paymentControlPluginNames) {
-            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
-            if (plugin == null) {
-                // First call to plugin, we log warn, if plugin is not registered
-                logger.warn("Skipping unknown payment control plugin {} when fetching results", pluginName);
-                continue;
-            }
-            prevResult = plugin.priorCall(inputPaymentControlContext, inputPluginProperties);
-            if (prevResult.getAdjustedPluginProperties() != null) {
-                inputPluginProperties = prevResult.getAdjustedPluginProperties();
-            }
-            if (prevResult.isAborted()) {
-                break;
-            }
-            inputPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(),
-                                                                          prevResult.getAdjustedPaymentMethodId() != null ? prevResult.getAdjustedPaymentMethodId() : inputPaymentControlContext.getPaymentMethodId(),
-                                                                          paymentStateControlContext.getAttemptId(),
-                                                                          paymentStateContext.getPaymentId(),
-                                                                          paymentStateContext.getPaymentExternalKey(),
-                                                                          paymentStateContext.getPaymentTransactionExternalKey(),
-                                                                          paymentStateContext.getTransactionType(),
-                                                                          prevResult.getAdjustedAmount() != null ? prevResult.getAdjustedAmount() : inputPaymentControlContext.getAmount(),
-                                                                          prevResult.getAdjustedCurrency() != null ? prevResult.getAdjustedCurrency() : inputPaymentControlContext.getCurrency(),
-                                                                          paymentStateControlContext.isApiPayment(),
-                                                                          paymentStateContext.getCallContext());
 
-        }
-        // Rebuild latest result to include inputPluginProperties
-        prevResult = new DefaultPriorPaymentControlResult(prevResult, inputPluginProperties);
-        // Adjust context with all values if necessary
-        adjustStateContextForPriorCall(paymentStateContext, prevResult);
-        return prevResult;
+        final PriorPaymentControlResult result = controlPluginRunner.executePluginPriorCalls(paymentStateContext.getAccount(),
+                                                                                             paymentControlContextArg.getPaymentMethodId(),
+                                                                                             paymentStateControlContext.getAttemptId(),
+                                                                                             paymentStateContext.getPaymentId(),
+                                                                                             paymentStateContext.getPaymentExternalKey(),
+                                                                                             paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                                             PaymentApiType.PAYMENT_TRANSACTION,
+                                                                                             paymentStateContext.getTransactionType(),
+                                                                                             null,
+                                                                                             paymentControlContextArg.getAmount(),
+                                                                                             paymentControlContextArg.getCurrency(),
+                                                                                             paymentStateControlContext.isApiPayment(),
+                                                                                             paymentControlPluginNames,
+                                                                                             paymentStateContext.getProperties(),
+                                                                                             paymentStateContext.getCallContext());
+
+        adjustStateContextForPriorCall(paymentStateContext, result);
+        return result;
     }
 
     protected void executePluginOnSuccessCalls(final List<String> paymentControlPluginNames, final PaymentControlContext paymentControlContext) {
-
-        Iterable<PluginProperty> inputPluginProperties = paymentStateContext.getProperties();
-        for (final String pluginName : paymentControlPluginNames) {
-            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
-            if (plugin != null) {
-                try {
-                    final OnSuccessPaymentControlResult result = plugin.onSuccessCall(paymentControlContext, inputPluginProperties);
-                    if (result.getAdjustedPluginProperties() != null) {
-                        inputPluginProperties = result.getAdjustedPluginProperties();
-                    }
-                    // Exceptions from the control plugins are ignored (and logged) because the semantics on what to do are undefined.
-                } catch (final PaymentControlApiException e) {
-                    logger.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + paymentControlContext.getPaymentExternalKey(), e);
-                } catch (final RuntimeException e) {
-                    logger.warn("Plugin " + pluginName + " failed to complete executePluginOnSuccessCalls call for " + paymentControlContext.getPaymentExternalKey(), e);
-                }
-            }
-        }
-        adjustStateContextPluginProperties(paymentStateContext, inputPluginProperties);
+        // Values that were obtained/chnaged after the payment call was made (paymentId, processedAmount, processedCurrency,... needs to be extracted from the paymentControlContext)
+        // paymentId, paymentExternalKey, transactionAmount, transaction currency are extracted from  paymentControlContext which was update from the operation result.
+        final OnSuccessPaymentControlResult result = controlPluginRunner.executePluginOnSuccessCalls(paymentStateContext.getAccount(),
+                                                                                                     paymentStateContext.getPaymentMethodId(),
+                                                                                                     paymentStateControlContext.getAttemptId(),
+                                                                                                     paymentControlContext.getPaymentId(),
+                                                                                                     paymentControlContext.getPaymentExternalKey(),
+                                                                                                     paymentControlContext.getTransactionId(),
+                                                                                                     paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                                                     PaymentApiType.PAYMENT_TRANSACTION,
+                                                                                                     paymentStateContext.getTransactionType(),
+                                                                                                     null,
+                                                                                                     paymentControlContext.getAmount(),
+                                                                                                     paymentControlContext.getCurrency(),
+                                                                                                     paymentControlContext.getProcessedAmount(),
+                                                                                                     paymentControlContext.getProcessedCurrency(),
+                                                                                                     paymentStateControlContext.isApiPayment(),
+                                                                                                     paymentControlPluginNames,
+                                                                                                     paymentStateContext.getProperties(),
+                                                                                                     paymentStateContext.getCallContext());
+        adjustStateContextPluginProperties(paymentStateContext, result.getAdjustedPluginProperties());
     }
 
     private OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentStateControlContext paymentStateControlContext, final PaymentControlContext paymentControlContext) {
@@ -250,32 +233,23 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
 
     private DateTime executePluginOnFailureCalls(final List<String> paymentControlPluginNames, final PaymentControlContext paymentControlContext) {
 
-        DateTime candidate = null;
-        Iterable<PluginProperty> inputPluginProperties = paymentStateContext.getProperties();
-
-        for (final String pluginName : paymentControlPluginNames) {
-            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
-            if (plugin != null) {
-                try {
-                    final OnFailurePaymentControlResult result = plugin.onFailureCall(paymentControlContext, inputPluginProperties);
-                    if (candidate == null) {
-                        candidate = result.getNextRetryDate();
-                    } else if (result.getNextRetryDate() != null) {
-                        candidate = candidate.compareTo(result.getNextRetryDate()) > 0 ? result.getNextRetryDate() : candidate;
-                    }
-
-                    if (result.getAdjustedPluginProperties() != null) {
-                        inputPluginProperties = result.getAdjustedPluginProperties();
-                    }
-
-                } catch (final PaymentControlApiException e) {
-                    logger.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + paymentControlContext.getPaymentExternalKey(), e);
-                    return candidate;
-                }
-            }
-        }
-        adjustStateContextPluginProperties(paymentStateContext, inputPluginProperties);
-        return candidate;
+        final OnFailurePaymentControlResult result = controlPluginRunner.executePluginOnFailureCalls(paymentStateContext.getAccount(),
+                                                                                                     paymentControlContext.getPaymentMethodId(),
+                                                                                                     paymentStateControlContext.getAttemptId(),
+                                                                                                     paymentStateContext.getPaymentId(),
+                                                                                                     paymentStateContext.getPaymentExternalKey(),
+                                                                                                     paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                                                     PaymentApiType.PAYMENT_TRANSACTION,
+                                                                                                     paymentStateContext.getTransactionType(),
+                                                                                                     null,
+                                                                                                     paymentControlContext.getAmount(),
+                                                                                                     paymentControlContext.getCurrency(),
+                                                                                                     paymentStateControlContext.isApiPayment(),
+                                                                                                     paymentControlPluginNames,
+                                                                                                     paymentStateContext.getProperties(),
+                                                                                                     paymentStateContext.getCallContext());
+        adjustStateContextPluginProperties(paymentStateContext, result.getAdjustedPluginProperties());
+        return result.getNextRetryDate();
     }
 
     private void adjustStateContextForPriorCall(final PaymentStateContext inputContext, @Nullable final PriorPaymentControlResult pluginResult) {
@@ -303,127 +277,4 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
         final PaymentStateControlContext input = (PaymentStateControlContext) inputContext;
         input.setProperties(pluginProperties);
     }
-
-    public static class DefaultPaymentControlContext extends DefaultCallContext implements PaymentControlContext {
-
-        private final Account account;
-        private final UUID paymentMethodId;
-        private final UUID attemptId;
-        private final UUID paymentId;
-        private final String paymentExternalKey;
-        private final UUID transactionId;
-        private final String transactionExternalKey;
-        private final TransactionType transactionType;
-        private final BigDecimal amount;
-        private final Currency currency;
-        private final BigDecimal processedAmount;
-        private final Currency processedCurrency;
-        private final boolean isApiPayment;
-
-        public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, final String transactionExternalKey, final TransactionType transactionType, final BigDecimal amount, final Currency currency,
-                                            final boolean isApiPayment, final CallContext callContext) {
-            this(account, paymentMethodId, attemptId, paymentId, paymentExternalKey, null, transactionExternalKey, transactionType, amount, currency, null, null, isApiPayment, callContext);
-        }
-
-        public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, @Nullable final UUID transactionId, final String transactionExternalKey, final TransactionType transactionType,
-                                            final BigDecimal amount, final Currency currency, @Nullable final BigDecimal processedAmount, @Nullable final Currency processedCurrency, final boolean isApiPayment, final CallContext callContext) {
-            super(callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(), callContext.getComments(), callContext.getUserToken(), callContext.getCreatedDate(), callContext.getUpdatedDate());
-            this.account = account;
-            this.paymentMethodId = paymentMethodId;
-            this.attemptId = attemptId;
-            this.paymentId = paymentId;
-            this.paymentExternalKey = paymentExternalKey;
-            this.transactionId = transactionId;
-            this.transactionExternalKey = transactionExternalKey;
-            this.transactionType = transactionType;
-            this.amount = amount;
-            this.currency = currency;
-            this.processedAmount = processedAmount;
-            this.processedCurrency = processedCurrency;
-            this.isApiPayment = isApiPayment;
-        }
-
-        @Override
-        public UUID getAccountId() {
-            return account.getId();
-        }
-
-        @Override
-        public String getPaymentExternalKey() {
-            return paymentExternalKey;
-        }
-
-        @Override
-        public String getTransactionExternalKey() {
-            return transactionExternalKey;
-        }
-
-        @Override
-        public TransactionType getTransactionType() {
-            return transactionType;
-        }
-
-        @Override
-        public BigDecimal getAmount() {
-            return amount;
-        }
-
-        @Override
-        public Currency getCurrency() {
-            return currency;
-        }
-
-        @Override
-        public UUID getPaymentMethodId() {
-            return paymentMethodId;
-        }
-
-        @Override
-        public UUID getPaymentId() {
-            return paymentId;
-        }
-
-        @Override
-        public UUID getAttemptPaymentId() {
-            return attemptId;
-        }
-
-        @Override
-        public BigDecimal getProcessedAmount() {
-            return processedAmount;
-        }
-
-        @Override
-        public Currency getProcessedCurrency() {
-            return processedCurrency;
-        }
-
-        @Override
-        public boolean isApiPayment() {
-            return isApiPayment;
-        }
-
-        public UUID getTransactionId() {
-            return transactionId;
-        }
-
-        @Override
-        public String toString() {
-            return "DefaultPaymentControlContext{" +
-                   "account=" + account +
-                   ", paymentMethodId=" + paymentMethodId +
-                   ", attemptId=" + attemptId +
-                   ", paymentId=" + paymentId +
-                   ", paymentExternalKey='" + paymentExternalKey + '\'' +
-                   ", transactionId=" + transactionId +
-                   ", transactionExternalKey='" + transactionExternalKey + '\'' +
-                   ", transactionType=" + transactionType +
-                   ", amount=" + amount +
-                   ", currency=" + currency +
-                   ", processedAmount=" + processedAmount +
-                   ", processedCurrency=" + processedCurrency +
-                   ", isApiPayment=" + isApiPayment +
-                   '}';
-        }
-    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java
index 602231e..7ad848f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PurchaseControlOperation.java
@@ -27,8 +27,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class PurchaseControlOperation extends OperationControlCallback {
 
-    public PurchaseControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> retryPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, retryPluginRegistry);
+    public PurchaseControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java
index d9152f0..5f4d7cd 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/RefundControlOperation.java
@@ -27,8 +27,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class RefundControlOperation extends OperationControlCallback {
 
-    public RefundControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+    public RefundControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java
index 67f4d9b..00200d1 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/VoidControlOperation.java
@@ -27,8 +27,8 @@ import org.killbill.commons.locker.GlobalLocker;
 
 public class VoidControlOperation extends OperationControlCallback {
 
-    public VoidControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+    public VoidControlOperation(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
index 9c1dbc7..f3ca0ca 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
@@ -47,6 +47,7 @@ import org.killbill.billing.payment.core.sm.control.AuthorizeControlOperation;
 import org.killbill.billing.payment.core.sm.control.CaptureControlOperation;
 import org.killbill.billing.payment.core.sm.control.ChargebackControlOperation;
 import org.killbill.billing.payment.core.sm.control.CompletionControlOperation;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.core.sm.control.CreditControlOperation;
 import org.killbill.billing.payment.core.sm.control.DefaultControlCompleted;
 import org.killbill.billing.payment.core.sm.control.DefaultControlInitiated;
@@ -78,16 +79,19 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner 
     private final PaymentProcessor paymentProcessor;
     private final RetryServiceScheduler retryServiceScheduler;
     private final PaymentControlStateMachineHelper paymentControlStateMachineHelper;
+    private final ControlPluginRunner controlPluginRunner;
 
     @Inject
     public PluginControlPaymentAutomatonRunner(@Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig, final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
                                                final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry, final Clock clock, final PaymentProcessor paymentProcessor, @Named(RETRYABLE_NAMED) final RetryServiceScheduler retryServiceScheduler,
-                                               final PaymentConfig paymentConfig, @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper paymentControlStateMachineHelper, final PersistentBus eventBus) {
+                                               final PaymentConfig paymentConfig, @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor, final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper paymentControlStateMachineHelper,
+                                               final ControlPluginRunner controlPluginRunner, final PersistentBus eventBus) {
         super(stateMachineConfig, paymentConfig, paymentDao, locker, pluginRegistry, clock, executor, eventBus, paymentSMHelper);
         this.paymentProcessor = paymentProcessor;
         this.paymentControlPluginRegistry = paymentControlPluginRegistry;
         this.retryServiceScheduler = retryServiceScheduler;
         this.paymentControlStateMachineHelper = paymentControlStateMachineHelper;
+        this.controlPluginRunner = controlPluginRunner;
     }
 
     public Payment run(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
@@ -132,7 +136,7 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner 
 
     public Payment completeRun(final PaymentStateControlContext paymentStateContext) throws PaymentApiException {
         try {
-            final OperationCallback callback = new CompletionControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+            final OperationCallback callback = new CompletionControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
             final LeavingStateCallback leavingStateCallback = new NoopControlInitiated();
             final EnteringStateCallback enteringStateCallback = new DefaultControlCompleted(this, paymentStateContext, paymentControlStateMachineHelper.getRetriedState(), retryServiceScheduler);
 
@@ -165,25 +169,25 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner 
         final OperationCallback callback;
         switch (transactionType) {
             case AUTHORIZE:
-                callback = new AuthorizeControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new AuthorizeControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             case CAPTURE:
-                callback = new CaptureControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new CaptureControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             case PURCHASE:
-                callback = new PurchaseControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new PurchaseControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             case VOID:
-                callback = new VoidControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new VoidControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             case CREDIT:
-                callback = new CreditControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new CreditControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             case REFUND:
-                callback = new RefundControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new RefundControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             case CHARGEBACK:
-                callback = new ChargebackControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
+                callback = new ChargebackControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
                 break;
             default:
                 throw new IllegalStateException("Unsupported transaction type " + transactionType);
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
index fb3c06b..0c0b0a7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -47,6 +47,7 @@ import org.killbill.billing.payment.core.janitor.Janitor;
 import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper;
 import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
 import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.dao.DefaultPaymentDao;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.invoice.PaymentTagHandler;
@@ -125,6 +126,8 @@ public class PaymentModule extends KillBillModule {
         bind(StateMachineProvider.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toInstance(new StateMachineProvider(DEFAULT_STATE_MACHINE_PAYMENT_XML));
         bind(StateMachineConfig.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toProvider(Key.get(StateMachineProvider.class, Names.named(STATE_MACHINE_PAYMENT)));
         bind(PaymentStateMachineHelper.class).asEagerSingleton();
+
+        bind(ControlPluginRunner.class).asEagerSingleton();
     }
 
     protected void installAutomatonRunner() {
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index 0484897..8510301 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -33,6 +33,7 @@ import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.control.plugin.api.PaymentApiType;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
@@ -114,7 +115,9 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
 
     @Override
     public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> pluginProperties) throws PaymentControlApiException {
+
         final TransactionType transactionType = paymentControlContext.getTransactionType();
+        Preconditions.checkArgument(paymentControlContext.getPaymentApiType() == PaymentApiType.PAYMENT_TRANSACTION);
         Preconditions.checkArgument(transactionType == TransactionType.PURCHASE ||
                                     transactionType == TransactionType.REFUND ||
                                     transactionType == TransactionType.CHARGEBACK);
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java
index 91408b7..d1bb59d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultFailureCallResult.java
@@ -22,10 +22,20 @@ import org.killbill.billing.payment.api.PluginProperty;
 
 public class DefaultFailureCallResult implements OnFailurePaymentControlResult {
 
+    private final Iterable<PluginProperty> adjustedPluginProperties;
     private final DateTime nextRetryDate;
 
+    public DefaultFailureCallResult() {
+        this(null, null);
+    }
+
     public DefaultFailureCallResult(final DateTime nextRetryDate) {
+        this(nextRetryDate, null);
+    }
+
+    public DefaultFailureCallResult(final DateTime nextRetryDate, final Iterable<PluginProperty> adjustedPluginProperties) {
         this.nextRetryDate = nextRetryDate;
+        this.adjustedPluginProperties = adjustedPluginProperties;
     }
 
     @Override
@@ -35,6 +45,6 @@ public class DefaultFailureCallResult implements OnFailurePaymentControlResult {
 
     @Override
     public Iterable<PluginProperty> getAdjustedPluginProperties() {
-        return null;
+        return adjustedPluginProperties;
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java
index 0060036..a97dc58 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultOnSuccessPaymentControlResult.java
@@ -22,8 +22,18 @@ import org.killbill.billing.payment.api.PluginProperty;
 
 public class DefaultOnSuccessPaymentControlResult implements OnSuccessPaymentControlResult {
 
+    private final Iterable<PluginProperty> adjustedPluginProperties;
+
+    public DefaultOnSuccessPaymentControlResult() {
+        this(null);
+    }
+
+    public DefaultOnSuccessPaymentControlResult(final Iterable<PluginProperty> adjustedPluginProperties) {
+        this.adjustedPluginProperties = adjustedPluginProperties;
+    }
+
     @Override
     public Iterable<PluginProperty> getAdjustedPluginProperties() {
-        return null;
+        return adjustedPluginProperties;
     }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
index 5d528a1..34f146d 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
@@ -46,7 +46,7 @@ import com.google.inject.Inject;
 public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
 
     @Inject
-    private OSGIServiceRegistration<PaymentControlPluginApi> retryPluginRegistry;
+    private OSGIServiceRegistration<PaymentControlPluginApi> controlPluginRegistry;
 
     private Account account;
     private UUID newPaymentMethodId;
@@ -58,7 +58,7 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
         final PaymentMethodPlugin paymentMethodInfo = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), false, null);
         newPaymentMethodId = paymentApi.addPaymentMethod(account, paymentMethodInfo.getExternalPaymentMethodId(), MockPaymentProviderPlugin.PLUGIN_NAME, false, paymentMethodInfo, ImmutableList.<PluginProperty>of(), callContext);
 
-        retryPluginRegistry.registerService(new OSGIServiceDescriptor() {
+        controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
             @Override
             public String getPluginSymbolicName() {
                 return null;
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java
new file mode 100644
index 0000000..fb29bff
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentGatewayApiWithPaymentControl.java
@@ -0,0 +1,257 @@
+/*
+ * 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.payment.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
+import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult;
+import org.killbill.billing.control.plugin.api.PaymentControlApiException;
+import org.killbill.billing.control.plugin.api.PaymentControlContext;
+import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
+import org.killbill.billing.control.plugin.api.PriorPaymentControlResult;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.retry.DefaultFailureCallResult;
+import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
+import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+public class TestPaymentGatewayApiWithPaymentControl extends PaymentTestSuiteNoDB {
+
+    @Inject
+    private OSGIServiceRegistration<PaymentControlPluginApi> controlPluginRegistry;
+
+    private Account account;
+
+    private PaymentOptions paymentOptions;
+
+    private TestPaymentGatewayApiControlPlugin plugin;
+    private TestPaymentGatewayApiValidationPlugin validationPlugin;
+
+    @BeforeMethod(groups = "fast")
+    public void beforeMethod() throws Exception {
+
+        super.beforeMethod();
+
+        account = testHelper.createTestAccount("arthur@gmail.com", true);
+
+        paymentOptions = new PaymentOptions() {
+            @Override
+            public boolean isExternalPayment() {
+                return false;
+            }
+
+            @Override
+            public List<String> getPaymentControlPluginNames() {
+                return ImmutableList.of(TestPaymentGatewayApiControlPlugin.PLUGIN_NAME, TestPaymentGatewayApiValidationPlugin.VALIDATION_PLUGIN_NAME);
+            }
+        };
+
+        plugin = new TestPaymentGatewayApiControlPlugin();
+
+        controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
+            @Override
+            public String getPluginSymbolicName() {
+                return null;
+            }
+
+            @Override
+            public String getRegistrationName() {
+                return TestPaymentGatewayApiControlPlugin.PLUGIN_NAME;
+            }
+        }, plugin);
+
+        validationPlugin = new TestPaymentGatewayApiValidationPlugin();
+        controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
+            @Override
+            public String getPluginSymbolicName() {
+                return null;
+            }
+
+            @Override
+            public String getRegistrationName() {
+                return TestPaymentGatewayApiValidationPlugin.VALIDATION_PLUGIN_NAME;
+            }
+        }, validationPlugin);
+
+    }
+
+    @Test(groups = "fast")
+    public void testBuildFormDescriptorWithPaymentControl() throws PaymentApiException {
+
+        final List<PluginProperty> initialProperties = new ArrayList<PluginProperty>();
+        initialProperties.add(new PluginProperty("keyA", "valueA", true));
+        initialProperties.add(new PluginProperty("keyB", "valueB", true));
+        initialProperties.add(new PluginProperty("keyC", "valueC", true));
+
+        final List<PluginProperty> priorNewProperties = new ArrayList<PluginProperty>();
+        priorNewProperties.add(new PluginProperty("keyD", "valueD", true));
+        final List<PluginProperty> priorRemovedProperties = new ArrayList<PluginProperty>();
+        priorRemovedProperties.add(new PluginProperty("keyA", "valueA", true));
+        plugin.setPriorCallProperties(priorNewProperties, priorRemovedProperties);
+
+        final List<PluginProperty> onResultNewProperties = new ArrayList<PluginProperty>();
+        onResultNewProperties.add(new PluginProperty("keyE", "valueE", true));
+        final List<PluginProperty> onResultRemovedProperties = new ArrayList<PluginProperty>();
+        onResultRemovedProperties.add(new PluginProperty("keyB", "valueB", true));
+        plugin.setOnResultProperties(onResultNewProperties, onResultRemovedProperties);
+
+        final List<PluginProperty> expectedPriorCallProperties = new ArrayList<PluginProperty>();
+        expectedPriorCallProperties.add(new PluginProperty("keyB", "valueB", true));
+        expectedPriorCallProperties.add(new PluginProperty("keyC", "valueC", true));
+        expectedPriorCallProperties.add(new PluginProperty("keyD", "valueD", true));
+
+        validationPlugin.setExpectedPriorCallProperties(expectedPriorCallProperties);
+
+        final List<PluginProperty> expectedProperties = new ArrayList<PluginProperty>();
+        expectedProperties.add(new PluginProperty("keyC", "valueC", true));
+        expectedProperties.add(new PluginProperty("keyD", "valueD", true));
+        expectedProperties.add(new PluginProperty("keyE", "valueE", true));
+
+        validationPlugin.setExpectedProperties(expectedProperties);
+
+        paymentGatewayApi.buildFormDescriptorWithPaymentControl(account, account.getPaymentMethodId(), ImmutableList.<PluginProperty>of(), initialProperties, paymentOptions, callContext);
+
+    }
+
+    public static class TestPaymentGatewayApiValidationPlugin implements PaymentControlPluginApi {
+
+        public static final String VALIDATION_PLUGIN_NAME = "TestPaymentGatewayApiValidationPlugin";
+
+        private Iterable<PluginProperty> expectedPriorCallProperties;
+        private Iterable<PluginProperty> expectedProperties;
+
+        public TestPaymentGatewayApiValidationPlugin() {
+            this.expectedPriorCallProperties = ImmutableList.of();
+            this.expectedProperties = ImmutableList.of();
+        }
+
+        public void setExpectedProperties(final Iterable<PluginProperty> expectedProperties) {
+            this.expectedProperties = expectedProperties;
+        }
+
+        public void setExpectedPriorCallProperties(final List<PluginProperty> expectedPriorCallProperties) {
+            this.expectedPriorCallProperties = expectedPriorCallProperties;
+        }
+
+        @Override
+        public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+            validate(properties, expectedPriorCallProperties);
+            return new DefaultPriorPaymentControlResult(false);
+        }
+
+        @Override
+        public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+            validate(properties, expectedProperties);
+            return new DefaultOnSuccessPaymentControlResult();
+        }
+
+        @Override
+        public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+            validate(properties, expectedProperties);
+            return new DefaultFailureCallResult();
+        }
+
+        private static void validate(final Iterable<PluginProperty> properties, final Iterable<PluginProperty> expected) {
+            Assert.assertEquals(Iterables.size(properties), Iterables.size(expected), "Got " + Iterables.size(properties) + "properties" + ", expected " + Iterables.size(expected));
+
+            for (final PluginProperty curExpected : expected) {
+                Assert.assertTrue(Iterables.any(properties, new Predicate<PluginProperty>() {
+                    @Override
+                    public boolean apply(final PluginProperty input) {
+                        return input.getKey().equals(curExpected.getKey()) && input.getValue().equals(curExpected.getValue());
+
+                    }
+                }), "Cannot find expected property" + curExpected.getKey());
+            }
+        }
+
+    }
+
+    public static class TestPaymentGatewayApiControlPlugin implements PaymentControlPluginApi {
+
+        public static final String PLUGIN_NAME = "TestPaymentGatewayApiControlPlugin";
+
+        private Iterable<PluginProperty> newPriorCallProperties;
+        private Iterable<PluginProperty> removedPriorCallProperties;
+
+        private Iterable<PluginProperty> newOnResultProperties;
+        private Iterable<PluginProperty> removedOnResultProperties;
+
+        public TestPaymentGatewayApiControlPlugin() {
+            this.newPriorCallProperties = ImmutableList.of();
+            this.removedPriorCallProperties = ImmutableList.of();
+            this.newOnResultProperties = ImmutableList.of();
+            this.removedOnResultProperties = ImmutableList.of();
+        }
+
+        public void setPriorCallProperties(final Iterable<PluginProperty> newPriorCallProperties, final Iterable<PluginProperty> removedPriorCallProperties) {
+            this.newPriorCallProperties = newPriorCallProperties;
+            this.removedPriorCallProperties = removedPriorCallProperties;
+        }
+
+        public void setOnResultProperties(final Iterable<PluginProperty> newOnResultProperties, final Iterable<PluginProperty> removedOnResultProperties) {
+            this.newOnResultProperties = newOnResultProperties;
+            this.removedOnResultProperties = removedOnResultProperties;
+        }
+
+        @Override
+        public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+            return new DefaultPriorPaymentControlResult(false, null, null, null, getAdjustedProperties(properties, newPriorCallProperties, removedPriorCallProperties));
+        }
+
+        @Override
+        public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+            return new DefaultOnSuccessPaymentControlResult(getAdjustedProperties(properties, newOnResultProperties, removedOnResultProperties));
+        }
+
+        @Override
+        public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+            return new DefaultFailureCallResult(null, getAdjustedProperties(properties, newOnResultProperties, removedOnResultProperties));
+        }
+
+        private static Iterable<PluginProperty> getAdjustedProperties(final Iterable<PluginProperty> input, final Iterable<PluginProperty> newProperties, final Iterable<PluginProperty> removedProperties) {
+            final Iterable<PluginProperty> filtered = Iterables.filter(input, new Predicate<PluginProperty>() {
+                @Override
+                public boolean apply(final PluginProperty p) {
+                    final boolean toBeRemoved = Iterables.any(removedProperties, new Predicate<PluginProperty>() {
+                        @Override
+                        public boolean apply(final PluginProperty a) {
+                            return a.getKey().equals(p.getKey()) && a.getValue().equals(p.getValue());
+                        }
+                    });
+                    return !toBeRemoved;
+                }
+            });
+            return Iterables.concat(filtered, newProperties);
+        }
+    }
+
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
index 7a69891..14f6758 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
@@ -37,6 +37,7 @@ import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.dispatcher.PluginDispatcher;
@@ -62,8 +63,8 @@ public class MockRetryablePaymentAutomatonRunner extends PluginControlPaymentAut
     @Inject
     public MockRetryablePaymentAutomatonRunner(@Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig, @Named(PaymentModule.STATE_MACHINE_RETRY) final StateMachineConfig retryStateMachine, final PaymentDao paymentDao, final GlobalLocker locker, final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry, final OSGIServiceRegistration<PaymentControlPluginApi> retryPluginRegistry, final Clock clock, final TagInternalApi tagApi, final PaymentProcessor paymentProcessor,
                                                @Named(RETRYABLE_NAMED) final RetryServiceScheduler retryServiceScheduler, final PaymentConfig paymentConfig, @com.google.inject.name.Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor,
-                                               final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper retrySMHelper, final PersistentBus eventBus) {
-        super(stateMachineConfig, paymentDao, locker, pluginRegistry, retryPluginRegistry, clock, paymentProcessor, retryServiceScheduler, paymentConfig, executor, paymentSMHelper, retrySMHelper, eventBus);
+                                               final PaymentStateMachineHelper paymentSMHelper, final PaymentControlStateMachineHelper retrySMHelper, final ControlPluginRunner controlPluginRunner, final PersistentBus eventBus) {
+        super(stateMachineConfig, paymentDao, locker, pluginRegistry, retryPluginRegistry, clock, paymentProcessor, retryServiceScheduler, paymentConfig, executor, paymentSMHelper, retrySMHelper, controlPluginRunner, eventBus);
     }
 
     @Override
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java
index f7007c0..3dd00a4 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java
@@ -28,6 +28,7 @@ import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.sm.control.AuthorizeControlOperation;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.dao.PaymentModelDao;
@@ -45,8 +46,8 @@ public class MockRetryAuthorizeOperationCallback extends AuthorizeControlOperati
     private Exception exception;
     private OperationResult result;
 
-    public MockRetryAuthorizeOperationCallback(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final OSGIServiceRegistration<PaymentControlPluginApi> retryPluginRegistry, final PaymentDao paymentDao, final Clock clock) {
-        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, retryPluginRegistry);
+    public MockRetryAuthorizeOperationCallback(final GlobalLocker locker, final PluginDispatcher<OperationResult> paymentPluginDispatcher, final PaymentStateControlContext paymentStateContext, final PaymentProcessor paymentProcessor, final ControlPluginRunner controlPluginRunner, final PaymentDao paymentDao, final Clock clock) {
+        super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, controlPluginRunner);
         this.paymentDao = paymentDao;
         this.clock = clock;
     }
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
index b016e7a..433806e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
@@ -41,6 +41,7 @@ import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
+import org.killbill.billing.payment.core.sm.control.ControlPluginRunner;
 import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
 import org.killbill.billing.payment.dao.MockPaymentDao;
 import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
@@ -109,6 +110,8 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
     @Inject
     private PaymentControlStateMachineHelper retrySMHelper;
     @Inject
+    private ControlPluginRunner controlPluginRunner;
+    @Inject
     private InternalCallContextFactory internalCallContextFactory;
 
     private Account account;
@@ -169,6 +172,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
                 executor,
                 paymentSMHelper,
                 retrySMHelper,
+                controlPluginRunner,
                 eventBus);
 
         paymentStateContext =
@@ -191,7 +195,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
                                                         runner.getPaymentPluginDispatcher(),
                                                         paymentStateContext,
                                                         null,
-                                                        runner.getRetryPluginRegistry(),
+                                                        controlPluginRunner,
                                                         paymentDao,
                                                         clock);
 
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
index 778f279..9c19560 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
@@ -23,6 +23,7 @@ import org.killbill.billing.account.api.AccountInternalApi;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentGatewayApi;
 import org.killbill.billing.payment.core.PaymentMethodProcessor;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
@@ -62,6 +63,8 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     @Inject
     protected PaymentApi paymentApi;
     @Inject
+    protected PaymentGatewayApi paymentGatewayApi;
+    @Inject
     protected AccountInternalApi accountInternalApi;
     @Inject
     protected TestPaymentHelper testHelper;
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
index 81c4813..1ab3f64 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
@@ -23,6 +23,7 @@ import org.killbill.billing.account.api.AccountInternalApi;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentGatewayApi;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.PaymentMethodProcessor;
 import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
@@ -60,6 +61,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
     @Inject
     protected PaymentApi paymentApi;
     @Inject
+    protected PaymentGatewayApi paymentGatewayApi;
+    @Inject
     protected AccountInternalApi accountApi;
     @Inject
     protected PaymentStateMachineHelper paymentSMHelper;

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index d61de93..6b6b458 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.33-SNAPSHOT</version>
+        <version>0.37</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.15.3-SNAPSHOT</version>
diff --git a/profiles/killbill/src/main/resources/killbill-server.properties b/profiles/killbill/src/main/resources/killbill-server.properties
index e418e11..734d17b 100644
--- a/profiles/killbill/src/main/resources/killbill-server.properties
+++ b/profiles/killbill/src/main/resources/killbill-server.properties
@@ -29,27 +29,24 @@ org.killbill.dao.logLevel=DEBUG
 org.killbill.catalog.uri=SpyCarAdvanced.xml
 
 # NotificationQ, Bus, ExtBus config
-org.killbill.notificationq.main.sleep=1000
-org.killbill.notificationq.main.claimed=10
-org.killbill.notificationq.main.sticky=true
+org.killbill.notificationq.main.sleep=100
+org.killbill.notificationq.main.claimed=1
+org.killbill.notificationq.main.queue.mode=STICKY_POLLING
+org.killbill.notificationq.main.notification.nbThreads=1
 
-org.killbill.persistent.bus.external.sticky=true
+org.killbill.persistent.bus.external.queue.mode=STICKY_EVENTS
 org.killbill.persistent.bus.external.inMemory=true
 
-#org.killbill.persistent.bus.external.sticky=true
+#org.killbill.persistent.bus.external.queue.mode=STICKY_EVENTS
 #org.killbill.persistent.bus.external.claimed=1
-#org.killbill.persistent.bus.external.inflight.claimed=1
 #org.killbill.persistent.bus.external.nbThreads=1
 #org.killbill.persistent.bus.external.sleep=0
-#org.killbill.persistent.bus.external.useInflightQ=true
 #org.killbill.persistent.bus.external.queue.capacity=100
 
-org.killbill.persistent.bus.main.sticky=true
+org.killbill.persistent.bus.main.queue.mode=STICKY_EVENTS
 org.killbill.persistent.bus.main.claimed=1
-org.killbill.persistent.bus.main.inflight.claimed=1
 org.killbill.persistent.bus.main.nbThreads=1
 org.killbill.persistent.bus.main.sleep=0
-org.killbill.persistent.bus.main.useInflightQ=true
 org.killbill.persistent.bus.main.queue.capacity=100
 
 # Start KB in multi-tenant
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestRL.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestRL.java
new file mode 100644
index 0000000..11e19de
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestRL.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Accounts;
+import org.killbill.billing.client.model.AuditLog;
+import org.killbill.billing.client.model.CustomField;
+import org.killbill.billing.client.model.InvoicePayments;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.client.model.PaymentMethodPluginDetail;
+import org.killbill.billing.client.model.Tag;
+import org.killbill.billing.util.api.AuditLevel;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestRL extends TestJaxrsBase {
+
+    @Test(groups = "slow", description = "Verify no PII data is required")
+    public void testEmptyAccount() throws Exception {
+        final Account emptyAccount = new Account();
+
+        final Account account = killBillClient.createAccount(emptyAccount, createdBy, reason, comment);
+        Assert.assertNotNull(account.getExternalKey());
+        Assert.assertNull(account.getName());
+        Assert.assertNull(account.getEmail());
+    }
+
+    @Test(groups = "slow", description = "Verify external key is unique")
+    public void testUniqueExternalKey() throws Exception {
+        // Verify the external key is not mandatory
+        final Account inputWithNoExternalKey = getAccount(UUID.randomUUID().toString(), null, UUID.randomUUID().toString());
+        Assert.assertNull(inputWithNoExternalKey.getExternalKey());
+
+        final Account account = killBillClient.createAccount(inputWithNoExternalKey, createdBy, reason, comment);
+        Assert.assertNotNull(account.getExternalKey());
+
+        final Account inputWithSameExternalKey = getAccount(UUID.randomUUID().toString(), account.getExternalKey(), UUID.randomUUID().toString());
+        try {
+            killBillClient.createAccount(inputWithSameExternalKey, createdBy, reason, comment);
+            Assert.fail();
+        } catch (final KillBillClientException e) {
+            Assert.assertEquals(e.getBillingException().getCode(), (Integer) ErrorCode.ACCOUNT_ALREADY_EXISTS.getCode());
+        }
+    }
+
+    @Test(groups = "slow", description = "Can create, retrieve, search and update accounts")
+    public void testAccountOk() throws Exception {
+        final Account input = createAccount();
+
+        // Retrieves by external key
+        final Account retrievedAccount = killBillClient.getAccount(input.getExternalKey());
+        Assert.assertTrue(retrievedAccount.equals(input));
+
+        // Try search endpoint
+        searchAccount(input, retrievedAccount);
+
+        // Update Account
+        final Account newInput = new Account(input.getAccountId(),
+                                             "zozo", 4, input.getExternalKey(), "rr@google.com", 18,
+                                             "USD", null, "UTC", "bl1", "bh2", "", "", "ca", "San Francisco", "usa", "en", "415-255-2991",
+                                             false, false, null, null);
+        final Account updatedAccount = killBillClient.updateAccount(newInput, createdBy, reason, comment);
+        Assert.assertTrue(updatedAccount.equals(newInput));
+
+        // Try search endpoint
+        searchAccount(input, null);
+    }
+
+    @Test(groups = "slow", description = "Can retrieve the account balance")
+    public void testAccountWithBalance() throws Exception {
+        final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+        final Account accountWithBalance = killBillClient.getAccount(accountJson.getAccountId(), true, false);
+        final BigDecimal accountBalance = accountWithBalance.getAccountBalance();
+        Assert.assertTrue(accountBalance.compareTo(BigDecimal.ZERO) > 0);
+    }
+
+    @Test(groups = "slow", description = "Cannot update a non-existent account")
+    public void testUpdateNonExistentAccount() throws Exception {
+        final Account input = getAccount();
+
+        Assert.assertNull(killBillClient.updateAccount(input, createdBy, reason, comment));
+    }
+
+    @Test(groups = "slow", description = "Cannot retrieve non-existent account")
+    public void testAccountNonExistent() throws Exception {
+        Assert.assertNull(killBillClient.getAccount(UUID.randomUUID()));
+        Assert.assertNull(killBillClient.getAccount(UUID.randomUUID().toString()));
+    }
+
+    @Test(groups = "slow", description = "Can CRUD payment methods")
+    public void testAccountPaymentMethods() throws Exception {
+        final Account accountJson = createAccount();
+        assertNotNull(accountJson);
+
+        final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail();
+        info.setProperties(getPaymentMethodCCProperties());
+        PaymentMethod paymentMethodJson = new PaymentMethod(null, UUID.randomUUID().toString(), accountJson.getAccountId(), true, PLUGIN_NAME, info);
+        final PaymentMethod paymentMethodCC = killBillClient.createPaymentMethod(paymentMethodJson, createdBy, reason, comment);
+        assertTrue(paymentMethodCC.getIsDefault());
+
+        //
+        // Add another payment method
+        //
+        final PaymentMethodPluginDetail info2 = new PaymentMethodPluginDetail();
+        info2.setProperties(getPaymentMethodPaypalProperties());
+        paymentMethodJson = new PaymentMethod(null, UUID.randomUUID().toString(), accountJson.getAccountId(), false, PLUGIN_NAME, info2);
+        final PaymentMethod paymentMethodPP = killBillClient.createPaymentMethod(paymentMethodJson, createdBy, reason, comment);
+        assertFalse(paymentMethodPP.getIsDefault());
+
+        //
+        // FETCH ALL PAYMENT METHODS
+        //
+        List<PaymentMethod> paymentMethods = killBillClient.getPaymentMethodsForAccount(accountJson.getAccountId());
+        assertEquals(paymentMethods.size(), 2);
+
+        //
+        // CHANGE DEFAULT
+        //
+        assertTrue(killBillClient.getPaymentMethod(paymentMethodCC.getPaymentMethodId()).getIsDefault());
+        assertFalse(killBillClient.getPaymentMethod(paymentMethodPP.getPaymentMethodId()).getIsDefault());
+        killBillClient.updateDefaultPaymentMethod(accountJson.getAccountId(), paymentMethodPP.getPaymentMethodId(), createdBy, reason, comment);
+        assertTrue(killBillClient.getPaymentMethod(paymentMethodPP.getPaymentMethodId()).getIsDefault());
+        assertFalse(killBillClient.getPaymentMethod(paymentMethodCC.getPaymentMethodId()).getIsDefault());
+
+        //
+        // DELETE NON DEFAULT PM
+        //
+        killBillClient.deletePaymentMethod(paymentMethodCC.getPaymentMethodId(), false, createdBy, reason, comment);
+
+        //
+        // FETCH ALL PAYMENT METHODS
+        //
+        paymentMethods = killBillClient.getPaymentMethodsForAccount(accountJson.getAccountId());
+        assertEquals(paymentMethods.size(), 1);
+
+        //
+        // DELETE DEFAULT PAYMENT METHOD (without special flag first)
+        //
+        try {
+            killBillClient.deletePaymentMethod(paymentMethodPP.getPaymentMethodId(), false, createdBy, reason, comment);
+            fail();
+        } catch (final KillBillClientException e) {
+        }
+
+        //
+        // RETRY TO DELETE DEFAULT PAYMENT METHOD (with special flag this time)
+        //
+        killBillClient.deletePaymentMethod(paymentMethodPP.getPaymentMethodId(), true, createdBy, reason, comment);
+
+        // CHECK ACCOUNT IS NOW AUTO_PAY_OFF
+        final List<Tag> tagsJson = killBillClient.getAccountTags(accountJson.getAccountId());
+        Assert.assertEquals(tagsJson.size(), 1);
+        final Tag tagJson = tagsJson.get(0);
+        Assert.assertEquals(tagJson.getTagDefinitionName(), "AUTO_PAY_OFF");
+        Assert.assertEquals(tagJson.getTagDefinitionId(), new UUID(0, 1));
+
+        // FETCH ACCOUNT AGAIN AND CHECK THERE IS NO DEFAULT PAYMENT METHOD SET
+        final Account updatedAccount = killBillClient.getAccount(accountJson.getAccountId());
+        Assert.assertEquals(updatedAccount.getAccountId(), accountJson.getAccountId());
+        Assert.assertNull(updatedAccount.getPaymentMethodId());
+
+        //
+        // FINALLY TRY TO REMOVE AUTO_PAY_OFF WITH NO DEFAULT PAYMENT METHOD ON ACCOUNT
+        //
+        try {
+            killBillClient.deleteAccountTag(accountJson.getAccountId(), new UUID(0, 1), createdBy, reason, comment);
+        } catch (final KillBillClientException e) {
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testAccountPaymentsWithRefund() throws Exception {
+        final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+        // Verify payments
+        final InvoicePayments objFromJson = killBillClient.getInvoicePaymentsForAccount(accountJson.getAccountId());
+        Assert.assertEquals(objFromJson.size(), 1);
+    }
+
+    @Test(groups = "slow", description = "Add tags to account")
+    public void testTags() throws Exception {
+        final Account input = createAccount();
+        // Use tag definition for AUTO_PAY_OFF
+        final UUID autoPayOffId = new UUID(0, 1);
+
+        // Add a tag
+        killBillClient.createAccountTag(input.getAccountId(), autoPayOffId, createdBy, reason, comment);
+
+        // Retrieves all tags
+        final List<Tag> tags1 = killBillClient.getAccountTags(input.getAccountId(), AuditLevel.FULL);
+        Assert.assertEquals(tags1.size(), 1);
+        Assert.assertEquals(tags1.get(0).getTagDefinitionId(), autoPayOffId);
+
+        // Verify adding the same tag a second time doesn't do anything
+        killBillClient.createAccountTag(input.getAccountId(), autoPayOffId, createdBy, reason, comment);
+
+        // Retrieves all tags again
+        killBillClient.createAccountTag(input.getAccountId(), autoPayOffId, createdBy, reason, comment);
+        final List<Tag> tags2 = killBillClient.getAccountTags(input.getAccountId(), AuditLevel.FULL);
+        Assert.assertEquals(tags2, tags1);
+
+        // Verify audit logs
+        Assert.assertEquals(tags2.get(0).getAuditLogs().size(), 1);
+        final AuditLog auditLogJson = tags2.get(0).getAuditLogs().get(0);
+        Assert.assertEquals(auditLogJson.getChangeType(), "INSERT");
+        Assert.assertEquals(auditLogJson.getChangedBy(), createdBy);
+        Assert.assertEquals(auditLogJson.getReasonCode(), reason);
+        Assert.assertEquals(auditLogJson.getComments(), comment);
+        Assert.assertNotNull(auditLogJson.getChangeDate());
+        Assert.assertNotNull(auditLogJson.getUserToken());
+    }
+
+    @Test(groups = "slow", description = "Add custom fields to account")
+    public void testCustomFields() throws Exception {
+        final Account accountJson = createAccount();
+        assertNotNull(accountJson);
+
+        final Collection<CustomField> customFields = new LinkedList<CustomField>();
+        customFields.add(new CustomField(null, accountJson.getAccountId(), ObjectType.ACCOUNT, "1", "value1", null));
+        customFields.add(new CustomField(null, accountJson.getAccountId(), ObjectType.ACCOUNT, "2", "value2", null));
+        customFields.add(new CustomField(null, accountJson.getAccountId(), ObjectType.ACCOUNT, "3", "value3", null));
+
+        killBillClient.createAccountCustomFields(accountJson.getAccountId(), customFields, createdBy, reason, comment);
+
+        final List<CustomField> accountCustomFields = killBillClient.getAccountCustomFields(accountJson.getAccountId());
+        assertEquals(accountCustomFields.size(), 3);
+
+        // Delete all custom fields for account
+        killBillClient.deleteAccountCustomFields(accountJson.getAccountId(), createdBy, reason, comment);
+
+        final List<CustomField> remainingCustomFields = killBillClient.getAccountCustomFields(accountJson.getAccountId());
+        assertEquals(remainingCustomFields.size(), 0);
+    }
+
+    @Test(groups = "slow", description = "Can paginate through all accounts")
+    public void testAccountsPagination() throws Exception {
+        for (int i = 0; i < 5; i++) {
+            createAccount();
+        }
+
+        final Accounts allAccounts = killBillClient.getAccounts();
+        Assert.assertEquals(allAccounts.size(), 5);
+
+        Accounts page = killBillClient.getAccounts(0L, 1L);
+        for (int i = 0; i < 5; i++) {
+            Assert.assertNotNull(page);
+            Assert.assertEquals(page.size(), 1);
+            Assert.assertEquals(page.get(0), allAccounts.get(i));
+            page = page.getNext();
+        }
+        Assert.assertNull(page);
+    }
+
+    private void searchAccount(final Account input, @Nullable final Account output) throws Exception {
+        // Search by id
+        if (output != null) {
+            doSearchAccount(input.getAccountId().toString(), output);
+        }
+
+        // Search by name
+        doSearchAccount(input.getName(), output);
+
+        // Search by email
+        doSearchAccount(input.getEmail(), output);
+
+        // Search by company name
+        doSearchAccount(input.getCompany(), output);
+
+        // Search by external key.
+        // Note: we will always find a match since we don't update it
+        final List<Account> accountsByExternalKey = killBillClient.searchAccounts(input.getExternalKey());
+        Assert.assertEquals(accountsByExternalKey.size(), 1);
+        Assert.assertEquals(accountsByExternalKey.get(0).getAccountId(), input.getAccountId());
+        Assert.assertEquals(accountsByExternalKey.get(0).getExternalKey(), input.getExternalKey());
+    }
+
+    private void doSearchAccount(final String key, @Nullable final Account output) throws Exception {
+        final List<Account> accountsByKey = killBillClient.searchAccounts(key);
+        if (output == null) {
+            Assert.assertEquals(accountsByKey.size(), 0);
+        } else {
+            Assert.assertEquals(accountsByKey.size(), 1);
+            Assert.assertEquals(accountsByKey.get(0), output);
+        }
+    }
+}