diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index 6f8e466..c988902 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -99,6 +99,7 @@ import org.killbill.billing.payment.api.PaymentMethod;
import org.killbill.billing.payment.api.PaymentOptions;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.api.AuditLevel;
@@ -949,10 +950,15 @@ public class AccountResource extends JaxRsResourceBase {
final UUID paymentMethodId;
if (paymentId != null) {
final Payment initialPayment = paymentApi.getPayment(paymentId, false, false, pluginProperties, callContext);
- final PaymentTransaction pendingTransaction = lookupPendingTransaction(initialPayment,
+ final PaymentTransaction pendingOrSuccessTransaction = lookupPendingOrSuccessTransaction(initialPayment,
json != null ? json.getTransactionId() : null,
json != null ? json.getTransactionExternalKey() : null,
json != null ? json.getTransactionType() : null);
+ // If transaction was already completed, return early (See #626)
+ if (pendingOrSuccessTransaction.getTransactionStatus() == TransactionStatus.SUCCESS) {
+ return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", pendingOrSuccessTransaction.getPaymentId());
+ }
+
paymentMethodId = initialPayment.getPaymentMethodId();
} else {
paymentMethodId = paymentMethodIdStr == null ? account.getPaymentMethodId() : UUID.fromString(paymentMethodIdStr);
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 e0e13b8..653adb8 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
@@ -61,6 +61,7 @@ import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PaymentOptions;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.util.api.AuditUserApi;
import org.killbill.billing.util.api.CustomFieldApiException;
@@ -80,6 +81,7 @@ import com.google.common.base.Function;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
@@ -302,7 +304,12 @@ public class PaymentResource extends ComboPaymentResource {
final UriInfo uriInfo,
final HttpServletRequest request) throws PaymentApiException, AccountApiException {
- final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+ final Iterable<PluginProperty> pluginPropertiesFromBody = extractPluginProperties(json.getProperties());
+
+ final Iterable<PluginProperty> pluginPropertiesFromQuery = extractPluginProperties(pluginPropertiesString);
+
+ final Iterable<PluginProperty> pluginProperties = Iterables.concat(pluginPropertiesFromQuery, pluginPropertiesFromBody);
+
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
final Payment initialPayment = getPaymentByIdOrKey(paymentIdStr, json == null ? null : json.getPaymentExternalKey(), pluginProperties, callContext);
@@ -310,11 +317,17 @@ public class PaymentResource extends ComboPaymentResource {
final BigDecimal amount = json == null ? null : json.getAmount();
final Currency currency = json == null || json.getCurrency() == null ? null : Currency.valueOf(json.getCurrency());
- final PaymentTransaction pendingTransaction = lookupPendingTransaction(initialPayment,
- json != null ? json.getTransactionId() : null,
- json != null ? json.getTransactionExternalKey() : null,
- json != null ? json.getTransactionType() : null);
+ final PaymentTransaction pendingOrSuccessTransaction = lookupPendingOrSuccessTransaction(initialPayment,
+ json != null ? json.getTransactionId() : null,
+ json != null ? json.getTransactionExternalKey() : null,
+ json != null ? json.getTransactionType() : null);
+ // If transaction was already completed, return early (See #626)
+ if (pendingOrSuccessTransaction.getTransactionStatus() == TransactionStatus.SUCCESS) {
+ return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", pendingOrSuccessTransaction.getPaymentId());
+ }
+
+ final PaymentTransaction pendingTransaction = pendingOrSuccessTransaction;
final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
final Payment result;
switch (pendingTransaction.getTransactionType()) {
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentPluginProperties.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentPluginProperties.java
new file mode 100644
index 0000000..83f656e
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPaymentPluginProperties.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.RequestOptions;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.PaymentTransaction;
+import org.killbill.billing.client.model.PluginProperty;
+import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
+import org.killbill.billing.control.plugin.api.OnSuccessPaymentControlResult;
+import org.killbill.billing.control.plugin.api.PaymentControlApiException;
+import org.killbill.billing.control.plugin.api.PaymentControlContext;
+import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
+import org.killbill.billing.control.plugin.api.PriorPaymentControlResult;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.payment.retry.DefaultFailureCallResult;
+import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
+import org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+
+public class TestPaymentPluginProperties extends TestJaxrsBase {
+
+ @Inject
+ private OSGIServiceRegistration<PaymentControlPluginApi> controlPluginRegistry;
+
+ private PluginPropertiesVerificator mockPaymentControlProviderPlugin;
+
+ public static class PluginPropertiesVerificator implements PaymentControlPluginApi {
+
+ public static final String PLUGIN_NAME = "PLUGIN_PROPERTY_VERIFICATOR";
+
+ private Iterable<org.killbill.billing.payment.api.PluginProperty> expectedProperties;
+
+ public PluginPropertiesVerificator() {
+ clearExpectPluginProperties();
+ }
+
+ @Override
+ public PriorPaymentControlResult priorCall(final PaymentControlContext paymentControlContext, final Iterable<org.killbill.billing.payment.api.PluginProperty> properties) throws PaymentControlApiException {
+ assertPluginProperties(properties);
+ return new DefaultPriorPaymentControlResult(false);
+ }
+
+ @Override
+ public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext paymentControlContext, final Iterable<org.killbill.billing.payment.api.PluginProperty> properties) throws PaymentControlApiException {
+ return new DefaultOnSuccessPaymentControlResult();
+ }
+
+ @Override
+ public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext paymentControlContext, final Iterable<org.killbill.billing.payment.api.PluginProperty> properties) throws PaymentControlApiException {
+ return new DefaultFailureCallResult();
+ }
+
+ public void setExpectPluginProperties(final Iterable<org.killbill.billing.payment.api.PluginProperty> expectedProperties) {
+ this.expectedProperties = expectedProperties;
+ }
+
+ public void clearExpectPluginProperties() {
+ this.expectedProperties = ImmutableList.of();
+ }
+
+ private void assertPluginProperties(final Iterable<org.killbill.billing.payment.api.PluginProperty> properties) {
+ for (org.killbill.billing.payment.api.PluginProperty input : properties) {
+ boolean found = false;
+ for (org.killbill.billing.payment.api.PluginProperty expect : expectedProperties) {
+ if (expect.getKey().equals(input.getKey()) && expect.getValue().equals(expect.getValue())) {
+ found = true;
+ break;
+ }
+ }
+ Assert.assertTrue(found);
+ }
+
+ for (org.killbill.billing.payment.api.PluginProperty expect : properties) {
+ boolean found = false;
+ for (org.killbill.billing.payment.api.PluginProperty input : expectedProperties) {
+ if (expect.getKey().equals(input.getKey()) && expect.getValue().equals(expect.getValue())) {
+ found = true;
+ break;
+ }
+ }
+ Assert.assertTrue(found);
+ }
+ }
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ mockPaymentControlProviderPlugin = new PluginPropertiesVerificator();
+ controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
+ @Override
+ public String getPluginSymbolicName() {
+ return null;
+ }
+
+ @Override
+ public String getPluginName() {
+ return PluginPropertiesVerificator.PLUGIN_NAME;
+ }
+
+ @Override
+ public String getRegistrationName() {
+ return PluginPropertiesVerificator.PLUGIN_NAME;
+ }
+ }, mockPaymentControlProviderPlugin);
+ }
+
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
+ mockPaymentControlProviderPlugin.clearExpectPluginProperties();
+ }
+
+ private void addProperty(final String key, final String value, final Map<String, String> dest, final List<org.killbill.billing.payment.api.PluginProperty> expectProperties) {
+ dest.put(key, value);
+ expectProperties.add(new org.killbill.billing.payment.api.PluginProperty(key, value, false));
+ }
+
+ private void addProperty(final String key, final String value, List<PluginProperty> bodyProperties, final List<org.killbill.billing.payment.api.PluginProperty> expectProperties) {
+ bodyProperties.add(new PluginProperty(key, value, false));
+ expectProperties.add(new org.killbill.billing.payment.api.PluginProperty(key, value, false));
+ }
+
+ @Test(groups = "slow")
+ public void testWithQueryPropertiesOnly() throws Exception {
+ final List<org.killbill.billing.payment.api.PluginProperty> expectProperties = new ArrayList<org.killbill.billing.payment.api.PluginProperty>();
+
+ final Map<String, String> queryProperties = new HashMap<String, String>();
+ addProperty("key1", "val1", queryProperties, expectProperties);
+ addProperty("key2", "val2", queryProperties, expectProperties);
+ addProperty("key3", "val3", queryProperties, expectProperties);
+ addProperty("key4", "val4", queryProperties, expectProperties);
+
+ final List<PluginProperty> bodyProperties = new ArrayList<PluginProperty>();
+
+ testInternal(queryProperties, bodyProperties, expectProperties);
+
+ }
+
+ @Test(groups = "slow")
+ public void testWithBodyPropertiesOnly() throws Exception {
+ final List<org.killbill.billing.payment.api.PluginProperty> expectProperties = new ArrayList<org.killbill.billing.payment.api.PluginProperty>();
+
+ final Map<String, String> queryProperties = new HashMap<String, String>();
+
+ final List<PluginProperty> bodyProperties = new ArrayList<PluginProperty>();
+ addProperty("keyXXX1", "valXXXX1", bodyProperties, expectProperties);
+ addProperty("keyXXX2", "valXXXX2", bodyProperties, expectProperties);
+ addProperty("keyXXX3", "valXXXX3", bodyProperties, expectProperties);
+
+ testInternal(queryProperties, bodyProperties, expectProperties);
+
+ }
+
+ @Test(groups = "slow")
+ public void testWithBodyAndQueryProperties() throws Exception {
+ final List<org.killbill.billing.payment.api.PluginProperty> expectProperties = new ArrayList<org.killbill.billing.payment.api.PluginProperty>();
+
+ final Map<String, String> queryProperties = new HashMap<String, String>();
+ addProperty("key1", "val1", queryProperties, expectProperties);
+ addProperty("key2", "val2", queryProperties, expectProperties);
+ addProperty("key3", "val3", queryProperties, expectProperties);
+ addProperty("key4", "val4", queryProperties, expectProperties);
+
+ final List<PluginProperty> bodyProperties = new ArrayList<PluginProperty>();
+ addProperty("keyXXX1", "valXXXX1", bodyProperties, expectProperties);
+ addProperty("keyXXX2", "valXXXX2", bodyProperties, expectProperties);
+ addProperty("keyXXX3", "valXXXX3", bodyProperties, expectProperties);
+
+ testInternal(queryProperties, bodyProperties, expectProperties);
+ }
+
+ @Test(groups = "slow")
+ public void testInternal(final Map<String, String> queryProperties, final List<PluginProperty> bodyProperties, final List<org.killbill.billing.payment.api.PluginProperty> expectProperties) throws Exception {
+ final Account account = createAccountWithDefaultPaymentMethod();
+ final UUID paymentMethodId = account.getPaymentMethodId();
+ final BigDecimal amount = BigDecimal.TEN;
+
+ final String pending = PaymentPluginStatus.PENDING.toString();
+ final ImmutableMap<String, String> pluginProperties = ImmutableMap.<String, String>of(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, pending);
+
+ TransactionType transactionType = TransactionType.AUTHORIZE;
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String authTransactionExternalKey = UUID.randomUUID().toString();
+
+ final Payment initialPayment = createVerifyTransaction(account, paymentMethodId, paymentExternalKey, authTransactionExternalKey, transactionType, amount, pluginProperties);
+
+ mockPaymentControlProviderPlugin.setExpectPluginProperties(expectProperties);
+
+ // Complete operation: first, only specify the payment id
+ final PaymentTransaction completeTransactionByPaymentId = new PaymentTransaction();
+ completeTransactionByPaymentId.setPaymentId(initialPayment.getPaymentId());
+ completeTransactionByPaymentId.setProperties(bodyProperties);
+
+ final RequestOptions basicRequestOptions = basicRequestOptions();
+ final Multimap<String, String> params = LinkedListMultimap.create(basicRequestOptions.getQueryParams());
+ params.putAll(KillBillHttpClient.CONTROL_PLUGIN_NAME, ImmutableList.<String>of(PluginPropertiesVerificator.PLUGIN_NAME));
+
+ final RequestOptions requestOptionsWithParams = basicRequestOptions.extend()
+ .withQueryParams(params).build();
+
+ killBillClient.completePayment(completeTransactionByPaymentId, queryProperties, requestOptionsWithParams);
+ }
+
+ private Payment createVerifyTransaction(final Account account,
+ @Nullable final UUID paymentMethodId,
+ final String paymentExternalKey,
+ final String transactionExternalKey,
+ final TransactionType transactionType,
+ final BigDecimal transactionAmount,
+ final Map<String, String> pluginProperties) throws KillBillClientException {
+ final PaymentTransaction authTransaction = new PaymentTransaction();
+ authTransaction.setAmount(transactionAmount);
+ authTransaction.setCurrency(account.getCurrency());
+ authTransaction.setPaymentExternalKey(paymentExternalKey);
+ authTransaction.setTransactionExternalKey(transactionExternalKey);
+ authTransaction.setTransactionType(transactionType.toString());
+ final Payment payment = killBillClient.createPayment(account.getAccountId(), paymentMethodId, authTransaction, pluginProperties, basicRequestOptions());
+ return payment;
+ }
+}