killbill-memoizeit

Changes

Details

diff --git a/api/src/main/java/com/ning/billing/beatrix/bus/api/ExtBusEventType.java b/api/src/main/java/com/ning/billing/beatrix/bus/api/ExtBusEventType.java
index ac4fe08..c4a241a 100644
--- a/api/src/main/java/com/ning/billing/beatrix/bus/api/ExtBusEventType.java
+++ b/api/src/main/java/com/ning/billing/beatrix/bus/api/ExtBusEventType.java
@@ -23,8 +23,10 @@ public enum ExtBusEventType {
     ACCOUNT_CREATION,
     ACCOUNT_CHANGE,
     SUBSCRIPTION_CREATION,
+    SUBSCRIPTION_PHASE,
     SUBSCRIPTION_CHANGE,
     SUBSCRIPTION_CANCEL,
+    SUBSCRIPTION_UNCANCEL,
     OVERDUE_CHANGE,
     INVOICE_CREATION,
     INVOICE_ADJUSTMENT,
diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java b/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
index 427e35b..1c5d83b 100644
--- a/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
@@ -52,6 +52,17 @@ public interface PaymentApi {
 
 
     /**
+     *
+     * @param account   the account
+     * @param paymentId the payment id
+     * @param context
+     * @return
+     * @throws PaymentApiException
+     */
+    public Payment retryPayment(Account account, UUID paymentId, CallContext context)
+            throws PaymentApiException;
+
+    /**
      * Create a refund for a given payment. The associated invoice is not adjusted.
      *
      * @param account      account to refund
diff --git a/beatrix/src/main/java/com/ning/billing/beatrix/extbus/BeatrixListener.java b/beatrix/src/main/java/com/ning/billing/beatrix/extbus/BeatrixListener.java
index 39356fb..d716c72 100644
--- a/beatrix/src/main/java/com/ning/billing/beatrix/extbus/BeatrixListener.java
+++ b/beatrix/src/main/java/com/ning/billing/beatrix/extbus/BeatrixListener.java
@@ -111,12 +111,18 @@ public class BeatrixListener {
             objectType = ObjectType.SUBSCRIPTION;
             objectId = realEventST.getSubscriptionId();
             if (realEventST.getTransitionType() == SubscriptionTransitionType.CREATE ||
-                    realEventST.getTransitionType() == SubscriptionTransitionType.RE_CREATE) {
+                    realEventST.getTransitionType() == SubscriptionTransitionType.RE_CREATE ||
+                    realEventST.getTransitionType() == SubscriptionTransitionType.TRANSFER ||
+                    realEventST.getTransitionType() == SubscriptionTransitionType.MIGRATE_ENTITLEMENT) {
                 eventBusType = ExtBusEventType.SUBSCRIPTION_CREATION;
             } else if (realEventST.getTransitionType() == SubscriptionTransitionType.CANCEL) {
                 eventBusType = ExtBusEventType.SUBSCRIPTION_CANCEL;
+            } else if (realEventST.getTransitionType() == SubscriptionTransitionType.PHASE) {
+                eventBusType = ExtBusEventType.SUBSCRIPTION_PHASE;
             } else if (realEventST.getTransitionType() == SubscriptionTransitionType.CHANGE) {
                 eventBusType = ExtBusEventType.SUBSCRIPTION_CHANGE;
+            } else if (realEventST.getTransitionType() == SubscriptionTransitionType.UNCANCEL) {
+                eventBusType = ExtBusEventType.SUBSCRIPTION_UNCANCEL;
             }
             break;
 
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
index f8d1f69..5c9a0d1 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
@@ -21,6 +21,8 @@ import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.ning.billing.ErrorCode;
 import com.ning.billing.catalog.api.Catalog;
@@ -128,7 +130,6 @@ public class DefaultEntitlementTransferApi extends EntitlementApiBase implements
                 }
                 break;
             case CANCEL:
-            case UNCANCEL:
                 break;
 
             default:
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
index 5841a98..f29646e 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
@@ -383,11 +383,23 @@ public class SubscriptionData extends EntityBase implements Subscription {
         if (transitions == null || event == null) {
             return null;
         }
+        SubscriptionTransitionData prev = null;
         for (final SubscriptionTransition cur : transitions) {
-            if (((SubscriptionTransitionData) cur).getId().equals(event.getId())) {
-                final SubscriptionTransitionData withSeq = new SubscriptionTransitionData((SubscriptionTransitionData)cur, seqId);
+            final SubscriptionTransitionData curData = (SubscriptionTransitionData) cur;
+            if (curData.getId().equals(event.getId())) {
+
+                final SubscriptionTransitionData withSeq = new SubscriptionTransitionData(curData, seqId);
                 return withSeq;
             }
+            if (curData.getTotalOrdering() < event.getTotalOrdering()) {
+                prev = curData;
+            }
+        }
+        // Since UNCANCEL are not part of the transitions, we compute a new 'UNCANCEL' transition based on the event right before that UNCANCEL
+        // This is used to be able to send a bus event for uncancellation
+        if (prev != null && event.getType() == EventType.API_USER && ((ApiEvent) event).getEventType() == ApiEventType.UNCANCEL) {
+             final SubscriptionTransitionData withSeq = new SubscriptionTransitionData((SubscriptionTransitionData)prev, EventType.API_USER, ApiEventType.UNCANCEL, seqId);
+            return withSeq;
         }
         return null;
     }
@@ -587,7 +599,6 @@ public class SubscriptionData extends EntityBase implements Subscription {
                     nextPhaseName = null;
                     break;
                 case UNCANCEL:
-                    break;
                 default:
                     throw new EntitlementError(String.format(
                             "Unexpected UserEvent type = %s", userEV
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
index 2cadced..d4d44aa 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
@@ -102,12 +102,17 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
     }
 
     public SubscriptionTransitionData(final SubscriptionTransitionData input, int remainingEventsForUserOperation) {
+       this(input, input.getEventType(), input.getApiEventType(), remainingEventsForUserOperation);
+    }
+
+    public SubscriptionTransitionData(final SubscriptionTransitionData input, final EventType eventType,
+                                      final ApiEventType apiEventType, int remainingEventsForUserOperation) {
         super();
         this.eventId = input.getId();
         this.subscriptionId = input.getSubscriptionId();
         this.bundleId = input.getBundleId();
-        this.eventType = input.getEventType();
-        this.apiEventType = input.getApiEventType();
+        this.eventType = eventType;
+        this.apiEventType = apiEventType;
         this.requestedTransitionTime = input.getRequestedTransitionTime();
         this.effectiveTransitionTime = input.getEffectiveTransitionTime();
         this.previousEventId = input.getPreviousEventId();
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionDataIterator.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionDataIterator.java
index 9da28ea..e2e4154 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionDataIterator.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionDataIterator.java
@@ -98,10 +98,8 @@ public class SubscriptionTransitionDataIterator implements Iterator<Subscription
     }
 
     private boolean shouldSkipForBillingEvents(final SubscriptionTransitionData input) {
-        // Junction system knows about all events except for MIGRATE_ENTITLEMENT and UNCANCEL-- which is a NO event as it undo
-        // something that should have happened in the future.
-        return (input.getTransitionType() == SubscriptionTransitionType.MIGRATE_ENTITLEMENT ||
-                input.getTransitionType() == SubscriptionTransitionType.UNCANCEL);
+        // Junction system knows about all events except for MIGRATE_ENTITLEMENT
+        return input.getTransitionType() == SubscriptionTransitionType.MIGRATE_ENTITLEMENT;
     }
 
 
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java
index f10b36b..8128583 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java
@@ -324,8 +324,14 @@ public class DefaultEntitlementDao implements EntitlementDao {
             @Override
             public List<EntitlementEvent> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
                 final List<EntitlementEventModelDao> models = entitySqlDaoWrapperFactory.become(EntitlementEventSqlDao.class).getEventsForSubscription(subscriptionId.toString(), context);
-
-                return new ArrayList<EntitlementEvent>(Collections2.transform(models, new Function<EntitlementEventModelDao, EntitlementEvent>() {
+                // Remove UNCANCEL events early on as they are not representative of a state transition but are just markers
+                final Collection<EntitlementEventModelDao> filteredModels = Collections2.filter(models, new Predicate<EntitlementEventModelDao>() {
+                    @Override
+                    public boolean apply(@Nullable final EntitlementEventModelDao input) {
+                        return input.getUserType() != ApiEventType.UNCANCEL;
+                    }
+                });
+                return new ArrayList<EntitlementEvent>(Collections2.transform(filteredModels, new Function<EntitlementEventModelDao, EntitlementEvent>() {
                     @Override
                     public EntitlementEvent apply(@Nullable final EntitlementEventModelDao input) {
                         return EntitlementEventModelDao.toEntitlementEvent(input);
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/model/EntitlementEventModelDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/model/EntitlementEventModelDao.java
index acc2167..6cc764c 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/model/EntitlementEventModelDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/model/EntitlementEventModelDao.java
@@ -56,7 +56,9 @@ public class EntitlementEventModelDao extends EntityBase implements EntityModelD
     private long currentVersion;
     private boolean isActive;
 
-    public EntitlementEventModelDao() { /* For the DAO mapper */ }
+    public EntitlementEventModelDao() {
+    /* For the DAO mapper */
+    }
 
     public EntitlementEventModelDao(final UUID id, final long totalOrdering, final EventType eventType, final ApiEventType userType,
                                     final DateTime requestedDate, final DateTime effectiveDate, final UUID subscriptionId,
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java
index 21f7b6b..04e6b3b 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java
@@ -473,4 +473,47 @@ public class TestTransfer extends EntitlementTestSuiteWithEmbeddedDB {
         final List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(newBundle.getId(), callContext);
         assertEquals(subscriptions.size(), 1);
     }
+
+    @Test(groups = "slow")
+    public void testTransferWithUncancel() throws Exception {
+
+        final UUID newAccountId = UUID.randomUUID();
+
+        final String baseProduct = "Shotgun";
+        final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+        final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        // CREATE BP
+        Subscription baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+        testListener.pushExpectedEvent(NextEvent.PHASE);
+        clock.addDays(30);
+        assertTrue(testListener.isCompleted(3000));
+
+        // SET CTD TO TRIGGER CANCELLATION EOT
+        final DateTime ctd = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+        entitlementInternalApi.setChargedThroughDate(baseSubscription.getId(), ctd, internalCallContext);
+
+
+        // CANCEL BP
+        baseSubscription = entitlementApi.getSubscriptionFromId(baseSubscription.getId(), callContext);
+        baseSubscription.cancel(clock.getUTCNow(), callContext);
+
+        // MOVE CLOCK one day AHEAD AND UNCANCEL BP
+        clock.addDays(1);
+        testListener.pushExpectedEvent(NextEvent.UNCANCEL);
+        baseSubscription.uncancel(callContext);
+        assertTrue(testListener.isCompleted(3000));
+
+        // MOVE CLOCK one day AHEAD AND UNCANCEL BP
+        clock.addDays(1);
+        final DateTime transferRequestedDate = clock.getUTCNow();
+        testListener.pushExpectedEvent(NextEvent.TRANSFER);
+        transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, true, false, callContext);
+        assertTrue(testListener.isCompleted(3000));
+
+        final SubscriptionBundle newBundle = entitlementApi.getBundleForAccountAndKey(newAccountId, bundle.getExternalKey(), callContext);
+        final List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(newBundle.getId(), callContext);
+        assertEquals(subscriptions.size(), 1);
+    }
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
index d0cd6b0..ff67ff3 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
@@ -146,11 +146,10 @@ public class TestUserApiAddOn extends EntitlementTestSuiteWithEmbeddedDB {
 
             aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId(), callContext);
             aoTransitions =  aoSubscription.getAllTransitions();
-            assertEquals(aoTransitions.size(), 4);
+            assertEquals(aoTransitions.size(), 3);
             assertEquals(aoTransitions.get(0).getTransitionType(), SubscriptionTransitionType.CREATE);
             assertEquals(aoTransitions.get(1).getTransitionType(), SubscriptionTransitionType.PHASE);
-            assertEquals(aoTransitions.get(2).getTransitionType(), SubscriptionTransitionType.UNCANCEL);
-            assertEquals(aoTransitions.get(3).getTransitionType(), SubscriptionTransitionType.CANCEL);
+            assertEquals(aoTransitions.get(2).getTransitionType(), SubscriptionTransitionType.CANCEL);
             assertTrue(aoSubscription.getFutureEndDate().compareTo(newBPChargedThroughDate) == 0);
 
             assertListenerStatus();
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
index 4d41e06..e63a21b 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
@@ -30,6 +30,7 @@ import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -136,6 +137,27 @@ public class PaymentResource extends JaxRsResourceBase {
         return Response.status(Status.OK).entity(paymentJsonSimple).build();
     }
 
+    @PUT
+    @Path("/{paymentId:" + UUID_PATTERN + "}")
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    public Response retryFailedPayment(@PathParam(ID_PARAM_NAME) final String paymentIdString,
+                                   @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                   @HeaderParam(HDR_REASON) final String reason,
+                                   @HeaderParam(HDR_COMMENT) final String comment,
+                                   @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException {
+
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+        final UUID paymentId = UUID.fromString(paymentIdString);
+        final Payment payment = paymentApi.getPayment(paymentId, false, callContext);
+        final Account account = accountApi.getAccountById(payment.getAccountId(), callContext);
+        final Payment newPayment = paymentApi.retryPayment(account, paymentId, callContext);
+
+        return Response.status(Status.OK).entity(new PaymentJsonSimple(newPayment)).build();
+    }
+
+
     @GET
     @Path("/{paymentId:" + UUID_PATTERN + "}/" + REFUNDS)
     @Produces(APPLICATION_JSON)
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
index c636c01..ba3f43e 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
@@ -90,6 +90,8 @@ public class AnalyticsListener implements OSGIKillbillEventHandler {
             case SUBSCRIPTION_CREATION:
             case SUBSCRIPTION_CHANGE:
             case SUBSCRIPTION_CANCEL:
+            case SUBSCRIPTION_PHASE:
+            case SUBSCRIPTION_UNCANCEL:
                 handleSubscriptionEvent(killbillEvent, callContext);
                 break;
             case OVERDUE_CHANGE:
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
index 92e5d45..346061b 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
@@ -29,6 +29,7 @@ import com.ning.billing.payment.core.PaymentMethodProcessor;
 import com.ning.billing.payment.core.PaymentProcessor;
 import com.ning.billing.payment.core.RefundProcessor;
 import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
 
@@ -67,6 +68,13 @@ public class DefaultPaymentApi implements PaymentApi {
     }
 
     @Override
+    public Payment retryPayment(final Account account, final UUID paymentId, final CallContext context) throws PaymentApiException {
+        final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), context);
+        paymentProcessor.retryPaymentFromApi(paymentId, internalCallContext);
+        return getPayment(paymentId, false, context);
+    }
+
+    @Override
     public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final TenantContext context) throws PaymentApiException {
         final Payment payment = paymentProcessor.getPayment(paymentId, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
         if (payment == null) {
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
index dc45727..232bfb5 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
@@ -113,8 +113,8 @@ public class PaymentProcessor extends ProcessorBase {
         this.autoPayoffRetryService = autoPayoffRetryService;
         this.clock = clock;
         this.paymentConfig = paymentConfig;
-        this.paymentPluginDispatcher = new PluginDispatcher<Payment>(executor);
-        this.voidPluginDispatcher = new PluginDispatcher<Void>(executor);
+        this.paymentPluginDispatcher = new PluginDispatcher<Payment>(paymentConfig.getPaymentTimeoutSeconds(), executor);
+        this.voidPluginDispatcher = new PluginDispatcher<Void>(paymentConfig.getPaymentTimeoutSeconds(), executor);
     }
 
     public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final InternalTenantContext context) throws PaymentApiException {
@@ -328,6 +328,13 @@ public class PaymentProcessor extends ProcessorBase {
         retryFailedPaymentInternal(paymentId, context, PaymentStatus.PAYMENT_FAILURE);
     }
 
+    public void retryPaymentFromApi(final UUID paymentId, final InternalCallContext context) {
+        log.info("Retrying payment " + paymentId + " time = " + clock.getUTCNow());
+        retryFailedPaymentInternal(paymentId, context, PaymentStatus.UNKNOWN,
+                                   PaymentStatus.AUTO_PAY_OFF,
+                                   PaymentStatus.PAYMENT_FAILURE,
+                                   PaymentStatus.PLUGIN_FAILURE);
+    }
 
     private void retryFailedPaymentInternal(final UUID paymentId, final InternalCallContext context, final PaymentStatus... expectedPaymentStates) {
 
@@ -386,7 +393,7 @@ public class PaymentProcessor extends ProcessorBase {
         } catch (AccountApiException e) {
             log.error(String.format("Failed to retry payment for paymentId %s", paymentId), e);
         } catch (PaymentApiException e) {
-            log.info(String.format("Failed to retry payment for paymentId %s", paymentId));
+            log.info(String.format("Failed to retry payment for paymentId %s", paymentId), e);
         } catch (TimeoutException e) {
             log.warn(String.format("Retry for payment %s timedout", paymentId));
             // STEPH we should throw some exception so NotificationQ does not clear status and retries us
diff --git a/payment/src/main/java/com/ning/billing/payment/dispatcher/PluginDispatcher.java b/payment/src/main/java/com/ning/billing/payment/dispatcher/PluginDispatcher.java
index ba0e3fc..d62750c 100644
--- a/payment/src/main/java/com/ning/billing/payment/dispatcher/PluginDispatcher.java
+++ b/payment/src/main/java/com/ning/billing/payment/dispatcher/PluginDispatcher.java
@@ -34,16 +34,18 @@ public class PluginDispatcher<T> {
 
     private final TimeUnit DEEFAULT_PLUGIN_TIMEOUT_UNIT = TimeUnit.SECONDS;
 
+    private final long timeoutSeconds;
     private final ExecutorService executor;
 
-    public PluginDispatcher(final ExecutorService executor) {
+    public PluginDispatcher(final long tiemoutSeconds, final ExecutorService executor) {
+        this.timeoutSeconds = tiemoutSeconds;
         this.executor = executor;
     }
 
+
     public T dispatchWithAccountLock(final Callable<T> task)
             throws PaymentApiException, TimeoutException {
-        final long DEFAULT_PLUGIN_TIMEOUT_SEC = 30;
-        return dispatchWithAccountLockAndTimeout(task, DEFAULT_PLUGIN_TIMEOUT_SEC, DEEFAULT_PLUGIN_TIMEOUT_UNIT);
+        return dispatchWithAccountLockAndTimeout(task, timeoutSeconds, DEEFAULT_PLUGIN_TIMEOUT_UNIT);
     }
 
     public T dispatchWithAccountLockAndTimeout(final Callable<T> task, final long timeout, final TimeUnit unit)
diff --git a/payment/src/test/java/com/ning/billing/payment/dispatcher/TestPluginDispatcher.java b/payment/src/test/java/com/ning/billing/payment/dispatcher/TestPluginDispatcher.java
new file mode 100644
index 0000000..9617ee9
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/dispatcher/TestPluginDispatcher.java
@@ -0,0 +1,77 @@
+package com.ning.billing.payment.dispatcher;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.ErrorCode;
+import com.ning.billing.payment.PaymentTestSuiteNoDB;
+import com.ning.billing.payment.api.PaymentApiException;
+
+public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
+
+    private final PluginDispatcher<Void> voidPluginDispatcher = new PluginDispatcher<Void>(10, Executors.newSingleThreadExecutor());
+
+    @Test(groups = "fast")
+    public void testDispatchWithTimeout() throws TimeoutException, PaymentApiException {
+        boolean gotIt = false;
+        try {
+            voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    Thread.sleep(1000);
+                    return null;
+                }
+            }, 100, TimeUnit.MILLISECONDS);
+            Assert.fail("Failed : should have had Timeout exception");
+        } catch (TimeoutException e) {
+            gotIt = true;
+        } catch (PaymentApiException e) {
+            Assert.fail("Failed : should have had Timeout exception");
+        }
+        Assert.assertTrue(gotIt);
+    }
+
+    @Test(groups = "fast")
+    public void testDispatchWithPaymentApiException() throws TimeoutException, PaymentApiException {
+        boolean gotIt = false;
+        try {
+            voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    throw new PaymentApiException(ErrorCode.PAYMENT_ADD_PAYMENT_METHOD, "foo", "foo");
+                }
+            }, 100, TimeUnit.MILLISECONDS);
+            Assert.fail("Failed : should have had Timeout exception");
+        } catch (TimeoutException e) {
+            Assert.fail("Failed : should have had PaymentApiException exception");
+        } catch (PaymentApiException e) {
+            gotIt = true;
+        }
+        Assert.assertTrue(gotIt);
+    }
+
+    @Test(groups = "fast")
+    public void testDispatchWithRuntimeExceptionWrappedInPaymentApiException() throws TimeoutException, PaymentApiException {
+        boolean gotIt = false;
+        try {
+            voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    throw new RuntimeException("whatever");
+                }
+            }, 100, TimeUnit.MILLISECONDS);
+            Assert.fail("Failed : should have had Timeout exception");
+        } catch (TimeoutException e) {
+            Assert.fail("Failed : should have had RuntimeException exception");
+        } catch (PaymentApiException e) {
+            gotIt = true;
+        } catch (RuntimeException e) {
+        }
+        Assert.assertTrue(gotIt);
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/config/PaymentConfig.java b/util/src/main/java/com/ning/billing/util/config/PaymentConfig.java
index d4d7ad0..47549a1 100644
--- a/util/src/main/java/com/ning/billing/util/config/PaymentConfig.java
+++ b/util/src/main/java/com/ning/billing/util/config/PaymentConfig.java
@@ -48,6 +48,11 @@ public interface PaymentConfig extends KillbillConfig {
     @Description("Maximum number of retries for failed payments")
     public int getPluginFailureRetryMaxAttempts();
 
+    @Config("killbill.payment.timeout.seconds")
+    @Default("90")
+    @Description("Timeout for each payment attempt")
+    public int getPaymentTimeoutSeconds();
+
     @Config("killbill.payment.off")
     @Default("false")
     @Description("Whether the payment subsystem is off")