Details
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/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/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")