killbill-memoizeit

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
new file mode 100644
index 0000000..9a50741
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ComboPaymentTransactionJson.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+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;
+    }
+
+    public PaymentMethodJson getPaymentMethod() {
+        return paymentMethod;
+    }
+
+    public PaymentTransactionJson getTransaction() {
+        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 +
+               '}';
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ComboPaymentTransactionJson)) {
+            return false;
+        }
+
+        final ComboPaymentTransactionJson that = (ComboPaymentTransactionJson) o;
+
+        if (account != null ? !account.equals(that.account) : that.account != null) {
+            return false;
+        }
+        if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) {
+            return false;
+        }
+        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);
+
+    }
+
+    @Override
+    public int hashCode() {
+        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/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 4c5dabb..d29d527 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -223,4 +223,6 @@ public interface JaxrsResource {
 
     public static final String UPCOMING_INVOICE_TARGET_DATE = "upcomingInvoiceTargetDate";
 
+    public static final String COMBO = "combo";
+
 }
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 43b3464..42b942f 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;
@@ -37,6 +38,7 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 
 import org.killbill.billing.ObjectType;
@@ -44,14 +46,21 @@ import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountUserApi;
 import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.jaxrs.json.AccountJson;
+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;
 import org.killbill.billing.payment.api.PaymentApi;
 import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.payment.api.PaymentOptions;
 import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.util.api.AuditUserApi;
 import org.killbill.billing.util.api.CustomFieldUserApi;
 import org.killbill.billing.util.api.TagUserApi;
@@ -63,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;
@@ -106,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();
     }
 
@@ -215,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());
     }
@@ -247,20 +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 {
+        return refundPaymentInternal(json, paymentIdStr, pluginPropertiesString, createdBy, reason, comment, uriInfo, request);
+    }
+
+    @Timed
+    @POST
+    @Path("/" + REFUNDS)
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @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);
+
+    }
+
+    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 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.createRefund(account, paymentId, json.getAmount(), currency,
+        final Payment payment = paymentApi.createRefund(account, initialPayment.getId(), json.getAmount(), currency,
                                                         json.getTransactionExternalKey(), pluginProperties, callContext);
         return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", payment.getId());
+
     }
 
     @Timed
@@ -279,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());
     }
 
@@ -307,25 +421,190 @@ 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<PluginProperty> paymentMethodPluginProperties = json.getPaymentMethodPluginProperties() != null ?
+                                                                       Iterables.<PluginPropertyJson, PluginProperty>transform(json.getPaymentMethodPluginProperties(),
+                                                                                                                               new Function<PluginPropertyJson, PluginProperty>() {
+                                                                                                                                   @Override
+                                                                                                                                   public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) {
+                                                                                                                                       return pluginPropertyJson.toPluginProperty();
+                                                                                                                                   }
+                                                                                                                               }
+                                                                                                                              ) :
+                                                                       ImmutableList.<PluginProperty>of();
+
+        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<PluginProperty> transactionPluginProperties = json.getTransactionPluginProperties() != null ?
+                                                                     Iterables.<PluginPropertyJson, PluginProperty>transform(json.getTransactionPluginProperties(),
+                                                                                                                             new Function<PluginPropertyJson, PluginProperty>() {
+                                                                                                                                 @Override
+                                                                                                                                 public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) {
+                                                                                                                                     return pluginPropertyJson.toPluginProperty();
+                                                                                                                                 }
+                                                                                                                             }
+                                                                                                                            ) :
+                                                                     ImmutableList.<PluginProperty>of();
+
+        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;
     }
 
+    private Account getOrCreateAccount(final AccountJson accountJson, final CallContext callContext) throws AccountApiException {
+        // Attempt to retrieve by accountId if specified
+        if (accountJson.getAccountId() != null) {
+            return accountUserApi.getAccountById(UUID.fromString(accountJson.getAccountId()), callContext);
+        }
+
+        if (accountJson.getExternalKey() != null) {
+            // Attempt to retrieve by account externalKey, ignore if does not exist so we can create it with the key specified.
+            try {
+                return accountUserApi.getAccountByKey(accountJson.getExternalKey(), callContext);
+            } catch (final AccountApiException ignore) {}
+        }
+        // Finally create if does not exist
+        return accountUserApi.createAccount(accountJson.toAccountData(), callContext);
+    }
+
+    private UUID getOrCreatePaymentMethod(final Account account, final PaymentMethodJson paymentMethodJson, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) throws PaymentApiException {
+
+        // 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) {
+            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;
+            }
+        }
+
+        // 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());
+        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/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index 1547008..cdc58a7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -233,7 +233,7 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
 
     private PaymentPluginApi getPaymentPluginApi(final PaymentModelDao item, final String pluginName) {
         final PaymentPluginApi pluginApi = pluginRegistry.getServiceForName(pluginName);
-        Preconditions.checkState(pluginApi != null, "Janitor IncompletePaymentTransactionTask cannot retrieve PaymentPluginApi " + item.getId() + ", skipping");
+        Preconditions.checkState(pluginApi != null, "Janitor IncompletePaymentTransactionTask cannot retrieve PaymentPluginApi for plugin %s (payment id %s), skipping", pluginName, item.getId());
         return pluginApi;
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
index 34e2fd8..d5855d0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
@@ -56,6 +56,8 @@ import org.killbill.commons.locker.LockFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.MoreObjects;
+
 public abstract class OperationControlCallback extends OperationCallbackBase<Payment, PaymentApiException> implements OperationCallback {
 
     protected final PaymentProcessor paymentProcessor;
@@ -151,30 +153,26 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
     }
 
     @Override
-    protected OperationException rewrapExecutionException(final PaymentStateContext paymentStateContext, final ExecutionException e) {
-        if (e.getCause() instanceof OperationException) {
-            return (OperationException) e.getCause();
-        } else if (e.getCause() instanceof LockFailedException) {
+    protected OperationException unwrapExceptionFromDispatchedTask(final PaymentStateContext paymentStateContext, final Exception e) {
+
+        // If this is an ExecutionException we attempt to extract the cause first
+        final Throwable originalExceptionOrCause = e instanceof ExecutionException ? MoreObjects.firstNonNull(e.getCause(), e) : e;
+
+        if (originalExceptionOrCause instanceof OperationException) {
+            return (OperationException) originalExceptionOrCause;
+        } else if (originalExceptionOrCause instanceof LockFailedException) {
             final String format = String.format("Failed to lock account %s", paymentStateContext.getAccount().getExternalKey());
             logger.error(String.format(format));
-            return new OperationException(e, getOperationResultOnException(paymentStateContext));
+        } else if (originalExceptionOrCause instanceof TimeoutException) {
+            logger.warn("RetryOperationCallback call TIMEOUT for account {}", paymentStateContext.getAccount().getExternalKey());
+        } else if (originalExceptionOrCause instanceof InterruptedException) {
+            logger.error("RetryOperationCallback call was interrupted for account {}", paymentStateContext.getAccount().getExternalKey());
         } else /* most probably RuntimeException */ {
             logger.warn("RetryOperationCallback failed for account {}", paymentStateContext.getAccount().getExternalKey(), e);
-            return new OperationException(e, getOperationResultOnException(paymentStateContext));
         }
-    }
-
-    @Override
-    protected OperationException wrapTimeoutException(final PaymentStateContext paymentStateContext, final TimeoutException e) {
-        logger.warn("RetryOperationCallback call TIMEOUT for account {}", paymentStateContext.getAccount().getExternalKey());
         return new OperationException(e, getOperationResultOnException(paymentStateContext));
     }
 
-    @Override
-    protected OperationException wrapInterruptedException(final PaymentStateContext paymentStateContext, final InterruptedException e) {
-        logger.error("RetryOperationCallback call was interrupted for account {}", paymentStateContext.getAccount().getExternalKey());
-        return new OperationException(e, getOperationResultOnException(paymentStateContext));
-    }
 
     protected void executePluginOnSuccessCalls(final List<String> paymentControlPluginNames, final PaymentRoutingContext paymentControlContext) {
         for (final String pluginName : paymentControlPluginNames) {
@@ -253,10 +251,8 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
         final DateTime retryDate = executePluginOnFailureCalls(paymentStateControlContext.getPaymentControlPluginNames(), paymentControlContext);
         if (retryDate != null) {
             ((PaymentStateControlContext) paymentStateContext).setRetryDate(retryDate);
-            return OperationResult.FAILURE;
-        } else {
-            return OperationResult.EXCEPTION;
         }
+        return getOperationResultOnException(paymentStateContext);
     }
 
     private DateTime executePluginOnFailureCalls(final List<String> paymentControlPluginNames, final PaymentRoutingContext paymentControlContext) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
index 6a81f1d..8ab58ff 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
@@ -66,12 +66,12 @@ public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOpe
             logger.debug("Successful plugin call for account {} with result {}", account.getExternalKey(), operationResult);
             return operationResult;
         } catch (final ExecutionException e) {
-            throw rewrapExecutionException(paymentStateContext, e);
+            throw unwrapExceptionFromDispatchedTask(paymentStateContext, e);
         } catch (final TimeoutException e) {
-            throw wrapTimeoutException(paymentStateContext, e);
+            throw unwrapExceptionFromDispatchedTask(paymentStateContext, e);
         } catch (final InterruptedException e) {
             Thread.currentThread().interrupt();
-            throw wrapInterruptedException(paymentStateContext, e);
+            throw unwrapExceptionFromDispatchedTask(paymentStateContext, e);
         }
     }
 
@@ -83,12 +83,5 @@ public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOpe
     //
     protected abstract CallbackOperationResult doCallSpecificOperationCallback() throws CallbackOperationException;
 
-    //
-    // The methods below allow to convert the exceptions thrown back by the Executor into an appropriate  OperationException
-    //
-    protected abstract OperationException rewrapExecutionException(final PaymentStateContext paymentStateContext, final ExecutionException e);
-
-    protected abstract OperationException wrapTimeoutException(final PaymentStateContext paymentStateContext, final TimeoutException e);
-
-    protected abstract OperationException wrapInterruptedException(final PaymentStateContext paymentStateContext, final InterruptedException e);
+    protected abstract OperationException unwrapExceptionFromDispatchedTask(final PaymentStateContext paymentStateContext, final Exception e);
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
index 6d57033..ba6e48d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
@@ -85,24 +85,26 @@ public abstract class PaymentOperation extends OperationCallbackBase<PaymentTran
     }
 
     @Override
-    protected OperationException rewrapExecutionException(final PaymentStateContext paymentStateContext, final ExecutionException e) {
+    protected OperationException unwrapExceptionFromDispatchedTask(final PaymentStateContext paymentStateContext, final Exception e) {
+
+        // If this is an ExecutionException we attempt to extract the cause first
+        final Throwable originalExceptionOrCause = e instanceof ExecutionException ? MoreObjects.firstNonNull(e.getCause(), e) : e;
+
         //
         // Any case of exception (checked or runtime) should lead to a TransactionStatus.UNKNOWN (and a XXX_ERRORED payment state).
         // In order to reach that state we create PaymentTransactionInfoPlugin with an PaymentPluginStatus.UNDEFINED status (and an OperationResult.EXCEPTION).
         //
-        final Throwable originalExceptionOrCause = MoreObjects.firstNonNull(e.getCause(), e);
         if (originalExceptionOrCause instanceof LockFailedException) {
             logger.warn("Failed to lock account {}", paymentStateContext.getAccount().getExternalKey());
+        } else if (originalExceptionOrCause instanceof TimeoutException) {
+            logger.error("Plugin call TIMEOUT for account {}", paymentStateContext.getAccount().getExternalKey());
+        } else if (originalExceptionOrCause instanceof InterruptedException) {
+            logger.error("Plugin call was interrupted for account {}", paymentStateContext.getAccount().getExternalKey());
         } else {
             logger.warn("Payment plugin call threw an exception for account {}", paymentStateContext.getAccount().getExternalKey(), originalExceptionOrCause);
         }
         return convertToUnknownTransactionStatusAndErroredPaymentState(originalExceptionOrCause);
-    }
 
-    @Override
-    protected OperationException wrapTimeoutException(final PaymentStateContext paymentStateContext, final TimeoutException e) {
-        logger.error("Plugin call TIMEOUT for account {}", paymentStateContext.getAccount().getExternalKey());
-        return convertToUnknownTransactionStatusAndErroredPaymentState(e);
     }
 
     //
@@ -122,16 +124,10 @@ public abstract class PaymentOperation extends OperationCallbackBase<PaymentTran
                                                                                                 paymentStateContext.getCallContext().getCreatedDate(),
                                                                                                 PaymentPluginStatus.UNDEFINED,
                                                                                                 null);
-
         paymentStateContext.setPaymentTransactionInfoPlugin(paymentInfoPlugin);
         return new OperationException(e, OperationResult.EXCEPTION);
     }
 
-    @Override
-    protected OperationException wrapInterruptedException(final PaymentStateContext paymentStateContext, final InterruptedException e) {
-        logger.error("Plugin call was interrupted for account {}", paymentStateContext.getAccount().getExternalKey());
-        return new OperationException(e, OperationResult.EXCEPTION);
-    }
 
     @Override
     protected abstract PaymentTransactionInfoPlugin doCallSpecificOperationCallback() throws PaymentPluginApiException;
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 1c11ac8..3aab61a 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
@@ -24,16 +24,20 @@ import javax.annotation.Nullable;
 
 import org.killbill.billing.client.KillBillClientException;
 import org.killbill.billing.client.model.Account;
-import org.killbill.billing.client.model.InvoicePayments;
+import org.killbill.billing.client.model.ComboPaymentTransaction;
 import org.killbill.billing.client.model.Payment;
 import org.killbill.billing.client.model.PaymentMethod;
 import org.killbill.billing.client.model.PaymentMethodPluginDetail;
-import org.killbill.billing.client.model.Payments;
 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 {
 
@@ -47,7 +51,44 @@ public class TestPayment extends TestJaxrsBase {
         testCreateRetrievePayment(account, nonDefaultPaymentMethod.getPaymentMethodId(), UUID.randomUUID().toString(), 2);
     }
 
-    public void testCreateRetrievePayment(final Account account, @Nullable final UUID paymentMethodId,
+    @Test(groups = "slow")
+    public void testComboAuthorization() throws Exception {
+        final Account accountJson = getAccount();
+        accountJson.setAccountId(null);
+
+        final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail();
+        info.setProperties(null);
+
+        final String paymentMethodExternalKey = UUID.randomUUID().toString();
+        final PaymentMethod paymentMethodJson = new PaymentMethod(null, paymentMethodExternalKey, null, true, PLUGIN_NAME, info);
+
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String authTransactionExternalKey = UUID.randomUUID().toString();
+        final PaymentTransaction authTransactionJson = new PaymentTransaction();
+        authTransactionJson.setAmount(BigDecimal.TEN);
+        authTransactionJson.setCurrency(accountJson.getCurrency());
+        authTransactionJson.setPaymentExternalKey(paymentExternalKey);
+        authTransactionJson.setTransactionExternalKey(authTransactionExternalKey);
+        authTransactionJson.setTransactionType("AUTHORIZE");
+
+        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);
+
+
+        // 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");
+
+
+    }
+
+    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();
@@ -59,7 +100,7 @@ public class TestPayment extends TestJaxrsBase {
         authTransaction.setTransactionType("AUTHORIZE");
         final Payment authPayment = killBillClient.createPayment(account.getAccountId(), paymentMethodId, authTransaction, createdBy, reason, comment);
         verifyPayment(account, paymentMethodId, authPayment, PaymentExternalKey, authTransactionExternalKey,
-                            BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, PaymentNb);
+                      BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ZERO, 1, PaymentNb);
 
         // Capture 1
         final String capture1TransactionExternalKey = UUID.randomUUID().toString();
@@ -69,22 +110,25 @@ 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);
+                      BigDecimal.TEN, BigDecimal.ONE, BigDecimal.ZERO, 2, PaymentNb);
         verifyPaymentTransaction(authPayment.getPaymentId(), capturedPayment1.getTransactions().get(1),
-                                       PaymentExternalKey, capture1TransactionExternalKey,
-                                       account, captureTransaction.getAmount(), "CAPTURE");
+                                 PaymentExternalKey, capture1TransactionExternalKey,
+                                 account, captureTransaction.getAmount(), "CAPTURE");
 
         // 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);
+                      BigDecimal.TEN, new BigDecimal("2"), BigDecimal.ZERO, 3, PaymentNb);
         verifyPaymentTransaction(authPayment.getPaymentId(), capturedPayment2.getTransactions().get(2),
-                                       PaymentExternalKey, capture2TransactionExternalKey,
-                                       account, captureTransaction.getAmount(), "CAPTURE");
+                                 PaymentExternalKey, capture2TransactionExternalKey,
+                                 account, captureTransaction.getAmount(), "CAPTURE");
 
         // Refund
         final String refundTransactionExternalKey = UUID.randomUUID().toString();
@@ -96,16 +140,16 @@ public class TestPayment extends TestJaxrsBase {
         refundTransaction.setTransactionExternalKey(refundTransactionExternalKey);
         final Payment refundPayment = killBillClient.refundPayment(refundTransaction, createdBy, reason, comment);
         verifyPayment(account, paymentMethodId, refundPayment, PaymentExternalKey, authTransactionExternalKey,
-                            BigDecimal.TEN, new BigDecimal("2"), new BigDecimal("2"), 4, PaymentNb);
+                      BigDecimal.TEN, new BigDecimal("2"), new BigDecimal("2"), 4, PaymentNb);
         verifyPaymentTransaction(authPayment.getPaymentId(), refundPayment.getTransactions().get(3),
-                                       PaymentExternalKey, refundTransactionExternalKey,
-                                       account, refundTransaction.getAmount(), "REFUND");
+                                 PaymentExternalKey, refundTransactionExternalKey,
+                                 account, refundTransaction.getAmount(), "REFUND");
     }
 
     private void verifyPayment(final Account account, @Nullable final UUID paymentMethodId, final Payment Payment,
-                                     final String PaymentExternalKey, final String authTransactionExternalKey,
-                                     final BigDecimal authAmount, final BigDecimal capturedAmount,
-                                     final BigDecimal refundedAmount, final int nbTransactions, final int PaymentNb) throws KillBillClientException {
+                               final String PaymentExternalKey, final String authTransactionExternalKey,
+                               final BigDecimal authAmount, final BigDecimal capturedAmount,
+                               final BigDecimal refundedAmount, final int nbTransactions, final int PaymentNb) throws KillBillClientException {
         Assert.assertEquals(Payment.getAccountId(), account.getAccountId());
         Assert.assertEquals(Payment.getPaymentMethodId(), Objects.firstNonNull(paymentMethodId, account.getPaymentMethodId()));
         Assert.assertNotNull(Payment.getPaymentId());
@@ -118,7 +162,7 @@ public class TestPayment extends TestJaxrsBase {
         Assert.assertEquals(Payment.getTransactions().size(), nbTransactions);
 
         verifyPaymentTransaction(Payment.getPaymentId(), Payment.getTransactions().get(0),
-                                       PaymentExternalKey, authTransactionExternalKey, account, authAmount, "AUTHORIZE");
+                                 PaymentExternalKey, authTransactionExternalKey, account, authAmount, "AUTHORIZE");
 
         final Payments Payments = killBillClient.getPayments();
         Assert.assertEquals(Payments.size(), PaymentNb);
@@ -132,9 +176,30 @@ public class TestPayment extends TestJaxrsBase {
         Assert.assertEquals(paymentsForAccount.get(PaymentNb - 1), Payment);
     }
 
+    private void verifyComboPayment(final Payment payment,
+                                    final String paymentExternalKey,
+                                    final BigDecimal authAmount,
+                                    final BigDecimal capturedAmount,
+                                    final BigDecimal refundedAmount,
+                                    final int nbTransactions,
+                                    final int PaymentNb) throws KillBillClientException {
+
+        Assert.assertNotNull(payment.getPaymentNumber());
+        Assert.assertEquals(payment.getPaymentExternalKey(), paymentExternalKey);
+        Assert.assertEquals(payment.getAuthAmount().compareTo(authAmount), 0);
+        Assert.assertEquals(payment.getCapturedAmount().compareTo(capturedAmount), 0);
+        Assert.assertEquals(payment.getRefundedAmount().compareTo(refundedAmount), 0);
+        Assert.assertEquals(payment.getTransactions().size(), nbTransactions);
+
+        final Payments Payments = killBillClient.getPayments();
+        Assert.assertEquals(Payments.size(), PaymentNb);
+        Assert.assertEquals(Payments.get(PaymentNb - 1), payment);
+
+    }
+
     private void verifyPaymentTransaction(final UUID PaymentId, final PaymentTransaction PaymentTransaction,
-                                                final String PaymentExternalKey, final String TransactionExternalKey,
-                                                final Account account, @Nullable final BigDecimal amount, final String transactionType) {
+                                          final String PaymentExternalKey, final String TransactionExternalKey,
+                                          final Account account, @Nullable final BigDecimal amount, final String transactionType) {
         Assert.assertEquals(PaymentTransaction.getPaymentId(), PaymentId);
         Assert.assertNotNull(PaymentTransaction.getTransactionId());
         Assert.assertEquals(PaymentTransaction.getTransactionType(), transactionType);