killbill-memoizeit

Introduce multiple PaymentControlPlugin for payment attempts

10/15/2014 9:11:07 PM

Changes

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index ada01fe..02c99b0 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -128,8 +128,8 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
         }
 
         @Override
-        public String getPaymentControlPluginName() {
-            return InvoicePaymentControlPluginApi.PLUGIN_NAME;
+        public List<String> getPaymentControlPluginNames() {
+            return ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
         }
     };
 
@@ -140,8 +140,8 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
         }
 
         @Override
-        public String getPaymentControlPluginName() {
-            return InvoicePaymentControlPluginApi.PLUGIN_NAME;
+        public List<String> getPaymentControlPluginNames() {
+            return ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
         }
     };
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index e3d8de4..b95fa5b 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -368,9 +368,9 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
             }
 
             @Override
-            public String getPaymentControlPluginName() {
-                /* Contract with plugin */
-                return "__INVOICE_PAYMENT_CONTROL_PLUGIN__";
+            public List<String> getPaymentControlPluginNames() {
+                /* Will default to org.killbill.payment.control.plugin in payment sub-system */
+                return null;
             }
         };
     }
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 3c0b151..15ec0cb 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
@@ -35,6 +35,7 @@ import org.killbill.billing.payment.core.PluginControlledPaymentProcessor;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.config.PaymentConfig;
 import org.killbill.billing.util.entity.Pagination;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -47,13 +48,15 @@ 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 PluginControlledPaymentProcessor pluginControlledPaymentProcessor;
     private final InternalCallContextFactory internalCallContextFactory;
 
     @Inject
-    public DefaultPaymentApi(final PaymentProcessor paymentProcessor, final PaymentMethodProcessor paymentMethodProcessor, final PluginControlledPaymentProcessor pluginControlledPaymentProcessor, final InternalCallContextFactory internalCallContextFactory) {
+    public DefaultPaymentApi(final PaymentConfig paymentConfig, final PaymentProcessor paymentProcessor, final PaymentMethodProcessor paymentMethodProcessor, final PluginControlledPaymentProcessor pluginControlledPaymentProcessor, final InternalCallContextFactory internalCallContextFactory) {
+        this.paymentConfig = paymentConfig;
         this.paymentProcessor = paymentProcessor;
         this.paymentMethodProcessor = paymentMethodProcessor;
         this.pluginControlledPaymentProcessor = pluginControlledPaymentProcessor;
@@ -137,7 +140,7 @@ public class DefaultPaymentApi implements PaymentApi {
                                            paymentMethodId :
                                            paymentMethodProcessor.createOrGetExternalPaymentMethod(UUID.randomUUID().toString(), account, properties, callContext, internalCallContext);
         return pluginControlledPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNulPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
-                                                               properties, paymentOptions.getPaymentControlPluginName(), callContext, internalCallContext);
+                                                               properties, toPaymentControlPluginNames(paymentOptions), callContext, internalCallContext);
 
     }
 
@@ -192,7 +195,7 @@ public class DefaultPaymentApi implements PaymentApi {
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
         return pluginControlledPaymentProcessor.createRefund(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
-                                                             properties, paymentOptions.getPaymentControlPluginName(), callContext, internalCallContext);
+                                                             properties, toPaymentControlPluginNames(paymentOptions), callContext, internalCallContext);
 
     }
 
@@ -259,7 +262,7 @@ public class DefaultPaymentApi implements PaymentApi {
 
         final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
         return pluginControlledPaymentProcessor.createChargeback(account, paymentId, paymentTransactionExternalKey, amount, currency,
-                                                                 paymentOptions.getPaymentControlPluginName(), callContext, internalCallContext);
+                                                                 toPaymentControlPluginNames(paymentOptions), callContext, internalCallContext);
     }
 
     @Override
@@ -423,6 +426,10 @@ public class DefaultPaymentApi implements PaymentApi {
         }
     }
 
+    private List<String> toPaymentControlPluginNames(final PaymentOptions paymentOptions) {
+        return paymentOptions.getPaymentControlPluginNames() != null ? paymentOptions.getPaymentControlPluginNames() : paymentConfig.getPaymentControlPluginNames();
+    }
+
     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");
diff --git a/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java b/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java
index a5fcee7..0157839 100644
--- a/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java
+++ b/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java
@@ -40,10 +40,12 @@ import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.CallOrigin;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.config.PaymentConfig;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
 
@@ -54,15 +56,18 @@ public class InvoiceHandler {
     private final PluginControlledPaymentProcessor pluginControlledPaymentProcessor;
     private final NonEntityDao nonEntityDao;
     private final CacheControllerDispatcher controllerDispatcher;
+    private final PaymentConfig paymentConfig;
 
     private static final Logger log = LoggerFactory.getLogger(InvoiceHandler.class);
 
     @Inject
-    public InvoiceHandler(final AccountInternalApi accountApi,
+    public InvoiceHandler(final PaymentConfig paymentConfig,
+                          final AccountInternalApi accountApi,
                           final PluginControlledPaymentProcessor pluginControlledPaymentProcessor,
                           final NonEntityDao nonEntityDao,
                           final InternalCallContextFactory internalCallContextFactory,
                           final CacheControllerDispatcher controllerDispatcher) {
+        this.paymentConfig = paymentConfig;
         this.accountApi = accountApi;
         this.internalCallContextFactory = internalCallContextFactory;
         this.pluginControlledPaymentProcessor = pluginControlledPaymentProcessor;
@@ -88,8 +93,9 @@ public class InvoiceHandler {
             final CallContext callContext = internalContext.toCallContext(nonEntityDao.retrieveIdFromObject(internalContext.getTenantRecordId(), ObjectType.TENANT, controllerDispatcher.getCacheController(CacheType.OBJECT_ID)));
 
             final BigDecimal amountToBePaid = null; // We let the plugin compute how much should be paid
+            final List<String> paymentControlPluginNames = paymentConfig.getPaymentControlPluginNames() != null ? paymentConfig.getPaymentControlPluginNames() : ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
             pluginControlledPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amountToBePaid, account.getCurrency(), UUID.randomUUID().toString(), UUID.randomUUID().toString(),
-                                                            properties, InvoicePaymentControlPluginApi.PLUGIN_NAME, callContext, internalContext);
+                                                            properties, paymentControlPluginNames, callContext, internalContext);
         } catch (final AccountApiException e) {
             log.error("Failed to process invoice payment", e);
         } catch (final PaymentApiException e) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/control/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/control/InvoicePaymentControlPluginApi.java
index dbfe997..897f23c 100644
--- a/payment/src/main/java/org/killbill/billing/payment/control/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/control/InvoicePaymentControlPluginApi.java
@@ -124,7 +124,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
             case REFUND:
                 return getPluginRefundResult(paymentControlContext, internalContext);
             case CHARGEBACK:
-                return new DefaultPriorPaymentControlResult(false, paymentControlContext.getAmount());
+                return new DefaultPriorPaymentControlResult(false, paymentControlContext.getAmount(), null, null);
             default:
                 throw new IllegalStateException("Unexpected transactionType " + transactionType);
         }
@@ -209,7 +209,8 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
     public void process_AUTO_PAY_OFF_removal(final Account account, final InternalCallContext internalCallContext) {
         final List<PluginAutoPayOffModelDao> entries = controlDao.getAutoPayOffEntry(account.getId());
         for (PluginAutoPayOffModelDao cur : entries) {
-            retryServiceScheduler.scheduleRetry(ObjectType.ACCOUNT, account.getId(), cur.getAttemptId(), PLUGIN_NAME, clock.getUTCNow());
+            // TODO In theory we should pass not only PLUGIN_NAME, but also all the plugin list associated which the original call
+            retryServiceScheduler.scheduleRetry(ObjectType.ACCOUNT, account.getId(), cur.getAttemptId(), ImmutableList.<String>of(PLUGIN_NAME), clock.getUTCNow());
         }
         controlDao.removeAutoPayOffEntry(account.getId());
     }
@@ -240,7 +241,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                                                      " aborted : invoice balance is = " + invoice.getBalance() +
                                                      ", requested payment amount is = " + paymentControlPluginContext.getAmount());
             } else {
-                return new DefaultPriorPaymentControlResult(isAborted, requestedAmount);
+                return new DefaultPriorPaymentControlResult(isAborted, requestedAmount, null, null);
             }
         } catch (InvoiceApiException e) {
             throw new PaymentControlApiException(e);
@@ -273,7 +274,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
                                                  " aborted : invoice item sum amount is " + amountToBeRefunded +
                                                  ", requested refund amount is = " + paymentControlPluginContext.getAmount());
         } else {
-            return new DefaultPriorPaymentControlResult(isAborted, amountToBeRefunded);
+            return new DefaultPriorPaymentControlResult(isAborted, amountToBeRefunded, null, null);
         }
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/AttemptCompletionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/AttemptCompletionTask.java
index 5714907..717142d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/AttemptCompletionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/AttemptCompletionTask.java
@@ -96,7 +96,7 @@ final class AttemptCompletionTask extends CompletionTaskBase<PaymentAttemptModel
 
             final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
             final boolean isApiPayment = true; // unclear
-            final RetryablePaymentStateContext paymentStateContext = new RetryablePaymentStateContext(attempt.getPluginName(),
+            final RetryablePaymentStateContext paymentStateContext = new RetryablePaymentStateContext(attempt.toPaymentControlPluginNames(),
                                                                                                       isApiPayment,
                                                                                                       transaction.getPaymentId(),
                                                                                                       attempt.getPaymentExternalKey(),
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlledPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlledPaymentProcessor.java
index 9f5fbda..c6c5979 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlledPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlledPaymentProcessor.java
@@ -17,11 +17,13 @@
 package org.killbill.billing.payment.core;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
+import javax.sql.rowset.Joinable;
 
 import org.killbill.automaton.MissingEntryException;
 import org.killbill.automaton.State;
@@ -53,6 +55,7 @@ import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.clock.Clock;
 import org.killbill.commons.locker.GlobalLocker;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.inject.name.Named;
 
@@ -60,6 +63,8 @@ import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NA
 
 public class PluginControlledPaymentProcessor extends ProcessorBase {
 
+    private static final Joiner JOINER = Joiner.on(", ");
+
     private final PluginControlledPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner;
     private final RetryStateMachineHelper retrySMHelper;
     private final CacheControllerDispatcher controllerDispatcher;
@@ -84,7 +89,7 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
     }
 
     public Payment createAuthorization(final boolean isApiPayment, final Account account, final UUID paymentMethodId, @Nullable final UUID paymentId, final BigDecimal amount, final Currency currency, final String paymentExternalKey, final String transactionExternalKey,
-                                             final Iterable<PluginProperty> properties, final String paymentControlPluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+                                             final Iterable<PluginProperty> properties, final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
                                                                 TransactionType.AUTHORIZE,
                                                                 account,
@@ -95,13 +100,13 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                                 amount,
                                                                 currency,
                                                                 properties,
-                                                                paymentControlPluginName,
+                                                                paymentControlPluginNames,
                                                                 callContext, internalCallContext);
     }
 
     public Payment createCapture(final boolean isApiPayment, final Account account, final UUID paymentId, final BigDecimal amount, final Currency currency,
                                        final String transactionExternalKey,
-                                       final Iterable<PluginProperty> properties, final String paymentControlPluginName,
+                                       final Iterable<PluginProperty> properties, final List<String> paymentControlPluginNames,
                                        final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
                                                                 TransactionType.CAPTURE,
@@ -113,13 +118,13 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                                 amount,
                                                                 currency,
                                                                 properties,
-                                                                paymentControlPluginName,
+                                                                paymentControlPluginNames,
                                                                 callContext, internalCallContext);
     }
 
     public Payment createPurchase(final boolean isApiPayment, final Account account, final UUID paymentMethodId, final UUID paymentId, final BigDecimal amount, final Currency currency,
                                         final String paymentExternalKey, final String transactionExternalKey, final Iterable<PluginProperty> properties,
-                                        final String paymentControlPluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+                                        final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
                                                                 TransactionType.PURCHASE,
                                                                 account,
@@ -130,7 +135,7 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                                 amount,
                                                                 currency,
                                                                 properties,
-                                                                paymentControlPluginName,
+                                                                paymentControlPluginNames,
                                                                 callContext, internalCallContext);
     }
 
@@ -151,7 +156,7 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
     }
 
     public Payment createRefund(final boolean isApiPayment, final Account account, final UUID paymentId, final BigDecimal amount, final Currency currency, final String transactionExternalKey,
-                                      final Iterable<PluginProperty> properties, final String paymentControlPluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+                                      final Iterable<PluginProperty> properties, final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
                                                                 TransactionType.REFUND,
                                                                 account,
@@ -162,12 +167,12 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                                 amount,
                                                                 currency,
                                                                 properties,
-                                                                paymentControlPluginName,
+                                                                paymentControlPluginNames,
                                                                 callContext, internalCallContext);
     }
 
     public Payment createCredit(final boolean isApiPayment, final Account account, final UUID paymentMethodId, final UUID paymentId, final BigDecimal amount, final Currency currency, final String paymentExternalKey,
-                                      final String transactionExternalKey, final Iterable<PluginProperty> properties, final String paymentControlPluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+                                      final String transactionExternalKey, final Iterable<PluginProperty> properties, final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
 
         return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
                                                                 TransactionType.CREDIT,
@@ -179,12 +184,12 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                                 amount,
                                                                 currency,
                                                                 properties,
-                                                                paymentControlPluginName,
+                                                                paymentControlPluginNames,
                                                                 callContext, internalCallContext);
     }
 
     public Payment createChargeback(final Account account, final UUID paymentId, final String transactionExternalKey, final BigDecimal amount, final Currency currency,
-                                          final String paymentControlPluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+                                          final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         return pluginControlledPaymentAutomatonRunner.run(true,
                                                                 TransactionType.CHARGEBACK,
                                                                 account,
@@ -195,11 +200,11 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                                 amount,
                                                                 currency,
                                                                 ImmutableList.<PluginProperty>of(),
-                                                                paymentControlPluginName,
+                                                                paymentControlPluginNames,
                                                                 callContext, internalCallContext);
     }
 
-    public void retryPaymentTransaction(final UUID attemptId, final String pluginName, final InternalCallContext internalCallContext) {
+    public void retryPaymentTransaction(final UUID attemptId, final List<String> paymentControlPluginNames, final InternalCallContext internalCallContext) {
         try {
 
             final PaymentAttemptModelDao attempt = paymentDao.getPaymentAttempt(attemptId, internalCallContext);
@@ -223,18 +228,29 @@ public class PluginControlledPaymentProcessor extends ProcessorBase {
                                                              attempt.getAmount(),
                                                              attempt.getCurrency(),
                                                              pluginProperties,
-                                                             pluginName,
+                                                             paymentControlPluginNames,
                                                              callContext,
                                                              internalCallContext);
 
         } catch (AccountApiException e) {
-            log.warn("Failed to retry attempt " + attemptId + " for plugin " + pluginName, e);
+            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
         } catch (PaymentApiException e) {
-            log.warn("Failed to retry attempt " + attemptId + " for plugin " + pluginName, e);
+            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
         } catch (PluginPropertySerializerException e) {
-            log.warn("Failed to retry attempt " + attemptId + " for plugin " + pluginName, e);
+            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
         } catch (MissingEntryException e) {
-            log.warn("Failed to retry attempt " + attemptId + " for plugin " + pluginName, e);
+            log.warn("Failed to retry attempt " + attemptId + toPluginNamesOnError(" for plugins ", paymentControlPluginNames), e);
+        }
+    }
+
+    private String toPluginNamesOnError(final String prefixMessage, final List<String> paymentControlPluginNames) {
+        if (paymentControlPluginNames == null || paymentControlPluginNames.size() == 0) {
+            return "";
         }
+        final StringBuilder tmp = new StringBuilder(prefixMessage)
+                .append("(")
+                .append(JOINER.join(paymentControlPluginNames))
+                .append(")");
+        return tmp.toString();
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
index 32b18a2..3aafe01 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
@@ -47,13 +47,13 @@ public class PaymentStateContext {
     protected PaymentTransactionInfoPlugin paymentInfoPlugin;
     protected BigDecimal amount;
     protected String paymentExternalKey;
+    protected Currency currency;
 
     // Can be updated later via paymentTransactionModelDao (e.g. for auth or purchase)
     protected final UUID paymentId;
     protected final UUID transactionId;
     protected final String paymentTransactionExternalKey;
     protected final Account account;
-    protected final Currency currency;
     protected final TransactionType transactionType;
     protected final boolean shouldLockAccountAndDispatch;
     protected final Iterable<PluginProperty> properties;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlledPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlledPaymentAutomatonRunner.java
index ec4280c..0df1324 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlledPaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlledPaymentAutomatonRunner.java
@@ -17,6 +17,7 @@
 package org.killbill.billing.payment.core.sm;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 
@@ -81,23 +82,23 @@ public class PluginControlledPaymentAutomatonRunner extends PaymentAutomatonRunn
     public Payment run(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
                        @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
                        @Nullable final BigDecimal amount, @Nullable final Currency currency,
-                       final Iterable<PluginProperty> properties, @Nullable final String pluginName,
+                       final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
                        final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         return run(retrySMHelper.getInitialState(), isApiPayment, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
-                   amount, currency, properties, pluginName, callContext, internalCallContext);
+                   amount, currency, properties, paymentControlPluginNames, callContext, internalCallContext);
     }
 
     public Payment run(final State state, final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
                        @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
                        @Nullable final BigDecimal amount, @Nullable final Currency currency,
-                       final Iterable<PluginProperty> properties, @Nullable final String pluginName,
+                       final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
                        final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
 
         final RetryablePaymentStateContext paymentStateContext = createContext(isApiPayment, transactionType, account, paymentMethodId,
                                                                                      paymentId, paymentExternalKey,
                                                                                      paymentTransactionExternalKey,
                                                                                      amount, currency,
-                                                                                     properties, pluginName, callContext, internalCallContext);
+                                                                                     properties, paymentControlPluginNames, callContext, internalCallContext);
         try {
 
             final OperationCallback callback = createOperationCallback(transactionType, paymentStateContext);
@@ -149,8 +150,8 @@ public class PluginControlledPaymentAutomatonRunner extends PaymentAutomatonRunn
     RetryablePaymentStateContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
                                                @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
                                                @Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> properties,
-                                               final String pluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
-        return new RetryablePaymentStateContext(pluginName, isApiPayment, paymentId, paymentExternalKey, paymentTransactionExternalKey, transactionType, account,
+                                               final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+        return new RetryablePaymentStateContext(paymentControlPluginNames, isApiPayment, paymentId, paymentExternalKey, paymentTransactionExternalKey, transactionType, account,
                                                 paymentMethodId, amount, currency, properties, internalCallContext, callContext);
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryablePaymentStateContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryablePaymentStateContext.java
index 678d987..7461aca 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryablePaymentStateContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryablePaymentStateContext.java
@@ -17,6 +17,7 @@
 package org.killbill.billing.payment.core.sm;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
@@ -38,15 +39,15 @@ import com.google.common.collect.Iterables;
 public class RetryablePaymentStateContext extends PaymentStateContext {
 
     private DateTime retryDate;
-    private String pluginName;
+    private List<String> paymentControlPluginNames;
     private Payment result;
 
-    public RetryablePaymentStateContext(@Nullable final String pluginName, final boolean isApiPayment, @Nullable final UUID paymentId, final String paymentExternalKey,
+    public RetryablePaymentStateContext(@Nullable final List<String> paymentControlPluginNames, final boolean isApiPayment, @Nullable final UUID paymentId, final String paymentExternalKey,
                                         @Nullable final String paymentTransactionExternalKey, final TransactionType transactionType,
                                         final Account account, @Nullable final UUID paymentMethodId, final BigDecimal amount, final Currency currency,
                                         final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext, final CallContext callContext) {
         super(isApiPayment, paymentId, null, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
-        this.pluginName = pluginName;
+        this.paymentControlPluginNames = paymentControlPluginNames;
     }
 
     public DateTime getRetryDate() {
@@ -57,12 +58,8 @@ public class RetryablePaymentStateContext extends PaymentStateContext {
         this.retryDate = retryDate;
     }
 
-    public String getPluginName() {
-        return pluginName;
-    }
-
-    public void setPluginName(final String pluginName) {
-        this.pluginName = pluginName;
+    public List<String> getPaymentControlPluginNames() {
+        return paymentControlPluginNames;
     }
 
     public Payment getResult() {
@@ -77,6 +74,10 @@ public class RetryablePaymentStateContext extends PaymentStateContext {
         this.amount = adjustedAmount;
     }
 
+    public void setCurrency(final Currency currency) {
+        this.currency = currency;
+    }
+
     public PaymentTransaction getCurrentTransaction() {
         if (result == null || result.getTransactions() == null) {
             return null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryCompletionOperationCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryCompletionOperationCallback.java
index 91e1f92..4dab872 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryCompletionOperationCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryCompletionOperationCallback.java
@@ -60,7 +60,7 @@ public class RetryCompletionOperationCallback extends RetryOperationCallback {
                                                                                                             retryablePaymentStateContext.isApiPayment(),
                                                                                                             paymentStateContext.callContext);
 
-                onCompletion(retryablePaymentStateContext.getPluginName(), updatedPaymentControlContext);
+                onCompletion(retryablePaymentStateContext.getPaymentControlPluginNames(), updatedPaymentControlContext);
                 return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS);
             }
         });
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryEnteringStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryEnteringStateCallback.java
index 0b7889f..1b9554d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryEnteringStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryEnteringStateCallback.java
@@ -51,7 +51,7 @@ public class RetryEnteringStateCallback implements EnteringStateCallback {
 
         if ("RETRIED".equals(state.getName())) {
             retryServiceScheduler.scheduleRetry(ObjectType.PAYMENT_ATTEMPT, attempt.getId(), attempt.getId(),
-                                                paymentStateContext.getPluginName(), paymentStateContext.getRetryDate());
+                                                paymentStateContext.getPaymentControlPluginNames(), paymentStateContext.getRetryDate());
         }
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryLeavingStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryLeavingStateCallback.java
index 4a4b2ed..cd9f2c3 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryLeavingStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryLeavingStateCallback.java
@@ -73,7 +73,7 @@ public class RetryLeavingStateCallback implements LeavingStateCallback {
                                                                                   utcNow, utcNow, stateContext.getPaymentExternalKey(), null,
                                                                                   stateContext.paymentTransactionExternalKey, transactionType, initialState.getName(),
                                                                                   stateContext.getAmount(), stateContext.getCurrency(),
-                                                                                  stateContext.getPluginName(), serializedProperties);
+                                                                                  stateContext.getPaymentControlPluginNames(), serializedProperties);
 
                 retryablePaymentAutomatonRunner.paymentDao.insertPaymentAttemptWithProperties(attempt, stateContext.internalCallContext);
                 stateContext.setAttemptId(attempt.getId());
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryOperationCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryOperationCallback.java
index 580e180..7d9acdf 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryOperationCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/RetryOperationCallback.java
@@ -17,6 +17,7 @@
 package org.killbill.billing.payment.core.sm;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
@@ -94,7 +95,7 @@ public abstract class RetryOperationCallback extends OperationCallbackBase<Payme
 
                 final PriorPaymentControlResult pluginResult;
                 try {
-                    pluginResult = getPluginResult(retryablePaymentStateContext.getPluginName(), paymentControlContext);
+                    pluginResult = getPluginResult(retryablePaymentStateContext.getPaymentControlPluginNames(), paymentControlContext);
                     if (pluginResult.isAborted()) {
                         // Transition to ABORTED
                         return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.EXCEPTION);
@@ -107,10 +108,7 @@ public abstract class RetryOperationCallback extends OperationCallbackBase<Payme
                 final boolean success;
                 try {
                     // Adjust amount with value returned by plugin if necessary
-                    if (paymentStateContext.getAmount() == null ||
-                        (pluginResult.getAdjustedAmount() != null && pluginResult.getAdjustedAmount().compareTo(paymentStateContext.getAmount()) != 0)) {
-                        ((RetryablePaymentStateContext) paymentStateContext).setAmount(pluginResult.getAdjustedAmount());
-                    }
+                    adjustStateContextValues(paymentStateContext, pluginResult);
 
                     final Payment result = doCallSpecificOperationCallback();
                     ((RetryablePaymentStateContext) paymentStateContext).setResult(result);
@@ -134,7 +132,7 @@ public abstract class RetryOperationCallback extends OperationCallbackBase<Payme
                                                                                                                     retryablePaymentStateContext.isApiPayment(),
                                                                                                                     paymentStateContext.callContext);
 
-                        onCompletion(retryablePaymentStateContext.getPluginName(), updatedPaymentControlContext);
+                        onCompletion(retryablePaymentStateContext.getPaymentControlPluginNames(), updatedPaymentControlContext);
                         return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS);
                     } else {
                         throw new OperationException(null, getOperationResultAndSetContext(retryablePaymentStateContext, paymentControlContext));
@@ -177,12 +175,30 @@ public abstract class RetryOperationCallback extends OperationCallbackBase<Payme
         return new OperationException(e, getOperationResultOnException(paymentStateContext));
     }
 
-    protected void onCompletion(final String pluginName, final PaymentControlContext paymentControlContext) {
-        final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
-        try {
-            plugin.onSuccessCall(paymentControlContext);
-        } catch (final PaymentControlApiException e) {
-            logger.warn("Plugin " + pluginName + " failed to complete onCompletion call for " + paymentControlContext.getPaymentExternalKey(), e);
+    protected void onCompletion(final List<String> paymentControlPluginNames, final PaymentControlContext paymentControlContext) {
+        for (String pluginName : paymentControlPluginNames) {
+            final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
+            if (plugin != null) {
+                try {
+                    plugin.onSuccessCall(paymentControlContext);
+                } catch (final PaymentControlApiException e) {
+                    logger.warn("Plugin " + pluginName + " failed to complete onCompletion call for " + paymentControlContext.getPaymentExternalKey(), e);
+                }
+            }
+        }
+    }
+
+    private final void adjustStateContextValues(final PaymentStateContext inputContext, final PriorPaymentControlResult pluginResult) {
+
+        final RetryablePaymentStateContext input = (RetryablePaymentStateContext) inputContext;
+        if (pluginResult.getAdjustedAmount() != null) {
+            input.setAmount(pluginResult.getAdjustedAmount());
+        }
+        if (pluginResult.getAdjustedCurrency() != null) {
+            input.setCurrency(pluginResult.getAdjustedCurrency());
+        }
+        if (pluginResult.getAdjustedPaymentMethodId() != null) {
+            input.setPaymentMethodId(pluginResult.getAdjustedPaymentMethodId());
         }
     }
 
@@ -192,14 +208,43 @@ public abstract class RetryOperationCallback extends OperationCallbackBase<Payme
         return operationResult;
     }
 
-    private PriorPaymentControlResult getPluginResult(final String pluginName, final PaymentControlContext paymentControlContext) throws PaymentControlApiException {
-        final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
-        final PriorPaymentControlResult result = plugin.priorCall(paymentControlContext);
-        return result;
+    private PriorPaymentControlResult getPluginResult(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;
+
+        PaymentControlContext inputPaymentControlContext = paymentControlContextArg;
+
+        for (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 payment plugin control {} when fetching results", pluginName);
+                continue;
+            }
+            prevResult = plugin.priorCall(inputPaymentControlContext);
+            if (prevResult.isAborted()) {
+                break;
+            }
+            inputPaymentControlContext = new DefaultPaymentControlContext(paymentStateContext.getAccount(),
+                                                                          prevResult.getAdjustedPaymentMethodId() != null ? prevResult.getAdjustedPaymentMethodId() : inputPaymentControlContext.getPaymentMethodId(),
+                                                                          retryablePaymentStateContext.getAttemptId(),
+                                                                          paymentStateContext.getPaymentId(),
+                                                                          paymentStateContext.getPaymentExternalKey(),
+                                                                          paymentStateContext.getPaymentTransactionExternalKey(),
+                                                                          paymentStateContext.getTransactionType(),
+                                                                          prevResult.getAdjustedAmount() != null ? prevResult.getAdjustedAmount() : inputPaymentControlContext.getAmount(),
+                                                                          prevResult.getAdjustedCurrency() != null ? prevResult.getAdjustedCurrency() : inputPaymentControlContext.getCurrency(),
+                                                                          paymentStateContext.getProperties(),
+                                                                          retryablePaymentStateContext.isApiPayment(),
+                                                                          paymentStateContext.callContext);
+
+        }
+        return prevResult;
     }
 
     private OperationResult getOperationResultAndSetContext(final RetryablePaymentStateContext retryablePaymentStateContext, final PaymentControlContext paymentControlContext) {
-        final DateTime retryDate = getNextRetryDate(retryablePaymentStateContext.getPluginName(), paymentControlContext);
+        final DateTime retryDate = getNextRetryDate(retryablePaymentStateContext.getPaymentControlPluginNames(), paymentControlContext);
         if (retryDate != null) {
             ((RetryablePaymentStateContext) paymentStateContext).setRetryDate(retryDate);
             return OperationResult.FAILURE;
@@ -208,15 +253,25 @@ public abstract class RetryOperationCallback extends OperationCallbackBase<Payme
         }
     }
 
-    private DateTime getNextRetryDate(final String pluginName, final PaymentControlContext paymentControlContext) {
-        try {
+    private DateTime getNextRetryDate(final List<String> paymentControlPluginNames, final PaymentControlContext paymentControlContext) {
+        DateTime candidate = null;
+        for (String pluginName : paymentControlPluginNames) {
             final PaymentControlPluginApi plugin = paymentControlPluginRegistry.getServiceForName(pluginName);
-            final FailureCallResult result = plugin.onFailureCall(paymentControlContext);
-            return result.getNextRetryDate();
-        } catch (final PaymentControlApiException e) {
-            logger.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + paymentControlContext.getPaymentExternalKey(), e);
-            return null;
+            if (plugin != null) {
+                try {
+                    final FailureCallResult result = plugin.onFailureCall(paymentControlContext);
+                    if (candidate == null) {
+                        candidate = result.getNextRetryDate();
+                    } else if (result.getNextRetryDate() != null) {
+                        candidate = candidate.compareTo(result.getNextRetryDate()) > 0 ? result.getNextRetryDate() : candidate;
+                    }
+                } catch (final PaymentControlApiException e) {
+                    logger.warn("Plugin " + pluginName + " failed to return next retryDate for payment " + paymentControlContext.getPaymentExternalKey(), e);
+                    return candidate;
+                }
+            }
         }
+        return candidate;
     }
 
     public static class DefaultPaymentControlContext extends DefaultCallContext implements PaymentControlContext {
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java
index fdc1354..85d218a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java
@@ -31,8 +31,13 @@ import org.killbill.billing.util.entity.Entity;
 import org.killbill.billing.util.entity.dao.EntityModelDao;
 import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
 
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
 public class PaymentAttemptModelDao extends EntityModelDaoBase implements EntityModelDao<Entity> {
 
+    private static final Joiner JOINER = Joiner.on(",");
+
     private UUID accountId;
     private UUID paymentMethodId;
     private String paymentExternalKey;
@@ -66,9 +71,9 @@ public class PaymentAttemptModelDao extends EntityModelDaoBase implements Entity
 
     public PaymentAttemptModelDao(final UUID accountId, final UUID paymentMethodId, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
                                   final String paymentExternalKey, final UUID transactionId, final String transactionExternalKey, final TransactionType transactionType, final String stateName,
-                                  final BigDecimal amount, final Currency currency, final String pluginName,  final byte [] pluginProperties) {
+                                  final BigDecimal amount, final Currency currency, final List<String> paymentControlPluginNames,  final byte [] pluginProperties) {
         this(accountId, paymentMethodId, UUID.randomUUID(), createdDate, updatedDate, paymentExternalKey, transactionId, transactionExternalKey, transactionType, stateName,
-             amount, currency, pluginName, pluginProperties);
+             amount, currency, toPluginNames(paymentControlPluginNames), pluginProperties);
     }
 
     public String getPaymentExternalKey() {
@@ -159,6 +164,14 @@ public class PaymentAttemptModelDao extends EntityModelDaoBase implements Entity
         this.currency = currency;
     }
 
+    public final List<String> toPaymentControlPluginNames() {
+        if (pluginName == null) {
+            return ImmutableList.<String>of();
+        }
+        final String [] parts = pluginName.split(",");
+        return ImmutableList.<String>copyOf(parts);
+    }
+
     @Override
     public boolean equals(final Object o) {
         if (this == o) {
@@ -233,4 +246,7 @@ public class PaymentAttemptModelDao extends EntityModelDaoBase implements Entity
         return TableName.PAYMENT_ATTEMPT_HISTORY;
     }
 
+    private static final String toPluginNames(final List<String> paymentControlPluginNames) {
+        return paymentControlPluginNames == null || paymentControlPluginNames.size() == 0 ? null : JOINER.join(paymentControlPluginNames);
+    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentControlProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentControlProviderPlugin.java
index 33ddf53..e6a2fc4 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentControlProviderPlugin.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentControlProviderPlugin.java
@@ -33,7 +33,7 @@ public class DefaultNoOpPaymentControlProviderPlugin implements PaymentControlPl
 
     @Override
     public PriorPaymentControlResult priorCall(final PaymentControlContext retryPluginContext) throws PaymentControlApiException {
-        return new DefaultPriorPaymentControlResult(isRetryAborted, null);
+        return new DefaultPriorPaymentControlResult(isRetryAborted, null, null, null);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPlugin.java
index 575c1e5..2d82d9f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPlugin.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentControlProviderPlugin.java
@@ -30,7 +30,7 @@ public class DefaultPaymentControlProviderPlugin implements PaymentControlPlugin
 
     @Override
     public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext) throws PaymentControlApiException {
-        return new DefaultPriorPaymentControlResult(false, null);
+        return new DefaultPriorPaymentControlResult(false, null, null, null);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
index b72d4c7..659d344 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
@@ -19,6 +19,7 @@
 package org.killbill.billing.payment.retry;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -70,7 +71,7 @@ public abstract class BaseRetryService implements RetryService {
                                                                               }
                                                                               final PaymentRetryNotificationKey key = (PaymentRetryNotificationKey) notificationKey;
                                                                               final InternalCallContext callContext = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, PAYMENT_RETRY_SERVICE, CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
-                                                                              retryPaymentTransaction(key.getAttemptId(), key.getPluginName(), callContext);
+                                                                              retryPaymentTransaction(key.getAttemptId(), key.getPaymentControlPluginNames(), callContext);
                                                                           }
                                                                       }
                                                                      );
@@ -104,17 +105,17 @@ public abstract class BaseRetryService implements RetryService {
             this.internalCallContextFactory = internalCallContextFactory;
         }
 
-        public boolean scheduleRetry(final ObjectType objectType, final UUID objectId, final UUID attemptId, final String pluginName, final DateTime timeOfRetry) {
-            return scheduleRetryInternal(objectType, objectId, attemptId, pluginName, timeOfRetry, null);
+        public boolean scheduleRetry(final ObjectType objectType, final UUID objectId, final UUID attemptId, final List<String> paymentControlPluginNames, final DateTime timeOfRetry) {
+            return scheduleRetryInternal(objectType, objectId, attemptId, paymentControlPluginNames, timeOfRetry, null);
         }
 
 
-        private boolean scheduleRetryInternal(final ObjectType objectType, final UUID objectId, final UUID attemptId, final String pluginName, final DateTime timeOfRetry, final EntitySqlDaoWrapperFactory<EntitySqlDao> transactionalDao) {
+        private boolean scheduleRetryInternal(final ObjectType objectType, final UUID objectId, final UUID attemptId, final List<String> paymentControlPluginNames, final DateTime timeOfRetry, final EntitySqlDaoWrapperFactory<EntitySqlDao> transactionalDao) {
             final InternalCallContext context = createCallContextFromPaymentId(objectType, objectId);
 
             try {
                 final NotificationQueue retryQueue = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, getQueueName());
-                final NotificationEvent key = new PaymentRetryNotificationKey(attemptId, pluginName);
+                final NotificationEvent key = new PaymentRetryNotificationKey(attemptId, paymentControlPluginNames);
                 if (retryQueue != null) {
                     if (transactionalDao == null) {
                         retryQueue.recordFutureNotification(timeOfRetry, key, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java
index dc2e457..07574be 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultPriorPaymentControlResult.java
@@ -17,21 +17,30 @@
 package org.killbill.billing.payment.retry;
 
 import java.math.BigDecimal;
+import java.util.UUID;
 
+import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.retry.plugin.api.PriorPaymentControlResult;
 
 public class DefaultPriorPaymentControlResult implements PriorPaymentControlResult {
 
     private final boolean isAborted;
     private final BigDecimal adjustedRetryAmount;
+    private final Currency adjustedCurrency;
+    private final UUID adjustedPaymentMethodId;
 
-    public DefaultPriorPaymentControlResult(final boolean isAborted, final BigDecimal adjustedRetryAmount) {
+    public DefaultPriorPaymentControlResult(final boolean isAborted,
+                                            final BigDecimal adjustedRetryAmount,
+                                            final Currency adjustedCurrency,
+                                            final UUID adjustedPaymentMethodId) {
         this.isAborted = isAborted;
         this.adjustedRetryAmount = adjustedRetryAmount;
+        this.adjustedCurrency = adjustedCurrency;
+        this.adjustedPaymentMethodId = adjustedPaymentMethodId;
     }
 
     public DefaultPriorPaymentControlResult(final boolean isAborted) {
-        this(isAborted, null);
+        this(isAborted, null, null, null);
     }
 
     @Override
@@ -43,4 +52,13 @@ public class DefaultPriorPaymentControlResult implements PriorPaymentControlResu
     public BigDecimal getAdjustedAmount() {
         return adjustedRetryAmount;
     }
+
+    @Override
+    public Currency getAdjustedCurrency() {
+        return adjustedCurrency;
+    }
+    @Override
+    public UUID getAdjustedPaymentMethodId() {
+        return adjustedPaymentMethodId;
+    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultRetryService.java
index f08f43a..b7e94a5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/DefaultRetryService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/DefaultRetryService.java
@@ -16,6 +16,7 @@
 
 package org.killbill.billing.payment.retry;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.killbill.billing.callcontext.InternalCallContext;
@@ -45,8 +46,8 @@ public class DefaultRetryService extends BaseRetryService implements RetryServic
 
 
     @Override
-    public void retryPaymentTransaction(final UUID attemptId, final String pluginName, final InternalCallContext context) {
-        processor.retryPaymentTransaction(attemptId, pluginName, context);
+    public void retryPaymentTransaction(final UUID attemptId, final List<String> paymentControlPluginNames, final InternalCallContext context) {
+        processor.retryPaymentTransaction(attemptId, paymentControlPluginNames, context);
     }
 
     public static class DefaultRetryServiceScheduler extends RetryServiceScheduler {
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java b/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java
index 50a3155..d2569fd 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java
@@ -16,6 +16,7 @@
 
 package org.killbill.billing.payment.retry;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.killbill.notificationq.api.NotificationEvent;
@@ -26,20 +27,20 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 public class PaymentRetryNotificationKey implements NotificationEvent {
 
     private final UUID attemptId;
-    private final String pluginName;
+    private final List<String> paymentControlPluginNames;
 
     @JsonCreator
     public PaymentRetryNotificationKey(@JsonProperty("attemptId") UUID attemptId,
-                                       @JsonProperty("pluginName") String pluginName) {
+                                       @JsonProperty("paymentControlPluginNames") List<String> paymentControlPluginNames) {
         this.attemptId = attemptId;
-        this.pluginName = pluginName;
+        this.paymentControlPluginNames = paymentControlPluginNames;
     }
 
     public UUID getAttemptId() {
         return attemptId;
     }
 
-    public String getPluginName() {
-        return pluginName;
+    public List<String> getPaymentControlPluginNames() {
+        return paymentControlPluginNames;
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java
index 9864e0a..ea320f1 100644
--- a/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java
@@ -18,6 +18,7 @@
 
 package org.killbill.billing.payment.retry;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.killbill.billing.callcontext.InternalCallContext;
@@ -35,5 +36,5 @@ public interface RetryService {
 
     public String getQueueName();
 
-    public void retryPaymentTransaction(final UUID attemptId, String pluginName, final InternalCallContext context);
+    public void retryPaymentTransaction(final UUID attemptId, List<String> paymentControlPluginNames, final InternalCallContext context);
 }
diff --git a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
index 5689d8f..b184656 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
+++ b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
@@ -14,7 +14,7 @@ CREATE TABLE payment_attempts (
     state_name varchar(32) NOT NULL,
     amount numeric(15,9),
     currency char(3),
-    plugin_name varchar(50) NOT NULL,
+    plugin_name varchar(1024) NOT NULL,
     plugin_properties blob(8194),
     created_by varchar(50) NOT NULL,
     created_date datetime NOT NULL,
@@ -45,7 +45,7 @@ CREATE TABLE payment_attempt_history (
     state_name varchar(32) NOT NULL,
     amount numeric(15,9),
     currency char(3),
-    plugin_name varchar(50) NOT NULL,
+    plugin_name varchar(1024) NOT NULL,
     plugin_properties blob(8194),
     change_type char(6) NOT NULL,
     created_by varchar(50) NOT NULL,
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index ce075e3..38eb6ad 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -57,8 +57,8 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         }
 
         @Override
-        public String getPaymentControlPluginName() {
-            return InvoicePaymentControlPluginApi.PLUGIN_NAME;
+        public List<String> getPaymentControlPluginNames() {
+            return ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
         }
     };
 
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
index 22454ae..fd55a2c 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
@@ -57,8 +57,8 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
             return false;
         }
         @Override
-        public String getPaymentControlPluginName() {
-            return InvoicePaymentControlPluginApi.PLUGIN_NAME;
+        public List<String> getPaymentControlPluginNames() {
+            return ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
         }
     };
 
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 992e210..918c9b1 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
@@ -17,6 +17,7 @@
 package org.killbill.billing.payment.core.sm;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 
@@ -48,6 +49,8 @@ import org.killbill.bus.api.PersistentBus;
 import org.killbill.clock.Clock;
 import org.killbill.commons.locker.GlobalLocker;
 
+import com.google.common.collect.ImmutableList;
+
 import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
 import static org.killbill.billing.payment.glue.PaymentModule.RETRYABLE_NAMED;
 
@@ -77,10 +80,10 @@ public class MockRetryablePaymentAutomatonRunner extends PluginControlledPayment
                                                      @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
                                                      @Nullable final BigDecimal amount, @Nullable final Currency currency,
                                                      final Iterable<PluginProperty> properties,
-                                                     final String pluginName, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+                                                     final List<String> pluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
         if (context == null) {
             return super.createContext(isApiPayment, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
-                                       amount, currency, properties, pluginName, callContext, internalCallContext);
+                                       amount, currency, properties, pluginNames, callContext, internalCallContext);
         } else {
             return context;
         }
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 5acf9df..71cb99e 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
@@ -167,7 +167,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
                 eventBus);
 
         paymentStateContext =
-                new RetryablePaymentStateContext(MockPaymentControlProviderPlugin.PLUGIN_NAME,
+                new RetryablePaymentStateContext(ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME),
                                                        true,
                                                        null,
                                                        paymentExternalKey,
@@ -632,7 +632,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
                                                            new PaymentTransactionModelDao(transactionId, attempt.getId(), paymentTransactionExternalKey, utcNow, utcNow, paymentId, TransactionType.AUTHORIZE, utcNow, TransactionStatus.PAYMENT_FAILURE, amount, currency, "bla", "foo"),
                                                            internalCallContext);
 
-        processor.retryPaymentTransaction(attempt.getId(), MockPaymentControlProviderPlugin.PLUGIN_NAME, internalCallContext);
+        processor.retryPaymentTransaction(attempt.getId(), ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME), internalCallContext);
 
         final List<PaymentAttemptModelDao> pas = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransactionExternalKey, internalCallContext);
         assertEquals(pas.size(), 2);
@@ -676,7 +676,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
                                                            internalCallContext
                                                           );
 
-        processor.retryPaymentTransaction(attempt.getId(), MockPaymentControlProviderPlugin.PLUGIN_NAME, internalCallContext);
+        processor.retryPaymentTransaction(attempt.getId(), ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME), internalCallContext);
 
         final List<PaymentAttemptModelDao> pas = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransactionExternalKey, internalCallContext);
         assertEquals(pas.size(), 2);
@@ -724,7 +724,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
                                                                internalCallContext
                                                               );
 
-            processor.retryPaymentTransaction(attempt.getId(), MockPaymentControlProviderPlugin.PLUGIN_NAME, internalCallContext);
+            processor.retryPaymentTransaction(attempt.getId(), ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME), internalCallContext);
 
             final List<PaymentAttemptModelDao> pas = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransactionExternalKey, internalCallContext);
             assertEquals(pas.size(), 2);
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
index 69045eb..67679de 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -64,7 +64,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         final byte[] serialized = PluginPropertySerializer.serialize(properties);
         final PaymentAttemptModelDao attempt = new PaymentAttemptModelDao(UUID.randomUUID(), UUID.randomUUID(), clock.getUTCNow(), clock.getUTCNow(),
                                                                           paymentExternalKey, transactionId, transactionExternalKey, transactionType, stateName,
-                                                                          BigDecimal.ZERO, Currency.ALL, pluginName, serialized);
+                                                                          BigDecimal.ZERO, Currency.ALL, ImmutableList.<String>of(pluginName), serialized);
 
         PaymentAttemptModelDao savedAttempt = paymentDao.insertPaymentAttemptWithProperties(attempt, internalCallContext);
         assertEquals(savedAttempt.getTransactionExternalKey(), transactionExternalKey);
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java
index fc0b8a3..6a6e347 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentControlProviderPlugin.java
@@ -44,7 +44,7 @@ public class MockPaymentControlProviderPlugin implements PaymentControlPluginApi
 
     @Override
     public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext) throws PaymentControlApiException{
-        return new DefaultPriorPaymentControlResult(isAborted, null);
+        return new DefaultPriorPaymentControlResult(isAborted, null, null, null);
     }
 
     @Override
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index 3281965..485e449 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -48,6 +48,7 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
@@ -62,8 +63,8 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
         }
 
         @Override
-        public String getPaymentControlPluginName() {
-            return InvoicePaymentControlPluginApi.PLUGIN_NAME;
+        public List<String> getPaymentControlPluginNames() {
+            return ImmutableList.of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
         }
     };
 
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
index 9b9537e..54744ef 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
@@ -144,7 +144,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
         final String transactionExternalKey = UUID.randomUUID().toString();
         try {
             pluginControlledPaymentProcessor.createPurchase(false, account, account.getPaymentMethodId(), null, amount, Currency.USD, paymentExternalKey, transactionExternalKey,
-                                                            createPropertiesForInvoice(invoice), InvoicePaymentControlPluginApi.PLUGIN_NAME, callContext, internalCallContext);
+                                                            createPropertiesForInvoice(invoice), ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME), callContext, internalCallContext);
         } catch (final PaymentApiException e) {
             failed = true;
         }
diff --git a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
index 6100e6d..91f75c0 100644
--- a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
@@ -80,6 +80,11 @@ public interface PaymentConfig extends KillbillConfig {
     @Description("Rate at which janitor tasks are scheduled")
     public TimeSpan getJanitorRunningRate();
 
+    @Config("org.killbill.payment.control.plugin")
+    @Default("__INVOICE_PAYMENT_CONTROL_PLUGIN__")
+    @Description("Whether the payment subsystem is off")
+    public List<String> getPaymentControlPluginNames();
+
     @Config("org.killbill.payment.off")
     @Default("false")
     @Description("Whether the payment subsystem is off")