killbill-memoizeit

Implement capture/refund/chargeback opertaion using payment

7/23/2015 5:01:19 PM

Details

diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java
index 83d65e5..9a50741 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java
@@ -29,18 +29,25 @@ public class ComboPaymentTransactionJson extends JsonBase {
     private final AccountJson account;
     private final PaymentMethodJson paymentMethod;
     private final PaymentTransactionJson transaction;
+    private final Iterable<PluginPropertyJson> paymentMethodPluginProperties;
+    private final Iterable<PluginPropertyJson> transactionPluginProperties;
 
     @JsonCreator
     public ComboPaymentTransactionJson(@JsonProperty("account") final AccountJson account,
                                        @JsonProperty("paymentMethod") final PaymentMethodJson paymentMethod,
                                        @JsonProperty("transaction") final PaymentTransactionJson transaction,
+                                       @JsonProperty("paymentMethodPluginProperties") final Iterable<PluginPropertyJson> paymentMethodPluginProperties,
+                                       @JsonProperty("transactionPluginProperties") final Iterable<PluginPropertyJson> transactionPluginProperties,
                                        @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
         super(auditLogs);
         this.account = account;
         this.paymentMethod = paymentMethod;
         this.transaction = transaction;
+        this.paymentMethodPluginProperties = paymentMethodPluginProperties;
+        this.transactionPluginProperties = transactionPluginProperties;
     }
 
+
     public AccountJson getAccount() {
         return account;
     }
@@ -53,12 +60,22 @@ public class ComboPaymentTransactionJson extends JsonBase {
         return transaction;
     }
 
+    public Iterable<PluginPropertyJson> getPaymentMethodPluginProperties() {
+        return paymentMethodPluginProperties;
+    }
+
+    public Iterable<PluginPropertyJson> getTransactionPluginProperties() {
+        return transactionPluginProperties;
+    }
+
     @Override
     public String toString() {
         return "ComboPaymentTransactionJson{" +
                "account=" + account +
                ", paymentMethod=" + paymentMethod +
                ", transaction=" + transaction +
+               ", paymentMethodPluginProperties=" + paymentMethodPluginProperties +
+               ", transactionPluginProperties=" + transactionPluginProperties +
                '}';
     }
 
@@ -79,7 +96,13 @@ public class ComboPaymentTransactionJson extends JsonBase {
         if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) {
             return false;
         }
-        return !(transaction != null ? !transaction.equals(that.transaction) : that.transaction != null);
+        if (transaction != null ? !transaction.equals(that.transaction) : that.transaction != null) {
+            return false;
+        }
+        if (paymentMethodPluginProperties != null ? !paymentMethodPluginProperties.equals(that.paymentMethodPluginProperties) : that.paymentMethodPluginProperties != null) {
+            return false;
+        }
+        return !(transactionPluginProperties != null ? !transactionPluginProperties.equals(that.transactionPluginProperties) : that.transactionPluginProperties != null);
 
     }
 
@@ -88,6 +111,8 @@ public class ComboPaymentTransactionJson extends JsonBase {
         int result = account != null ? account.hashCode() : 0;
         result = 31 * result + (paymentMethod != null ? paymentMethod.hashCode() : 0);
         result = 31 * result + (transaction != null ? transaction.hashCode() : 0);
+        result = 31 * result + (paymentMethodPluginProperties != null ? paymentMethodPluginProperties.hashCode() : 0);
+        result = 31 * result + (transactionPluginProperties != null ? transactionPluginProperties.hashCode() : 0);
         return result;
     }
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
index ea98904..163d217 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicReference;
 
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
@@ -50,6 +51,7 @@ import org.killbill.billing.jaxrs.json.ComboPaymentTransactionJson;
 import org.killbill.billing.jaxrs.json.PaymentJson;
 import org.killbill.billing.jaxrs.json.PaymentMethodJson;
 import org.killbill.billing.jaxrs.json.PaymentTransactionJson;
+import org.killbill.billing.jaxrs.json.PluginPropertyJson;
 import org.killbill.billing.jaxrs.util.Context;
 import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
 import org.killbill.billing.payment.api.Payment;
@@ -70,8 +72,12 @@ import org.killbill.clock.Clock;
 
 import com.codahale.metrics.annotation.Timed;
 import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
 import com.wordnik.swagger.annotations.ApiResponse;
@@ -113,7 +119,25 @@ public class PaymentResource extends JaxRsResourceBase {
         final Payment payment = paymentApi.getPayment(paymentIdId, withPluginInfo, pluginProperties, tenantContext);
         final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(payment.getAccountId(), auditMode.getLevel(), tenantContext);
         final PaymentJson result = new PaymentJson(payment, accountAuditLogs);
+        return Response.status(Response.Status.OK).entity(result).build();
+    }
 
+    @Timed
+    @GET
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Retrieve a payment by id", response = PaymentJson.class)
+    @ApiResponses(value = {@ApiResponse(code = 404, message = "Payment not found")})
+    public Response getPaymentByExternalKey(@QueryParam(QUERY_WITH_PLUGIN_INFO) @DefaultValue("false") final Boolean withPluginInfo,
+                                            @QueryParam(QUERY_EXTERNAL_KEY) final String paymentExternalKey,
+                                            @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                            @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                                            @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        verifyNonNullOrEmpty(paymentExternalKey, "Payment externalKey needs to be specified");
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        final TenantContext tenantContext = context.createContext(request);
+        final Payment payment = paymentApi.getPaymentByExternalKey(paymentExternalKey, withPluginInfo, pluginProperties, tenantContext);
+        final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(payment.getAccountId(), auditMode.getLevel(), tenantContext);
+        final PaymentJson result = new PaymentJson(payment, accountAuditLogs);
         return Response.status(Response.Status.OK).entity(result).build();
     }
 
@@ -222,18 +246,45 @@ public class PaymentResource extends JaxRsResourceBase {
                                          @HeaderParam(HDR_COMMENT) final String comment,
                                          @javax.ws.rs.core.Context final UriInfo uriInfo,
                                          @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return captureAuthorizationInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    @Timed
+    @POST
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Capture an existing authorization")
+    @ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")})
+    public Response captureAuthorizationByExternalKey(final PaymentTransactionJson json,
+                                                      @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                                      @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 UriInfo uriInfo,
+                                                      @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return captureAuthorizationInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    private Response captureAuthorizationInternal(final PaymentTransactionJson json,
+                                                  @Nullable final String paymentIdStr,
+                                                  final List<String> pluginPropertiesString,
+                                                  final String createdBy,
+                                                  final String reason,
+                                                  final String comment,
+                                                  final UriInfo uriInfo,
+                                                  final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+
         verifyNonNullOrEmpty(json, "PaymentTransactionJson body should be specified");
         verifyNonNullOrEmpty(json.getAmount(), "PaymentTransactionJson amount needs to be set");
 
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-        final UUID paymentId = UUID.fromString(paymentIdStr);
-        final Payment initialPayment = paymentApi.getPayment(paymentId, false, pluginProperties, callContext);
+        final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json.getPaymentExternalKey(), pluginProperties, callContext);
 
         final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
         final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
 
-        final Payment payment = paymentApi.createCapture(account, paymentId, json.getAmount(), currency,
+        final Payment payment = paymentApi.createCapture(account, initialPayment.getId(), json.getAmount(), currency,
                                                          json.getTransactionExternalKey(), pluginProperties, callContext);
         return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
     }
@@ -254,72 +305,50 @@ public class PaymentResource extends JaxRsResourceBase {
                                   @HeaderParam(HDR_COMMENT) final String comment,
                                   @javax.ws.rs.core.Context final UriInfo uriInfo,
                                   @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
-        verifyNonNullOrEmpty(json, "PaymentTransactionJson body should be specified");
-        verifyNonNullOrEmpty(json.getAmount(), "PaymentTransactionJson amount needs to be set");
-
-        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
-        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-        final UUID paymentId = UUID.fromString(paymentIdStr);
-        final Payment initialPayment = paymentApi.getPayment(paymentId, false, pluginProperties, callContext);
-
-        final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
-        final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
-
-        final Payment payment = paymentApi.createRefund(account, paymentId, json.getAmount(), currency,
-                                                        json.getTransactionExternalKey(), pluginProperties, callContext);
-        return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
+        return refundPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
     }
 
     @Timed
     @POST
+    @Path("/" + REFUNDS)
     @Consumes(APPLICATION_JSON)
     @Produces(APPLICATION_JSON)
-    @Path("/" + COMBO)
-    @ApiOperation(value = "Combo api to create a new payment transaction on a existing (or not) account ")
-    @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid data for Account or PaymentMethod")})
-    public Response createPayment(final ComboPaymentTransactionJson json,
-                                  @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
-                                  @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
-                                  @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 UriInfo uriInfo,
-                                  @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+    @ApiOperation(value = "Refund an existing payment")
+    @ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")})
+    public Response refundPaymentByExternalKey(final PaymentTransactionJson json,
+                                               @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                               @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 UriInfo uriInfo,
+                                               @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return refundPaymentInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
 
-        verifyNonNullOrEmpty(json, "ComboPaymentTransactionJson body should be specified");
+    }
 
-        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-        final Account account = getOrCreateAccount(json.getAccount(), callContext);
+    private Response refundPaymentInternal(final PaymentTransactionJson json,
+                                           @Nullable final String paymentIdStr,
+                                           final List<String> pluginPropertiesString,
+                                           final String createdBy,
+                                           final String reason,
+                                           final String comment,
+                                           final UriInfo uriInfo,
+                                           final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+
+        verifyNonNullOrEmpty(json, "PaymentTransactionJson body should be specified");
+        verifyNonNullOrEmpty(json.getAmount(), "PaymentTransactionJson amount needs to be set");
 
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
-        final UUID paymentMethodId = getOrCreatePaymentMethod(account, json.getPaymentMethod(), pluginProperties, callContext);
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+        final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json.getPaymentExternalKey(), pluginProperties, callContext);
 
-        final PaymentTransactionJson paymentTransactionJson = json.getTransaction();
-        final TransactionType transactionType = TransactionType.valueOf(paymentTransactionJson.getTransactionType());
-        final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
-        final Payment result;
+        final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
+        final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
+
+        final Payment payment = paymentApi.createRefund(account, initialPayment.getId(), json.getAmount(), currency,
+                                                        json.getTransactionExternalKey(), pluginProperties, callContext);
+        return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
 
-        final UUID paymentId = null; // If we need to specify a paymentId (e.g 3DS authorization, we can use regular API, no need for combo call)
-        switch (transactionType) {
-            case AUTHORIZE:
-                result = paymentApi.createAuthorizationWithPaymentControl(account, paymentMethodId, paymentId, paymentTransactionJson.getAmount(), account.getCurrency(),
-                                                                          paymentTransactionJson.getPaymentExternalKey(), paymentTransactionJson.getTransactionExternalKey(),
-                                                                          pluginProperties, paymentOptions, callContext);
-                break;
-            case PURCHASE:
-                result = paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, paymentId, paymentTransactionJson.getAmount(), account.getCurrency(),
-                                                                     paymentTransactionJson.getPaymentExternalKey(), paymentTransactionJson.getTransactionExternalKey(),
-                                                                     pluginProperties, paymentOptions, callContext);
-                break;
-            case CREDIT:
-                result = paymentApi.createCreditWithPaymentControl(account, paymentMethodId, paymentId, paymentTransactionJson.getAmount(), account.getCurrency(),
-                                                                   paymentTransactionJson.getPaymentExternalKey(), paymentTransactionJson.getTransactionExternalKey(),
-                                                                   pluginProperties, paymentOptions, callContext);
-                break;
-            default:
-                return Response.status(Status.PRECONDITION_FAILED).entity("TransactionType " + transactionType + " is not allowed for an account").build();
-        }
-        return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", result.getId());
     }
 
     @Timed
@@ -338,15 +367,41 @@ public class PaymentResource extends JaxRsResourceBase {
                                 @HeaderParam(HDR_COMMENT) final String comment,
                                 @javax.ws.rs.core.Context final UriInfo uriInfo,
                                 @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return voidPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    @Timed
+    @DELETE
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Void an existing payment")
+    @ApiResponses(value = {@ApiResponse(code = 404, message = "Account or payment not found")})
+    public Response voidPaymentByExternalKey(final PaymentTransactionJson json,
+                                             @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                             @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 UriInfo uriInfo,
+                                             @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return voidPaymentInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    private Response voidPaymentInternal(final PaymentTransactionJson json,
+                                         @Nullable final String paymentIdStr,
+                                         final List<String> pluginPropertiesString,
+                                         final String createdBy,
+                                         final String reason,
+                                         final String comment,
+                                         final UriInfo uriInfo,
+                                         final HttpServletRequest request) throws PaymentApiException, AccountApiException {
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-        final UUID paymentId = UUID.fromString(paymentIdStr);
-        final Payment initialPayment = paymentApi.getPayment(paymentId, false, pluginProperties, callContext);
+        final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json.getPaymentExternalKey(), pluginProperties, callContext);
 
         final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
 
         final String transactionExternalKey = json != null ? json.getTransactionExternalKey() : null;
-        final Payment payment = paymentApi.createVoid(account, paymentId, transactionExternalKey, pluginProperties, callContext);
+        final Payment payment = paymentApi.createVoid(account, initialPayment.getId(), transactionExternalKey, pluginProperties, callContext);
         return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
     }
 
@@ -366,22 +421,119 @@ public class PaymentResource extends JaxRsResourceBase {
                                       @HeaderParam(HDR_COMMENT) final String comment,
                                       @javax.ws.rs.core.Context final UriInfo uriInfo,
                                       @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return chargebackPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    @Timed
+    @POST
+    @Path("/" + CHARGEBACKS)
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Record a chargeback")
+    @ApiResponses(value = {@ApiResponse(code = 404, message = "Account not found")})
+    public Response chargebackPaymentByExternalKey(final PaymentTransactionJson json,
+                                                   @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                                   @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 UriInfo uriInfo,
+                                                   @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        return chargebackPaymentInternal(json, null, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    private Response chargebackPaymentInternal(final PaymentTransactionJson json,
+                                               @Nullable final String paymentIdStr,
+                                               final List<String> pluginPropertiesString,
+                                               final String createdBy,
+                                               final String reason,
+                                               final String comment,
+                                               final UriInfo uriInfo,
+                                               final HttpServletRequest request) throws PaymentApiException, AccountApiException {
         verifyNonNullOrEmpty(json, "PaymentTransactionJson body should be specified");
         verifyNonNullOrEmpty(json.getAmount(), "PaymentTransactionJson amount needs to be set");
 
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-        final UUID paymentId = UUID.fromString(paymentIdStr);
-        final Payment initialPayment = paymentApi.getPayment(paymentId, false, pluginProperties, callContext);
+        final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json.getPaymentExternalKey(), pluginProperties, callContext);
 
         final Account account = accountUserApi.getAccountById(initialPayment.getAccountId(), callContext);
         final Currency currency = json.getCurrency() == null ? account.getCurrency() : Currency.valueOf(json.getCurrency());
 
-        final Payment payment = paymentApi.createChargeback(account, paymentId, json.getAmount(), currency,
+        final Payment payment = paymentApi.createChargeback(account, initialPayment.getId(), json.getAmount(), currency,
                                                             json.getTransactionExternalKey(), callContext);
         return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
     }
 
+    @Timed
+    @POST
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @Path("/" + COMBO)
+    @ApiOperation(value = "Combo api to create a new payment transaction on a existing (or not) account ")
+    @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid data for Account or PaymentMethod")})
+    public Response createComboPayment(final ComboPaymentTransactionJson json,
+                                       @QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List<String> paymentControlPluginNames,
+                                       @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 UriInfo uriInfo,
+                                       @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+
+        verifyNonNullOrEmpty(json, "ComboPaymentTransactionJson body should be specified");
+
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+        final Account account = getOrCreateAccount(json.getAccount(), callContext);
+
+        final Iterable paymentMethodPluginProperties = Iterables.<PluginPropertyJson, PluginProperty>transform(json.getPaymentMethodPluginProperties(),
+                                                                                                               new Function<PluginPropertyJson, PluginProperty>() {
+                                                                                                                   @Override
+                                                                                                                   public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) {
+                                                                                                                       return pluginPropertyJson.toPluginProperty();
+                                                                                                                   }
+                                                                                                               }
+                                                                                                              );
+
+        final UUID paymentMethodId = getOrCreatePaymentMethod(account, json.getPaymentMethod(), paymentMethodPluginProperties, callContext);
+
+        final PaymentTransactionJson paymentTransactionJson = json.getTransaction();
+        final TransactionType transactionType = TransactionType.valueOf(paymentTransactionJson.getTransactionType());
+        final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
+        final Payment result;
+
+
+        final Iterable transactionPluginProperties = Iterables.<PluginPropertyJson, PluginProperty>transform(json.getTransactionPluginProperties(),
+                                                                                                               new Function<PluginPropertyJson, PluginProperty>() {
+                                                                                                                   @Override
+                                                                                                                   public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) {
+                                                                                                                       return pluginPropertyJson.toPluginProperty();
+                                                                                                                   }
+                                                                                                               }
+                                                                                                              );
+
+        final Currency currency = paymentTransactionJson.getCurrency() == null ? account.getCurrency() : Currency.valueOf(paymentTransactionJson.getCurrency());
+        final UUID paymentId = null; // If we need to specify a paymentId (e.g 3DS authorization, we can use regular API, no need for combo call)
+        switch (transactionType) {
+            case AUTHORIZE:
+                result = paymentApi.createAuthorizationWithPaymentControl(account, paymentMethodId, paymentId, paymentTransactionJson.getAmount(), currency,
+                                                                          paymentTransactionJson.getPaymentExternalKey(), paymentTransactionJson.getTransactionExternalKey(),
+                                                                          transactionPluginProperties, paymentOptions, callContext);
+                break;
+            case PURCHASE:
+                result = paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, paymentId, paymentTransactionJson.getAmount(), currency,
+                                                                     paymentTransactionJson.getPaymentExternalKey(), paymentTransactionJson.getTransactionExternalKey(),
+                                                                     transactionPluginProperties, paymentOptions, callContext);
+                break;
+            case CREDIT:
+                result = paymentApi.createCreditWithPaymentControl(account, paymentMethodId, paymentId, paymentTransactionJson.getAmount(), currency,
+                                                                   paymentTransactionJson.getPaymentExternalKey(), paymentTransactionJson.getTransactionExternalKey(),
+                                                                   transactionPluginProperties, paymentOptions, callContext);
+                break;
+            default:
+                return Response.status(Status.PRECONDITION_FAILED).entity("TransactionType " + transactionType + " is not allowed for an account").build();
+        }
+        return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", result.getId());
+    }
+
     @Override
     protected ObjectType getObjectType() {
         return ObjectType.PAYMENT;
@@ -407,23 +559,52 @@ public class PaymentResource extends JaxRsResourceBase {
     }
 
     private UUID getOrCreatePaymentMethod(final Account account, final PaymentMethodJson paymentMethodJson, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) throws PaymentApiException {
-        // Attempt to retrieve by paymentMethodId if specified
+
+        // Get all payment methods for account
+        final List<PaymentMethod> accountPaymentMethods = paymentApi.getAccountPaymentMethods(account.getId(), false, ImmutableList.<PluginProperty>of(), callContext);
+
+        // If we were specified a paymentMethod id and we find it, we return it
         if (paymentMethodJson.getPaymentMethodId() != null) {
-            try {
-                return paymentApi.getPaymentMethodById(UUID.fromString(paymentMethodJson.getPaymentMethodId()), false, false, pluginProperties, callContext).getId();
-            } catch (PaymentApiException ignore) {
+            final UUID match = UUID.fromString(paymentMethodJson.getPaymentMethodId());
+            if (Iterables.any(accountPaymentMethods, new Predicate<PaymentMethod>() {
+                @Override
+                public boolean apply(final PaymentMethod input) {
+                    return input.getId().equals(match);
+                }
+            })) {
+                return match;
             }
         }
 
-        verifyNonNullOrEmpty(paymentMethodJson.getExternalKey(), "PaymentMethod externalKey should be specified");
-        try {
-            // Attempt to retrieve by paymentMethod externalKey
-            return paymentApi.getPaymentMethodByExternalKey(paymentMethodJson.getExternalKey(), false, false, pluginProperties, callContext).getId();
-        } catch (PaymentApiException ignore) {
+        // If we were specified a paymentMethod externalKey and we find it, we return it
+        if (paymentMethodJson.getExternalKey() != null) {
+            final PaymentMethod match = Iterables.tryFind(accountPaymentMethods, new Predicate<PaymentMethod>() {
+                @Override
+                public boolean apply(final PaymentMethod input) {
+                    return input.getExternalKey().equals(paymentMethodJson.getExternalKey());
+                }
+            }).orNull();
+            if (match != null) {
+                return match.getId();
+            }
         }
+
+        // Only set as default if this is the first paymentMethod on the account
+        final boolean isDefault = accountPaymentMethods.isEmpty();
         final PaymentMethod paymentData = paymentMethodJson.toPaymentMethod(account.getId().toString());
-        // Finally create if does not exist
-        return paymentApi.addPaymentMethod(account, paymentMethodJson.getExternalKey(), paymentMethodJson.getPluginName(), paymentMethodJson.isDefault(),
+        return paymentApi.addPaymentMethod(account, paymentMethodJson.getExternalKey(), paymentMethodJson.getPluginName(), isDefault,
                                            paymentData.getPluginDetail(), pluginProperties, callContext);
     }
+
+    private Payment getPaymentByIdOrKey(@Nullable final String paymentIdStr, @Nullable final String externalKey, final Iterable<PluginProperty> pluginProperties, final TenantContext tenantContext) throws PaymentApiException {
+
+        Preconditions.checkArgument(paymentIdStr != null || externalKey != null, "Need to set either paymentId or payment externalKey");
+        if (paymentIdStr != null) {
+            final UUID paymentId = UUID.fromString(paymentIdStr);
+            return paymentApi.getPayment(paymentId, false, pluginProperties, tenantContext);
+        } else {
+            return paymentApi.getPaymentByExternalKey(externalKey, false, pluginProperties, tenantContext);
+        }
+    }
+
 }
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
index 7098b96..aac1839 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
@@ -30,10 +30,13 @@ import org.killbill.billing.client.model.PaymentMethod;
 import org.killbill.billing.client.model.PaymentMethodPluginDetail;
 import org.killbill.billing.client.model.PaymentTransaction;
 import org.killbill.billing.client.model.Payments;
+import org.killbill.billing.client.model.PluginProperty;
+import org.killbill.billing.jaxrs.json.PluginPropertyJson;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
 public class TestPayment extends TestJaxrsBase {
@@ -66,15 +69,24 @@ public class TestPayment extends TestJaxrsBase {
         authTransactionJson.setTransactionExternalKey(authTransactionExternalKey);
         authTransactionJson.setTransactionType("AUTHORIZE");
 
-        final ComboPaymentTransaction comboPaymentTransaction = new ComboPaymentTransaction(accountJson, paymentMethodJson, authTransactionJson);
+        final ComboPaymentTransaction comboPaymentTransaction = new ComboPaymentTransaction(accountJson, paymentMethodJson, authTransactionJson, ImmutableList.<PluginProperty>of(), ImmutableList.<PluginProperty>of());
 
         final Payment payment = killBillClient.createPayment(comboPaymentTransaction, ImmutableMap.<String, String>of(), createdBy, reason, comment);
         verifyComboPayment(payment, paymentExternalKey,
-                      BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+                           BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, 1);
+
+
+        // Void payment using externalKey
+        final String voidTransactionExternalKey = UUID.randomUUID().toString();
+        final Payment voidPayment = killBillClient.voidPayment(null, paymentExternalKey, voidTransactionExternalKey, ImmutableMap.<String, String>of(), createdBy, reason, comment);
+        verifyPaymentTransaction(voidPayment.getPaymentId(), voidPayment.getTransactions().get(1),
+                                 paymentExternalKey, voidTransactionExternalKey,
+                                 accountJson, null, "VOID");
+
 
     }
 
-    public void testCreateRetrievePayment(final Account account, @Nullable final UUID paymentMethodId,
+    private void testCreateRetrievePayment(final Account account, @Nullable final UUID paymentMethodId,
                                           final String PaymentExternalKey, final int PaymentNb) throws Exception {
         // Authorization
         final String authTransactionExternalKey = UUID.randomUUID().toString();
@@ -96,6 +108,7 @@ public class TestPayment extends TestJaxrsBase {
         captureTransaction.setCurrency(account.getCurrency());
         captureTransaction.setPaymentExternalKey(PaymentExternalKey);
         captureTransaction.setTransactionExternalKey(capture1TransactionExternalKey);
+        // captureAuthorization is using paymentId
         final Payment capturedPayment1 = killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment);
         verifyPayment(account, paymentMethodId, capturedPayment1, PaymentExternalKey, authTransactionExternalKey,
                       BigDecimal.TEN, BigDecimal.ONE, BigDecimal.ZERO, 2, PaymentNb);
@@ -106,6 +119,8 @@ public class TestPayment extends TestJaxrsBase {
         // Capture 2
         final String capture2TransactionExternalKey = UUID.randomUUID().toString();
         captureTransaction.setTransactionExternalKey(capture2TransactionExternalKey);
+        // captureAuthorization is using externalKey
+        captureTransaction.setPaymentId(null);
         final Payment capturedPayment2 = killBillClient.captureAuthorization(captureTransaction, createdBy, reason, comment);
         verifyPayment(account, paymentMethodId, capturedPayment2, PaymentExternalKey, authTransactionExternalKey,
                       BigDecimal.TEN, new BigDecimal("2"), BigDecimal.ZERO, 3, PaymentNb);