killbill-aplcache
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 7(+3 -4)
payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java 142(+142 -0)
payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java 49(+49 -0)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java 59(+42 -17)
payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java 34(+24 -10)
payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java 54(+32 -22)
payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java 154(+7 -147)
payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java 52(+12 -40)
payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java 148(+148 -0)
payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java 97(+97 -0)
payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java 2(+1 -1)
payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java 53(+0 -53)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java 16(+16 -0)
pom.xml 2(+1 -1)
profiles/killbill/pom.xml 5(+5 -0)
util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java 94(+94 -0)
util/src/main/resources/ehcache.xml 18(+16 -2)
Details
diff --git a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
index 89c7cb7..73f06f0 100644
--- a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -53,8 +53,9 @@ public interface TenantInternalApi {
public String getPluginConfig(String pluginName, InternalTenantContext tenantContext);
+ public String getPluginPaymentStateMachineConfig(String pluginName, InternalTenantContext tenantContext);
+
public List<String> getTenantValuesForKey(final String key, final InternalTenantContext tenantContext);
public Tenant getTenantByApiKey(final String key) throws TenantApiException;
-
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 2fd7a3d..f767946 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -594,8 +594,8 @@ public class TestOverdueIntegration extends TestOverdueBase {
invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 5, 1), callContext);
- // 2012-05-31 => DAY 30 have to get out of trial before first payment. A payment error, one for each invoice, should be on the bus (because there is no payment method)
- addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // 2012-05-31 => DAY 30 have to get out of trial before first payment. An invoice payment error, one for each invoice, should be on the bus (because there is no payment method)
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT_ERROR);
invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
@@ -610,8 +610,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
checkODState(OverdueWrapper.CLEAR_STATE_NAME);
// 2012-07-05 => DAY 65 - 35 days after invoice
- // Single PAYMENT_ERROR here here triggered by the invoice
- addDaysAndCheckForCompletion(20, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ addDaysAndCheckForCompletion(20, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT_ERROR);
invoiceChecker.checkInvoice(account.getId(), 3, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 30), new LocalDate(2012, 7, 31), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 7, 31), callContext);
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 1b52ef3..d08f54f 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
@@ -30,6 +30,7 @@ public interface JaxrsResource {
public static final String REGISTER_NOTIFICATION_CALLBACK = "registerNotificationCallback";
public static final String UPLOAD_PLUGIN_CONFIG = "uploadPluginConfig";
public static final String UPLOAD_PER_TENANT_CONFIG = "uploadPerTenantConfig";
+ public static final String UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG = "uploadPluginPaymentStateMachineConfig";
public static final String USER_KEY_VALUE = "userKeyValue";
public static final String SEARCH = "search";
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
index ff55211..5a3eb50 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
@@ -225,7 +225,6 @@ public class TenantResource extends JaxRsResourceBase {
return insertTenantKey(TenantKey.PER_TENANT_CONFIG, null, perTenantConfig, uriInfo, "getPerTenantConfiguration", createdBy, reason, comment, request);
}
-
@TimedResource
@GET
@Path("/" + UPLOAD_PER_TENANT_CONFIG)
@@ -248,6 +247,46 @@ public class TenantResource extends JaxRsResourceBase {
return deleteTenantKey(TenantKey.PER_TENANT_CONFIG, null, createdBy, reason, comment, request);
}
+ @TimedResource
+ @POST
+ @Path("/" + UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG + "/{pluginName:" + ANYTHING_PATTERN + "}")
+ @Consumes(TEXT_PLAIN)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Add a per tenant payment state machine for a plugin")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid tenantId supplied")})
+ public Response uploadPluginPaymentStateMachineConfig(final String paymentStateMachineConfig,
+ @PathParam("pluginName") final String pluginName,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws TenantApiException {
+ return insertTenantKey(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, paymentStateMachineConfig, uriInfo, "getPluginPaymentStateMachineConfig", createdBy, reason, comment, request);
+ }
+
+ @TimedResource
+ @GET
+ @Path("/" + UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG + "/{pluginName:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve a per tenant payment state machine for a plugin", response = TenantKeyJson.class)
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid tenantId supplied")})
+ public Response getPluginPaymentStateMachineConfig(@PathParam("pluginName") final String pluginName,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
+ return getTenantKey(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, request);
+ }
+
+ @TimedResource
+ @DELETE
+ @Path("/" + UPLOAD_PLUGIN_PAYMENT_STATE_MACHINE_CONFIG + "/{pluginName:" + ANYTHING_PATTERN + "}")
+ @ApiOperation(value = "Delete a per tenant payment state machine for a plugin")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid tenantId supplied")})
+ public Response deletePluginPaymentStateMachineConfig(@PathParam("pluginName") final String pluginName,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
+ return deleteTenantKey(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, createdBy, reason, comment, request);
+ }
@TimedResource
@POST
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
index a256c61..9058564 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -17,43 +17,58 @@
package org.killbill.billing.payment.api;
-import java.util.UUID;
-
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.payment.core.PaymentTransactionInfoPluginConverter;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
-import org.killbill.commons.locker.GlobalLocker;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class DefaultAdminPaymentApi implements AdminPaymentApi {
+import org.killbill.billing.util.config.definition.PaymentConfig;
- private static final Logger log = LoggerFactory.getLogger(DefaultAdminPaymentApi.class);
+public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPaymentApi {
private final PaymentDao paymentDao;
- private final GlobalLocker locker;
private final InternalCallContextFactory internalCallContextFactory;
@Inject
- public DefaultAdminPaymentApi(final PaymentDao paymentDao, final InternalCallContextFactory internalCallContextFactory, final GlobalLocker locker) {
+ public DefaultAdminPaymentApi(final PaymentConfig paymentConfig, final PaymentDao paymentDao, final InternalCallContextFactory internalCallContextFactory) {
+ super(paymentConfig, internalCallContextFactory);
this.paymentDao = paymentDao;
this.internalCallContextFactory = internalCallContextFactory;
- this.locker = locker;
}
@Override
- public void fixPaymentTransactionState(final Payment payment, PaymentTransaction paymentTransaction, TransactionStatus transactionStatus, @Nullable String lastSuccessPaymentState, String currentPaymentStateName,
- Iterable<PluginProperty> properties, CallContext callContext)
- throws PaymentApiException {
-
+ public void fixPaymentTransactionState(final Payment payment,
+ final PaymentTransaction paymentTransaction,
+ @Nullable final TransactionStatus transactionStatusMaybeNull,
+ @Nullable final String lastSuccessPaymentState,
+ final String currentPaymentStateName,
+ final Iterable<PluginProperty> properties,
+ final CallContext callContext) throws PaymentApiException {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(payment.getAccountId(), callContext);
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(),
- currentPaymentStateName, lastSuccessPaymentState, paymentTransaction.getId(),
- transactionStatus, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
- paymentTransaction.getGatewayErrorCode(), paymentTransaction.getGatewayErrorMsg(), internalCallContext);
+
+ final TransactionStatus transactionStatus;
+ if (transactionStatusMaybeNull == null) {
+ checkNotNullParameter(paymentTransaction.getPaymentInfoPlugin(), "PaymentTransactionInfoPlugin");
+ transactionStatus = PaymentTransactionInfoPluginConverter.toTransactionStatus(paymentTransaction.getPaymentInfoPlugin());
+ } else {
+ transactionStatus = transactionStatusMaybeNull;
+ }
+
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(),
+ null,
+ payment.getId(),
+ paymentTransaction.getTransactionType(),
+ currentPaymentStateName,
+ lastSuccessPaymentState,
+ paymentTransaction.getId(),
+ transactionStatus,
+ paymentTransaction.getProcessedAmount(),
+ paymentTransaction.getProcessedCurrency(),
+ paymentTransaction.getGatewayErrorCode(),
+ paymentTransaction.getGatewayErrorMsg(),
+ internalCallContext);
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
index 7303f80..562889a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
@@ -29,6 +29,7 @@ import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.currency.KillBillMoney;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
@@ -192,6 +193,8 @@ public class DefaultPayment extends EntityBase implements Payment {
final Currency chargebackProcessedCurrency,
final BigDecimal chargebackAmount,
final Currency chargebackCurrency) {
+ BigDecimal unformattedAmountForTransactions = null;
+
final Collection<PaymentTransaction> candidateTransactions = Collections2.<PaymentTransaction>filter(transactions,
new Predicate<PaymentTransaction>() {
@Override
@@ -206,32 +209,34 @@ public class DefaultPayment extends EntityBase implements Payment {
currencyForTransactions = getCurrencyForTransactions(candidateTransactions, false);
if (currencyForTransactions == null) {
// Multiple currencies - cannot compute the total
- return BigDecimal.ZERO;
+ unformattedAmountForTransactions = BigDecimal.ZERO;
} else if (currencyForTransactions != paymentCurrency) {
// Different currency than the main payment currency
- return BigDecimal.ZERO;
+ unformattedAmountForTransactions = BigDecimal.ZERO;
} else {
final BigDecimal amountForTransactions = getAmountForTransactions(candidateTransactions, false);
- return getAmountForTransactions(amountForTransactions,
- takeChargebacksIntoAccount,
- currencyForTransactions,
- chargebackTransactions,
- chargebackProcessedAmount,
- chargebackProcessedCurrency,
- chargebackAmount,
- chargebackCurrency);
+ unformattedAmountForTransactions = getAmountForTransactions(amountForTransactions,
+ takeChargebacksIntoAccount,
+ currencyForTransactions,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
}
} else {
final BigDecimal amountForTransactions = getAmountForTransactions(candidateTransactions, true);
- return getAmountForTransactions(amountForTransactions,
- takeChargebacksIntoAccount,
- currencyForTransactions,
- chargebackTransactions,
- chargebackProcessedAmount,
- chargebackProcessedCurrency,
- chargebackAmount,
- chargebackCurrency);
+ unformattedAmountForTransactions = getAmountForTransactions(amountForTransactions,
+ takeChargebacksIntoAccount,
+ currencyForTransactions,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
}
+
+ return unformattedAmountForTransactions == null || currencyForTransactions == null ? unformattedAmountForTransactions : KillBillMoney.of(unformattedAmountForTransactions, currencyForTransactions);
}
private static BigDecimal getAmountForTransactions(final BigDecimal amountForTransactions,
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index e0751bd..5bfd18d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -89,7 +89,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createAuthorization(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -133,7 +135,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createAuthorization(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -170,7 +174,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = paymentProcessor.createCapture(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -210,7 +215,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createCapture(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -249,7 +256,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = paymentProcessor.createPurchase(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -299,7 +307,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = pluginControlPaymentProcessor.createPurchase(IS_API_PAYMENT, account, nonNulPaymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -333,7 +343,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createVoid(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -371,7 +383,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createVoid(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -407,7 +421,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createRefund(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, amount, currency, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -449,7 +465,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createRefund(IS_API_PAYMENT, account, paymentId, amount, currency, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -488,7 +506,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = paymentProcessor.createCredit(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
SHOULD_LOCK_ACCOUNT, properties, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -531,7 +550,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = pluginControlPaymentProcessor.createCredit(IS_API_PAYMENT, account, paymentMethodId, paymentId, amount, currency, paymentExternalKey, paymentTransactionExternalKey,
properties, paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -606,7 +627,9 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createChargeback(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey, amount, currency, true,
callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -645,7 +668,8 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
payment = pluginControlPaymentProcessor.createChargeback(IS_API_PAYMENT, account, paymentId, paymentTransactionExternalKey, amount, currency,
paymentControlPluginNames, callContext, internalCallContext);
- paymentTransaction = payment.getTransactions().get(payment.getTransactions().size() - 1);
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
+
return payment;
} finally {
logExitAPICall(transactionType,
@@ -677,14 +701,7 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
payment = paymentProcessor.createChargebackReversal(IS_API_PAYMENT, NULL_ATTEMPT_ID, account, paymentId, paymentTransactionExternalKey, null, null, true, callContext, internalCallContext);
- // See https://github.com/killbill/killbill/issues/552
- paymentTransaction = Iterables.<PaymentTransaction>find(Lists.<PaymentTransaction>reverse(payment.getTransactions()),
- new Predicate<PaymentTransaction>() {
- @Override
- public boolean apply(final PaymentTransaction input) {
- return paymentTransactionExternalKey.equals(input.getExternalKey());
- }
- });
+ paymentTransaction = findPaymentTransaction(payment, paymentTransactionExternalKey);
return payment;
} finally {
@@ -869,6 +886,21 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
return paymentMethods;
}
+ private PaymentTransaction findPaymentTransaction(final Payment payment, @Nullable final String paymentTransactionExternalKey) {
+ // By design, the payment transactions are already correctly sorted (by effective date asc)
+ if (paymentTransactionExternalKey == null) {
+ return Iterables.getLast(payment.getTransactions());
+ } else {
+ return Iterables.<PaymentTransaction>find(Lists.<PaymentTransaction>reverse(payment.getTransactions()),
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction input) {
+ return paymentTransactionExternalKey.equals(input.getExternalKey());
+ }
+ });
+ }
+ }
+
private void logEnterAPICall(final String transactionType,
final Account account,
@Nullable final UUID paymentMethodId,
@@ -919,8 +951,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
paymentControlPluginNames);
}
-
-
private void logAPICallInternal(final String prefixMsg,
final String transactionType,
final Account account,
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java
index 74710f0..7e28bdb 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentTransaction.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
+ * 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
@@ -23,6 +24,7 @@ import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityBase;
import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
+import org.killbill.billing.util.currency.KillBillMoney;
public class DefaultPaymentTransaction extends EntityBase implements PaymentTransaction {
@@ -50,9 +52,9 @@ public class DefaultPaymentTransaction extends EntityBase implements PaymentTran
this.transactionType = transactionType;
this.effectiveDate = effectiveDate;
this.status = status;
- this.amount = amount;
+ this.amount = amount == null || currency == null ? amount : KillBillMoney.of(amount, currency);
this.currency = currency;
- this.processedAmount = processedAmount;
+ this.processedAmount = processedAmount == null || processedCurrency == null ? processedAmount : KillBillMoney.of(processedAmount, processedCurrency);
this.processedCurrency = processedCurrency;
this.gatewayErrorCode = gatewayErrorCode;
this.gatewayErrorMsg = gatewayErrorMsg;
diff --git a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
index 1c1d8dc..145507a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
+++ b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentBusEventHandler.java
@@ -101,10 +101,9 @@ public class PaymentBusEventHandler {
} catch (final AccountApiException e) {
log.warn("Failed to process invoice payment", e);
} catch (final PaymentApiException e) {
- // Log as error unless:
- if (e.getCode() != ErrorCode.PAYMENT_NULL_INVOICE.getCode() /* Nothing left to be paid */ &&
- e.getCode() != ErrorCode.PAYMENT_CREATE_PAYMENT.getCode() /* User payment error */) {
- log.error("Failed to process invoice payment {}", e.toString());
+ // Log as warn unless nothing left to be paid
+ if (e.getCode() != ErrorCode.PAYMENT_PLUGIN_API_ABORTED.getCode()) {
+ log.warn("Failed to process invoice payment {}", e.toString());
}
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
new file mode 100644
index 0000000..ae76cb2
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.killbill.automaton.DefaultStateMachineConfig;
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.cache.CacheLoaderArgument;
+import org.killbill.billing.util.cache.TenantStateMachineConfigCacheLoader.LoaderCallback;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.xmlloader.XMLLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.io.Resources;
+
+public class EhCacheStateMachineConfigCache implements StateMachineConfigCache {
+
+ private static final Logger logger = LoggerFactory.getLogger(EhCacheStateMachineConfigCache.class);
+
+ private final TenantInternalApi tenantInternalApi;
+ private final CacheController cacheController;
+ private final CacheInvalidationCallback cacheInvalidationCallback;
+ private final LoaderCallback loaderCallback;
+
+ private StateMachineConfig defaultPaymentStateMachineConfig;
+
+ @Inject
+ public EhCacheStateMachineConfigCache(final TenantInternalApi tenantInternalApi,
+ final CacheControllerDispatcher cacheControllerDispatcher,
+ @Named(PaymentModule.STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK) final CacheInvalidationCallback cacheInvalidationCallback) {
+ this.tenantInternalApi = tenantInternalApi;
+ // Can be null if mis-configured (e.g. missing in ehcache.xml)
+ this.cacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG);
+ this.cacheInvalidationCallback = cacheInvalidationCallback;
+ this.loaderCallback = new LoaderCallback() {
+ public Object loadStateMachineConfig(final String stateMachineConfigXML) throws PaymentApiException {
+ tenantInternalApi.initializeCacheInvalidationCallback(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, cacheInvalidationCallback);
+
+ try {
+ final InputStream stream = new ByteArrayInputStream(stateMachineConfigXML.getBytes());
+ return XMLLoader.getObjectFromStream(new URI("dummy"), stream, DefaultStateMachineConfig.class);
+ } catch (final Exception e) {
+ // TODO 0.17 proper error code
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid payment state machine config");
+ }
+ }
+ };
+ }
+
+ @Override
+ public void loadDefaultPaymentStateMachineConfig(final String url) throws PaymentApiException {
+ if (url != null) {
+ try {
+ defaultPaymentStateMachineConfig = XMLLoader.getObjectFromString(Resources.getResource(url).toExternalForm(), DefaultStateMachineConfig.class);
+ } catch (final Exception e) {
+ // TODO 0.17 proper error code
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid default payment state machine config");
+ }
+ }
+ }
+
+ @Override
+ public StateMachineConfig getPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) throws PaymentApiException {
+ if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID || cacheController == null) {
+ return defaultPaymentStateMachineConfig;
+ }
+
+ final String pluginConfigKey = getCacheKeyName(pluginName, tenantContext);
+ final CacheLoaderArgument cacheLoaderArgument = createCacheLoaderArgument(pluginName);
+ try {
+ StateMachineConfig pluginPaymentStateMachineConfig = (StateMachineConfig) cacheController.get(pluginConfigKey, cacheLoaderArgument);
+ // It means we are using the default state machine config in a multi-tenant deployment
+ if (pluginPaymentStateMachineConfig == null) {
+ pluginPaymentStateMachineConfig = defaultPaymentStateMachineConfig;
+ cacheController.add(pluginConfigKey, pluginPaymentStateMachineConfig);
+ }
+ return pluginPaymentStateMachineConfig;
+ } catch (final IllegalStateException e) {
+ // TODO 0.17 proper error code
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid payment state machine");
+ }
+ }
+
+ // See also DefaultTenantUserApi - we use the same conventions as the main XML cache (so we can re-use the invalidation code)
+ private String getCacheKeyName(final String pluginName, final InternalTenantContext internalContext) {
+ final StringBuilder tenantKey = new StringBuilder(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString());
+ tenantKey.append(pluginName);
+ tenantKey.append(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+ tenantKey.append(internalContext.getTenantRecordId());
+ return tenantKey.toString();
+ }
+
+ @Override
+ public void clearPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) {
+ if (tenantContext.getTenantRecordId() != InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID && cacheController != null) {
+ final String key = getCacheKeyName(pluginName, tenantContext);
+ cacheController.remove(key);
+ }
+ }
+
+ private CacheLoaderArgument createCacheLoaderArgument(final String pluginName) {
+ final Object[] args = new Object[2];
+ args[0] = loaderCallback;
+ args[1] = pluginName;
+ final ObjectType irrelevant = null;
+ final InternalTenantContext notUsed = null;
+ return new CacheLoaderArgument(irrelevant, args, notUsed);
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCache.java b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCache.java
new file mode 100644
index 0000000..64c77ad
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCache.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.api.PaymentApiException;
+
+public interface StateMachineConfigCache {
+
+ public void loadDefaultPaymentStateMachineConfig(String url) throws PaymentApiException;
+
+ public StateMachineConfig getPaymentStateMachineConfig(String pluginName, InternalTenantContext tenantContext) throws PaymentApiException;
+
+ public void clearPaymentStateMachineConfig(String pluginName, InternalTenantContext tenantContext);
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java
new file mode 100644
index 0000000..4a47495
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/StateMachineConfigCacheInvalidationCallback.java
@@ -0,0 +1,49 @@
+/*
+ * 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.payment.caching;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// Similar to TenantCacheInvalidationCallback
+public class StateMachineConfigCacheInvalidationCallback implements CacheInvalidationCallback {
+
+ private final Logger log = LoggerFactory.getLogger(StateMachineConfigCacheInvalidationCallback.class);
+
+ private final StateMachineConfigCache stateMachineConfigCache;
+
+ @Inject
+ public StateMachineConfigCacheInvalidationCallback(final StateMachineConfigCache stateMachineConfigCache) {
+ this.stateMachineConfigCache = stateMachineConfigCache;
+ }
+
+ @Override
+ public void invalidateCache(final TenantKey tenantKey, final Object cookie, final InternalTenantContext tenantContext) {
+ if (cookie == null) {
+ return;
+ }
+
+ log.info("Invalidate payment state machine config cache for pluginName='{}', tenantRecordId='{}'", cookie, tenantContext.getTenantRecordId());
+ stateMachineConfigCache.clearPaymentStateMachineConfig(cookie.toString(), 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 5c6d3d8..5fd6dd2 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
@@ -241,7 +241,7 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
payment.getId(), paymentTransaction.getId(), paymentTransaction.getTransactionStatus(), transactionStatus);
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(payment.getAccountId(), callContext);
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, lastSuccessPaymentState,
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), paymentTransaction.getAttemptId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, lastSuccessPaymentState,
paymentTransaction.getId(), transactionStatus, processedAmount, processedCurrency, gatewayErrorCode, gatewayError, internalCallContext);
return true;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index f9b3dde..7885616 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -49,7 +49,9 @@ 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.payment.core.janitor.IncompletePaymentTransactionTask;
+import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
import org.killbill.billing.payment.core.sm.PaymentAutomatonRunner;
+import org.killbill.billing.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
@@ -70,6 +72,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -327,33 +330,123 @@ public class PaymentProcessor extends ProcessorBase {
);
}
- private Payment performOperation(final boolean isApiPayment, @Nullable final UUID attemptId,
- final TransactionType transactionType, final Account account,
- @Nullable final UUID paymentMethodId, @Nullable final UUID paymentId, @Nullable final UUID transactionId,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- @Nullable final String paymentExternalKey, @Nullable final String paymentTransactionExternalKey,
- final boolean shouldLockAccountAndDispatch, @Nullable final OperationResult overridePluginOperationResult,
+ private Payment performOperation(final boolean isApiPayment,
+ @Nullable final UUID attemptId,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final UUID transactionId,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ @Nullable final String paymentExternalKey,
+ @Nullable final String paymentTransactionExternalKey,
+ final boolean shouldLockAccountAndDispatch,
+ @Nullable final OperationResult overridePluginOperationResult,
final Iterable<PluginProperty> properties,
- final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- final UUID nonNullPaymentId = paymentAutomatonRunner.run(isApiPayment,
- transactionType,
- account,
- attemptId,
- paymentMethodId,
- paymentId,
- transactionId,
- paymentExternalKey,
- paymentTransactionExternalKey,
- amount,
- currency,
- shouldLockAccountAndDispatch,
- overridePluginOperationResult,
- properties,
- callContext,
- internalCallContext);
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ final PaymentStateContext paymentStateContext = paymentAutomatonRunner.buildPaymentStateContext(isApiPayment,
+ transactionType,
+ account,
+ attemptId,
+ paymentMethodId != null ? paymentMethodId : account.getPaymentMethodId(),
+ paymentId,
+ transactionId,
+ paymentExternalKey,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ shouldLockAccountAndDispatch,
+ overridePluginOperationResult,
+ properties,
+ callContext,
+ internalCallContext);
+ final PaymentAutomatonDAOHelper daoHelper = paymentAutomatonRunner.buildDaoHelper(paymentStateContext, internalCallContext);
+
+ String currentStateName = null;
+ if (paymentStateContext.getPaymentId() != null) {
+ PaymentModelDao paymentModelDao = daoHelper.getPayment();
+ if (paymentStateContext.getTransactionId() != null || paymentStateContext.getPaymentTransactionExternalKey() != null) {
+ // If a transaction id or key is passed, we are maybe completing an existing transaction (unless a new key was provided)
+ final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext());
+ PaymentTransactionModelDao transactionToComplete = findTransactionToCompleteAndRunSanityChecks(paymentModelDao, paymentTransactionsForCurrentPayment, paymentStateContext);
+
+ if (transactionToComplete != null) {
+ // For completion calls, always invoke the Janitor first to get the latest state. The state machine will then
+ // prevent disallowed transitions in case the state couldn't be fixed (or if it's already in a final state).
+ final PaymentPluginApi plugin = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), internalCallContext);
+ final List<PaymentTransactionInfoPlugin> pluginTransactions = getPaymentTransactionInfoPlugins(plugin, paymentModelDao, properties, callContext);
+ paymentModelDao = invokeJanitor(paymentModelDao, paymentTransactionsForCurrentPayment, pluginTransactions, internalCallContext);
+
+ final UUID transactionToCompleteId = transactionToComplete.getId();
+ transactionToComplete = Iterables.<PaymentTransactionModelDao>find(paymentTransactionsForCurrentPayment,
+ new Predicate<PaymentTransactionModelDao>() {
+ @Override
+ public boolean apply(final PaymentTransactionModelDao input) {
+ return transactionToCompleteId.equals(input.getId());
+ }
+ });
+
+ // We can't tell where we should be in the state machine - bail (cannot be enforced by the state machine unfortunately because UNKNOWN and PLUGIN_FAILURE are both treated as EXCEPTION)
+ if (transactionToComplete.getTransactionStatus() == TransactionStatus.UNKNOWN) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentStateContext.getTransactionType(), transactionToComplete.getTransactionStatus());
+ }
+
+ paymentStateContext.setPaymentTransactionModelDao(transactionToComplete);
+ }
+ }
+
+ // Use the original payment method id of the payment being completed
+ paymentStateContext.setPaymentMethodId(paymentModelDao.getPaymentMethodId());
+ // We always take the last successful state name to permit retries on failures
+ currentStateName = paymentModelDao.getLastSuccessStateName();
+ }
+
+ // Sanity: no paymentMethodId was passed through API and account does not have a default paymentMethodId
+ if (paymentStateContext.getPaymentMethodId() == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
+ }
+
+ final UUID nonNullPaymentId = paymentAutomatonRunner.run(paymentStateContext, daoHelper, currentStateName, transactionType);
+
return getPayment(nonNullPaymentId, true, properties, callContext, internalCallContext);
}
+ private PaymentTransactionModelDao findTransactionToCompleteAndRunSanityChecks(final PaymentModelDao paymentModelDao,
+ final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment,
+ final PaymentStateContext paymentStateContext) throws PaymentApiException {
+ final Collection<PaymentTransactionModelDao> completionCandidates = new LinkedList<PaymentTransactionModelDao>();
+ for (final PaymentTransactionModelDao paymentTransactionModelDao : paymentTransactionsForCurrentPayment) {
+ // Check if we already have a transaction for that id or key
+ if (!(paymentStateContext.getTransactionId() != null && paymentTransactionModelDao.getId().equals(paymentStateContext.getTransactionId())) &&
+ !(paymentStateContext.getPaymentTransactionExternalKey() != null && paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()))) {
+ // Sanity: if not, prevent multiple PENDING transactions for initial calls (cannot be enforced by the state machine unfortunately)
+ if ((paymentTransactionModelDao.getTransactionType() == TransactionType.AUTHORIZE ||
+ paymentTransactionModelDao.getTransactionType() == TransactionType.PURCHASE ||
+ paymentTransactionModelDao.getTransactionType() == TransactionType.CREDIT) &&
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentTransactionModelDao.getTransactionType(), paymentModelDao.getStateName());
+ } else {
+ continue;
+ }
+ }
+
+ // Sanity: if we already have a transaction for that id or key, the transaction type must match
+ if (paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentStateContext.getTransactionType(), paymentModelDao.getStateName());
+ }
+
+ // UNKNOWN transactions are potential candidates, we'll invoke the Janitor first though
+ if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING || paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
+ completionCandidates.add(paymentTransactionModelDao);
+ }
+ }
+
+ Preconditions.checkState(Iterables.<PaymentTransactionModelDao>size(completionCandidates) <= 1, "There should be at most one completion candidate");
+ return Iterables.<PaymentTransactionModelDao>getLast(completionCandidates, null);
+ }
+
// Used in bulk get API (getAccountPayments / getPayments)
private List<PaymentTransactionInfoPlugin> getPaymentTransactionInfoPluginsIfNeeded(@Nullable final PaymentPluginApi pluginApi, final PaymentModelDao paymentModelDao, final TenantContext context) {
if (pluginApi == null) {
@@ -402,15 +495,7 @@ public class PaymentProcessor extends ProcessorBase {
return toPayment(paymentModelDao, transactionsForPayment, pluginTransactions, tenantContextWithAccountRecordId);
}
- // Used in bulk get API (getAccountPayments)
- private Payment toPayment(final PaymentModelDao curPaymentModelDao, final Iterable<PaymentTransactionModelDao> curTransactionsModelDao, @Nullable final Iterable<PaymentTransactionInfoPlugin> pluginTransactions, final InternalTenantContext internalTenantContext) {
- final Ordering<PaymentTransaction> perPaymentTransactionOrdering = Ordering.<PaymentTransaction>from(new Comparator<PaymentTransaction>() {
- @Override
- public int compare(final PaymentTransaction o1, final PaymentTransaction o2) {
- return o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
- }
- });
-
+ private PaymentModelDao invokeJanitor(final PaymentModelDao curPaymentModelDao, final Collection<PaymentTransactionModelDao> curTransactionsModelDao, @Nullable final Iterable<PaymentTransactionInfoPlugin> pluginTransactions, final InternalTenantContext internalTenantContext) {
// Need to filter for optimized codepaths looking up by account_record_id
final Iterable<PaymentTransactionModelDao> filteredTransactions = Iterables.filter(curTransactionsModelDao, new Predicate<PaymentTransactionModelDao>() {
@Override
@@ -420,7 +505,7 @@ public class PaymentProcessor extends ProcessorBase {
});
PaymentModelDao newPaymentModelDao = curPaymentModelDao;
- final Collection<PaymentTransaction> transactions = new LinkedList<PaymentTransaction>();
+ final Collection<PaymentTransactionModelDao> transactionsModelDao = new LinkedList<PaymentTransactionModelDao>();
for (final PaymentTransactionModelDao curPaymentTransactionModelDao : filteredTransactions) {
PaymentTransactionModelDao newPaymentTransactionModelDao = curPaymentTransactionModelDao;
@@ -435,6 +520,23 @@ public class PaymentProcessor extends ProcessorBase {
}
}
+ transactionsModelDao.add(newPaymentTransactionModelDao);
+ }
+
+ curTransactionsModelDao.clear();
+ curTransactionsModelDao.addAll(transactionsModelDao);
+
+ return newPaymentModelDao;
+ }
+
+ // Used in bulk get API (getAccountPayments)
+ private Payment toPayment(final PaymentModelDao curPaymentModelDao, final Collection<PaymentTransactionModelDao> curTransactionsModelDao, @Nullable final Iterable<PaymentTransactionInfoPlugin> pluginTransactions, final InternalTenantContext internalTenantContext) {
+ final Collection<PaymentTransactionModelDao> transactionsModelDao = new LinkedList<PaymentTransactionModelDao>(curTransactionsModelDao);
+ final PaymentModelDao newPaymentModelDao = invokeJanitor(curPaymentModelDao, transactionsModelDao, pluginTransactions, internalTenantContext);
+
+ final Collection<PaymentTransaction> transactions = new LinkedList<PaymentTransaction>();
+ for (final PaymentTransactionModelDao newPaymentTransactionModelDao : transactionsModelDao) {
+ final PaymentTransactionInfoPlugin paymentTransactionInfoPlugin = findPaymentTransactionInfoPlugin(newPaymentTransactionModelDao, pluginTransactions);
final PaymentTransaction transaction = new DefaultPaymentTransaction(newPaymentTransactionModelDao.getId(),
newPaymentTransactionModelDao.getAttemptId(),
newPaymentTransactionModelDao.getTransactionExternalKey(),
@@ -454,14 +556,21 @@ public class PaymentProcessor extends ProcessorBase {
transactions.add(transaction);
}
+ final Ordering<PaymentTransaction> perPaymentTransactionOrdering = Ordering.<PaymentTransaction>from(new Comparator<PaymentTransaction>() {
+ @Override
+ public int compare(final PaymentTransaction o1, final PaymentTransaction o2) {
+ return o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+ }
+ });
final List<PaymentTransaction> sortedTransactions = perPaymentTransactionOrdering.immutableSortedCopy(transactions);
- return new DefaultPayment(curPaymentModelDao.getId(),
- curPaymentModelDao.getCreatedDate(),
- curPaymentModelDao.getUpdatedDate(),
- curPaymentModelDao.getAccountId(),
- curPaymentModelDao.getPaymentMethodId(),
- curPaymentModelDao.getPaymentNumber(),
- curPaymentModelDao.getExternalKey(),
+
+ return new DefaultPayment(newPaymentModelDao.getId(),
+ newPaymentModelDao.getCreatedDate(),
+ newPaymentModelDao.getUpdatedDate(),
+ newPaymentModelDao.getAccountId(),
+ newPaymentModelDao.getPaymentMethodId(),
+ newPaymentModelDao.getPaymentNumber(),
+ newPaymentModelDao.getExternalKey(),
sortedTransactions);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
index 9a87984..dcd1707 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
@@ -17,11 +17,14 @@
package org.killbill.billing.payment.core.sm.control;
+import java.util.List;
+
import org.joda.time.DateTime;
import org.killbill.automaton.OperationException;
import org.killbill.automaton.State;
import org.killbill.automaton.State.LeavingStateCallback;
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.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
@@ -38,6 +41,10 @@ import com.google.common.collect.ImmutableList;
public class DefaultControlInitiated implements LeavingStateCallback {
+ private static final ImmutableList<TransactionStatus> TRANSIENT_TRANSACTION_STATUSES = ImmutableList.<TransactionStatus>builder().add(TransactionStatus.PENDING)
+ .add(TransactionStatus.UNKNOWN)
+ .build();
+
private final PluginControlPaymentAutomatonRunner pluginControlPaymentAutomatonRunner;
private final PaymentStateControlContext stateContext;
private final State initialState;
@@ -59,6 +66,19 @@ public class DefaultControlInitiated implements LeavingStateCallback {
public void leavingState(final State state) throws OperationException {
final DateTime utcNow = pluginControlPaymentAutomatonRunner.getClock().getUTCNow();
+ // Retrieve the associated payment transaction, if any
+ PaymentTransactionModelDao paymentTransactionModelDaoCandidate = null;
+ if (stateContext.getTransactionId() != null) {
+ paymentTransactionModelDaoCandidate = paymentDao.getPaymentTransaction(stateContext.getTransactionId(), stateContext.getInternalCallContext());
+ Preconditions.checkNotNull(paymentTransactionModelDaoCandidate, "paymentTransaction cannot be null for id " + stateContext.getTransactionId());
+ } else if (stateContext.getPaymentTransactionExternalKey() != null) {
+ final List<PaymentTransactionModelDao> paymentTransactionModelDaos = paymentDao.getPaymentTransactionsByExternalKey(stateContext.getPaymentTransactionExternalKey(), stateContext.getInternalCallContext());
+ if (!paymentTransactionModelDaos.isEmpty()) {
+ paymentTransactionModelDaoCandidate = paymentTransactionModelDaos.get(paymentTransactionModelDaos.size() - 1);
+ }
+ }
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentTransactionModelDaoCandidate != null && TRANSIENT_TRANSACTION_STATUSES.contains(paymentTransactionModelDaoCandidate.getTransactionStatus()) ? paymentTransactionModelDaoCandidate : null;
+
if (stateContext.getPaymentId() != null && stateContext.getPaymentExternalKey() == null) {
final PaymentModelDao payment = paymentDao.getPayment(stateContext.getPaymentId(), stateContext.getInternalCallContext());
Preconditions.checkNotNull(payment, "payment cannot be null for id " + stateContext.getPaymentId());
@@ -67,9 +87,8 @@ public class DefaultControlInitiated implements LeavingStateCallback {
} else if (stateContext.getPaymentExternalKey() == null) {
stateContext.setPaymentExternalKey(UUIDs.randomUUID().toString());
}
- if (stateContext.getTransactionId() != null && stateContext.getPaymentTransactionExternalKey() == null) {
- final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(stateContext.getTransactionId(), stateContext.getInternalCallContext());
- Preconditions.checkNotNull(paymentTransactionModelDao, "paymentTransaction cannot be null for id " + stateContext.getTransactionId());
+
+ if (paymentTransactionModelDao != null) {
stateContext.setPaymentTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey());
} else if (stateContext.getPaymentTransactionExternalKey() == null) {
stateContext.setPaymentTransactionExternalKey(UUIDs.randomUUID().toString());
@@ -82,20 +101,26 @@ public class DefaultControlInitiated implements LeavingStateCallback {
if (state.getName().equals(initialState.getName()) || state.getName().equals(retriedState.getName())) {
try {
- //
- // We don't serialize any properties at this stage to avoid serializing sensitive information.
- // However, if after going through the control plugins, the attempt end up in RETRIED state,
- // the properties will be serialized in the enteringState callback (any plugin that sets a
- // retried date is responsible to correctly remove sensitive information such as CVV, ...)
- //
- final byte[] serializedProperties = PluginPropertySerializer.serialize(ImmutableList.<PluginProperty>of());
- final PaymentAttemptModelDao attempt = new PaymentAttemptModelDao(stateContext.getAccount().getId(), stateContext.getPaymentMethodId(),
- utcNow, utcNow, stateContext.getPaymentExternalKey(), stateContext.getTransactionId(),
- stateContext.getPaymentTransactionExternalKey(), transactionType, initialState.getName(),
- stateContext.getAmount(), stateContext.getCurrency(),
- stateContext.getPaymentControlPluginNames(), serializedProperties);
-
- pluginControlPaymentAutomatonRunner.getPaymentDao().insertPaymentAttemptWithProperties(attempt, stateContext.getInternalCallContext());
+ final PaymentAttemptModelDao attempt;
+ if (paymentTransactionModelDao != null && paymentTransactionModelDao.getAttemptId() != null) {
+ attempt = pluginControlPaymentAutomatonRunner.getPaymentDao().getPaymentAttempt(paymentTransactionModelDao.getAttemptId(), stateContext.getInternalCallContext());
+ Preconditions.checkNotNull(attempt, "attempt cannot be null for id " + paymentTransactionModelDao.getAttemptId());
+ } else {
+ //
+ // We don't serialize any properties at this stage to avoid serializing sensitive information.
+ // However, if after going through the control plugins, the attempt end up in RETRIED state,
+ // the properties will be serialized in the enteringState callback (any plugin that sets a
+ // retried date is responsible to correctly remove sensitive information such as CVV, ...)
+ //
+ final byte[] serializedProperties = PluginPropertySerializer.serialize(ImmutableList.<PluginProperty>of());
+
+ attempt = new PaymentAttemptModelDao(stateContext.getAccount().getId(), stateContext.getPaymentMethodId(),
+ utcNow, utcNow, stateContext.getPaymentExternalKey(), stateContext.getTransactionId(),
+ stateContext.getPaymentTransactionExternalKey(), transactionType, initialState.getName(),
+ stateContext.getAmount(), stateContext.getCurrency(),
+ stateContext.getPaymentControlPluginNames(), serializedProperties);
+ pluginControlPaymentAutomatonRunner.getPaymentDao().insertPaymentAttemptWithProperties(attempt, stateContext.getInternalCallContext());
+ }
stateContext.setAttemptId(attempt.getId());
} catch (final PluginPropertySerializerException e) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
index c6545dc..fa55ff0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
@@ -57,6 +57,10 @@ public class PaymentAutomatonDAOHelper {
private final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
private final PersistentBus eventBus;
+ // Cached
+ private String pluginName = null;
+ private PaymentPluginApi paymentPluginApi = null;
+
// Used to build new payments and transactions
public PaymentAutomatonDAOHelper(final PaymentStateContext paymentStateContext,
final DateTime utcNow, final PaymentDao paymentDao,
@@ -128,6 +132,7 @@ public class PaymentAutomatonDAOHelper {
final String lastSuccessPaymentState = paymentSMHelper.isSuccessState(currentPaymentStateName) ? currentPaymentStateName : null;
paymentDao.updatePaymentAndTransactionOnCompletion(paymentStateContext.getAccount().getId(),
+ paymentStateContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getTransactionType(),
currentPaymentStateName,
@@ -145,27 +150,24 @@ public class PaymentAutomatonDAOHelper {
}
public String getPaymentProviderPluginName() throws PaymentApiException {
+ if (pluginName != null) {
+ return pluginName;
+ }
+
final UUID paymentMethodId = paymentStateContext.getPaymentMethodId();
final PaymentMethodModelDao methodDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodId, internalCallContext);
if (methodDao == null) {
throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
}
- return methodDao.getPluginName();
+ pluginName = methodDao.getPluginName();
+ return pluginName;
}
- public PaymentPluginApi getPaymentProviderPlugin() throws PaymentApiException {
+ public PaymentPluginApi getPaymentPluginApi() throws PaymentApiException {
final String pluginName = getPaymentProviderPluginName();
return getPaymentPluginApi(pluginName);
}
- public PaymentPluginApi getPaymentPluginApi(final String pluginName) throws PaymentApiException {
- final PaymentPluginApi pluginApi = pluginRegistry.getServiceForName(pluginName);
- if (pluginApi == null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_PLUGIN, pluginName);
- }
- return pluginApi;
- }
-
public PaymentModelDao getPayment() throws PaymentApiException {
final PaymentModelDao paymentModelDao;
paymentModelDao = paymentDao.getPayment(paymentStateContext.getPaymentId(), internalCallContext);
@@ -183,6 +185,18 @@ public class PaymentAutomatonDAOHelper {
return paymentDao;
}
+ private PaymentPluginApi getPaymentPluginApi(final String pluginName) throws PaymentApiException {
+ if (paymentPluginApi != null) {
+ return paymentPluginApi;
+ }
+
+ paymentPluginApi = pluginRegistry.getServiceForName(pluginName);
+ if (paymentPluginApi == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_PLUGIN, pluginName);
+ }
+ return paymentPluginApi;
+ }
+
private PaymentModelDao buildNewPaymentModelDao() {
final DateTime createdDate = utcNow;
final DateTime updatedDate = utcNow;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
index 99c913b..d7a03e2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -20,7 +20,6 @@ package org.killbill.billing.payment.core.sm;
import java.math.BigDecimal;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -35,6 +34,7 @@ import org.killbill.automaton.State;
import org.killbill.automaton.State.EnteringStateCallback;
import org.killbill.automaton.State.LeavingStateCallback;
import org.killbill.automaton.StateMachine;
+import org.killbill.automaton.StateMachineConfig;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
@@ -68,7 +68,6 @@ import org.killbill.billing.payment.core.sm.payments.VoidOperation;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
import org.killbill.billing.payment.dispatcher.PluginDispatcher;
-import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.config.definition.PaymentConfig;
@@ -76,10 +75,7 @@ import org.killbill.bus.api.PersistentBus;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
+import com.google.common.base.MoreObjects;
public class PaymentAutomatonRunner {
@@ -89,6 +85,7 @@ public class PaymentAutomatonRunner {
protected final PluginDispatcher<OperationResult> paymentPluginDispatcher;
protected final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
protected final Clock clock;
+
private final PersistentBus eventBus;
private final PaymentConfig paymentConfig;
@@ -110,42 +107,57 @@ public class PaymentAutomatonRunner {
this.paymentConfig = paymentConfig;
final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit());
this.paymentPluginDispatcher = new PluginDispatcher<OperationResult>(paymentPluginTimeoutSec, executors);
-
}
- public UUID run(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID attemptId, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final UUID transactionId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final boolean shouldLockAccount, final OperationResult overridePluginOperationResult, final Iterable<PluginProperty> properties,
- final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- final DateTime utcNow = clock.getUTCNow();
-
+ public PaymentStateContext buildPaymentStateContext(final boolean isApiPayment,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID attemptId,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final UUID transactionId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final boolean shouldLockAccount,
+ final OperationResult overridePluginOperationResult,
+ final Iterable<PluginProperty> properties,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
// Retrieve the payment id from the payment external key if needed
final UUID effectivePaymentId = paymentId != null ? paymentId : retrievePaymentId(paymentExternalKey, internalCallContext);
- final PaymentStateContext paymentStateContext = new PaymentStateContext(isApiPayment, effectivePaymentId, transactionId, attemptId, paymentExternalKey, paymentTransactionExternalKey, transactionType,
- account, paymentMethodId, amount, currency, shouldLockAccount, overridePluginOperationResult, properties, internalCallContext, callContext);
-
- final PaymentAutomatonDAOHelper daoHelper = new PaymentAutomatonDAOHelper(paymentStateContext, utcNow, paymentDao, pluginRegistry, internalCallContext, eventBus, paymentSMHelper);
-
- final UUID effectivePaymentMethodId;
- final String currentStateName;
- if (effectivePaymentId != null) {
- final PaymentModelDao paymentModelDao = daoHelper.getPayment();
- effectivePaymentMethodId = paymentModelDao.getPaymentMethodId();
- currentStateName = paymentModelDao.getLastSuccessStateName() != null ? paymentModelDao.getLastSuccessStateName() : paymentSMHelper.getInitStateNameForTransaction();
-
- // Check for illegal states (should never happen)
- Preconditions.checkState(currentStateName != null, "State name cannot be null for payment " + effectivePaymentId);
- Preconditions.checkState(paymentMethodId == null || effectivePaymentMethodId.equals(paymentMethodId), "Specified payment method id " + paymentMethodId + " doesn't match the one on the payment " + effectivePaymentMethodId);
- } else {
- // If the payment method is not specified, retrieve the default one on the account; it could still be null, in which case
- //
- effectivePaymentMethodId = paymentMethodId != null ? paymentMethodId : account.getPaymentMethodId();
- currentStateName = paymentSMHelper.getInitStateNameForTransaction();
- }
+ return new PaymentStateContext(isApiPayment,
+ effectivePaymentId,
+ transactionId,
+ attemptId,
+ paymentExternalKey,
+ paymentTransactionExternalKey,
+ transactionType,
+ account,
+ paymentMethodId,
+ amount,
+ currency,
+ shouldLockAccount,
+ overridePluginOperationResult,
+ properties,
+ internalCallContext,
+ callContext);
+ }
+
+ public PaymentAutomatonDAOHelper buildDaoHelper(final PaymentStateContext paymentStateContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ final DateTime utcNow = clock.getUTCNow();
+
+ return new PaymentAutomatonDAOHelper(paymentStateContext, utcNow, paymentDao, pluginRegistry, internalCallContext, eventBus, paymentSMHelper);
+ }
- paymentStateContext.setPaymentMethodId(effectivePaymentMethodId);
+ public UUID run(final PaymentStateContext paymentStateContext,
+ final PaymentAutomatonDAOHelper daoHelper,
+ @Nullable final String currentStateNameOrNull,
+ final TransactionType transactionType) throws PaymentApiException {
+ final String currentStateName = MoreObjects.firstNonNull(currentStateNameOrNull, paymentSMHelper.getInitStateNameForTransaction());
final OperationCallback operationCallback;
final LeavingStateCallback leavingStateCallback;
@@ -190,7 +202,7 @@ public class PaymentAutomatonRunner {
throw new IllegalStateException("Unsupported transaction type " + transactionType);
}
- runStateMachineOperation(currentStateName, transactionType, leavingStateCallback, operationCallback, enteringStateCallback, account.getId(), getInvoiceId(properties));
+ runStateMachineOperation(currentStateName, transactionType, leavingStateCallback, operationCallback, enteringStateCallback, paymentStateContext, daoHelper);
return paymentStateContext.getPaymentId();
}
@@ -206,39 +218,33 @@ public class PaymentAutomatonRunner {
return clock;
}
- protected void runStateMachineOperation(final String initialStateName, final TransactionType transactionType,
- final LeavingStateCallback leavingStateCallback, final OperationCallback operationCallback, final EnteringStateCallback enteringStateCallback,
- final UUID accountId, final String invoiceId) throws PaymentApiException {
+ private void runStateMachineOperation(final String initialStateName,
+ final TransactionType transactionType,
+ final LeavingStateCallback leavingStateCallback,
+ final OperationCallback operationCallback,
+ final EnteringStateCallback enteringStateCallback,
+ final PaymentStateContext paymentStateContext,
+ final PaymentAutomatonDAOHelper daoHelper) throws PaymentApiException {
try {
- final StateMachine initialStateMachine = paymentSMHelper.getStateMachineForStateName(initialStateName);
+ final StateMachineConfig stateMachineConfig = paymentSMHelper.getStateMachineConfig(daoHelper.getPaymentProviderPluginName(), paymentStateContext.getInternalCallContext());
+ final StateMachine initialStateMachine = stateMachineConfig.getStateMachineForState(initialStateName);
final State initialState = initialStateMachine.getState(initialStateName);
- final Operation operation = paymentSMHelper.getOperationForTransaction(transactionType);
+ final Operation operation = paymentSMHelper.getOperationForTransaction(stateMachineConfig, transactionType);
initialState.runOperation(operation, operationCallback, enteringStateCallback, leavingStateCallback);
} catch (final MissingEntryException e) {
throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INVALID_OPERATION, transactionType, initialStateName);
} catch (final OperationException e) {
if (e.getCause() == null) {
- throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
} else if (e.getCause() instanceof PaymentApiException) {
throw (PaymentApiException) e.getCause();
} else {
- throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
+ throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
}
}
}
- private String getInvoiceId(final Iterable<PluginProperty> properties) {
- final PluginProperty invoiceProperty = Iterables.tryFind(properties, new Predicate<PluginProperty>() {
- @Override
- public boolean apply(final PluginProperty input) {
- return InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID.equals(input.getKey());
- }
- }).orNull();
-
- return invoiceProperty == null || invoiceProperty.getValue() == null ? null : invoiceProperty.getValue().toString();
- }
-
private UUID retrievePaymentId(@Nullable final String paymentExternalKey, final InternalCallContext internalCallContext) {
if (paymentExternalKey == null) {
return null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
index 0b08255..5e07f04 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/ChargebackInitiated.java
@@ -17,7 +17,14 @@
package org.killbill.billing.payment.core.sm.payments;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.automaton.OperationException;
import org.killbill.automaton.OperationResult;
+import org.killbill.automaton.State;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
@@ -25,6 +32,7 @@ import org.killbill.billing.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
public class ChargebackInitiated extends PaymentLeavingStateCallback {
@@ -34,33 +42,35 @@ public class ChargebackInitiated extends PaymentLeavingStateCallback {
}
@Override
- protected void validatePaymentIdAndTransactionType(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- if (OperationResult.FAILURE.equals(paymentStateContext.getOverridePluginOperationResult()) && !existingPaymentTransactions.iterator().hasNext()) {
- // Chargeback reversals can only happen after a successful chargeback
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentStateContext.getPaymentId());
- }
- super.validatePaymentIdAndTransactionType(existingPaymentTransactions);
- }
+ public void leavingState(final State oldState) throws OperationException {
+ // Sanity: chargeback reversals can only happen after a successful chargeback
+ if (OperationResult.FAILURE.equals(paymentStateContext.getOverridePluginOperationResult())) {
+ final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = paymentStateContext.getPaymentId() != null ?
+ daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()) :
+ ImmutableList.<PaymentTransactionModelDao>of();
+ final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
- @Override
- protected void validateUniqueTransactionExternalKey(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- // If no key specified, system will allocate a unique one later, there is nothing to check
- if (paymentStateContext.getPaymentTransactionExternalKey() == null) {
- return;
+ if (Iterables.<PaymentTransactionModelDao>isEmpty(existingPaymentTransactionsForTransactionIdOrKey)) {
+ // Chargeback reversals can only happen after a successful chargeback
+ throw new OperationException(new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentStateContext.getPaymentId()));
+ }
}
- // The main difference with the default implementation is that an existing transaction in a SUCCESS state can exist (chargeback reversal)
- if (Iterables.any(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+ super.leavingState(oldState);
+ }
+
+ private Iterable<PaymentTransactionModelDao> filterExistingPaymentTransactionsForTransactionIdOrKey(final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, @Nullable final UUID paymentTransactionId, @Nullable final String paymentTransactionExternalKey) {
+ return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
@Override
public boolean apply(final PaymentTransactionModelDao input) {
- // An existing transaction for a different payment (to do really well, we should also check on paymentExternalKey which is not available here)
- return (paymentStateContext.getPaymentId() != null && input.getPaymentId().compareTo(paymentStateContext.getPaymentId()) != 0) ||
- // Or, an existing transaction for a different account.
- (!input.getAccountRecordId().equals(paymentStateContext.getInternalCallContext().getAccountRecordId()));
-
+ if (paymentTransactionId != null && input.getId().equals(paymentTransactionId)) {
+ return true;
+ }
+ if (paymentTransactionExternalKey != null && input.getTransactionExternalKey().equals(paymentTransactionExternalKey)) {
+ return true;
+ }
+ return false;
}
- })) {
- throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
- }
+ });
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
index 8c45d50..90f8599 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
*
* Groupon 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
@@ -17,31 +17,19 @@
package org.killbill.billing.payment.core.sm.payments;
-import java.util.List;
-import java.util.UUID;
-
-import javax.annotation.Nullable;
-
import org.killbill.automaton.OperationException;
import org.killbill.automaton.State;
import org.killbill.automaton.State.LeavingStateCallback;
-import org.killbill.billing.ErrorCode;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.TransactionStatus;
-import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
import org.killbill.billing.payment.core.sm.PaymentStateContext;
-import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-
public abstract class PaymentLeavingStateCallback implements LeavingStateCallback {
- private final Logger logger = LoggerFactory.getLogger(PaymentLeavingStateCallback.class);
+ private static final Logger logger = LoggerFactory.getLogger(PaymentLeavingStateCallback.class);
protected final PaymentAutomatonDAOHelper daoHelper;
protected final PaymentStateContext paymentStateContext;
@@ -55,141 +43,13 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
public void leavingState(final State oldState) throws OperationException {
logger.debug("Leaving state {}", oldState.getName());
- // Create or update the payment and transaction
try {
- // No paymentMethodId was passed through API and account does not have a default paymentMethodId
- if (paymentStateContext.getPaymentMethodId() == null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
+ // We always create a new transaction (even in case of PAYMENT_FAILURE or PLUGIN_FAILURE) except for PENDING payments (completion)
+ if (paymentStateContext.getPaymentTransactionModelDao() == null || paymentStateContext.getPaymentTransactionModelDao().getTransactionStatus() != TransactionStatus.PENDING) {
+ daoHelper.createNewPaymentTransaction();
}
-
- // If we were given a paymentId (or existing paymentExternalId -> effectivePaymentId) we first fetch existing transactions (required for sanity and handling PENDING transactions)
- final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = paymentStateContext.getPaymentId() != null ?
- daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()) :
- ImmutableList.<PaymentTransactionModelDao>of();
-
- //
- // Extract existing transaction matching the transactionId if specified (for e.g notifyPendingTransactionOfStateChanged), or based on transactionExternalKey
- //
- final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
-
- // Validate the payment transactions belong to the right payment
- validatePaymentIdAndTransactionType(existingPaymentTransactionsForTransactionIdOrKey);
-
- // Validate some constraints on the unicity of that paymentTransactionExternalKey
- validateUniqueTransactionExternalKey(existingPaymentTransactionsForTransactionIdOrKey);
-
- //
- // Handle PENDING case:
- // a) If we have a PENDING transaction for the same (payment transaction) key, this is a completion and we want to re-use the same transaction
- // b) If we have a PENDING transaction for a different (payment transaction) key, and for an initial request (AUTH, PURCHASE, CREDIT), we FAIL the request
- // (unfortunately this cannot be caught by the state machine because the transition XXX_PENDING -> _SUCCESS needs to be allowed and this is irrespective of the keys)
- // c) If we have a PENDING transaction for a different (payment transaction) key, and for other follow-up request (CAPTURE, REFUND, ..), we ignore it and create a new transaction
- //
- final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType = filterPendingTransactionsForPaymentAndTransactionType(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionType());
-
- // Case b)
- validateUniqueInitialPendingTransaction(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getTransactionType(), paymentStateContext.getPaymentTransactionExternalKey());
-
-
- final PaymentTransactionModelDao pendingPaymentTransaction = filterPendingTransactionsForTransactionKey(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getPaymentTransactionExternalKey());
- if (pendingPaymentTransaction != null) {
- // Case a) Set the current paymentTransaction in the context (needed for the state machine logic)
- paymentStateContext.setPaymentTransactionModelDao(pendingPaymentTransaction);
- return;
- }
-
- // At this point we are left with PAYMENT_FAILURE, PLUGIN_FAILURE or nothing, and we validated the uniqueness of the paymentTransactionExternalKey so we will create a new row
- daoHelper.createNewPaymentTransaction();
-
- } catch (PaymentApiException e) {
+ } catch (final PaymentApiException e) {
throw new OperationException(e);
}
}
-
- private void validateUniqueInitialPendingTransaction(final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType, final TransactionType transactionType, final String paymentTransactionExternalKey) throws PaymentApiException {
- if (transactionType != TransactionType.AUTHORIZE &&
- transactionType != TransactionType.PURCHASE &&
- transactionType != TransactionType.CREDIT) {
- return;
- }
-
- final PaymentTransactionModelDao existingPendingTransactionForDifferentKey = Iterables.tryFind(pendingTransactionsForPaymentAndTransactionType, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- return !input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
- }
- }).orNull();
- if (existingPendingTransactionForDifferentKey != null) {
- throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
- }
- }
-
- protected Iterable<PaymentTransactionModelDao> filterExistingPaymentTransactionsForTransactionIdOrKey(final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, @Nullable final UUID paymentTransactionId, @Nullable final String paymentTransactionExternalKey) throws PaymentApiException {
- return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- if (paymentTransactionId != null && input.getId().equals(paymentTransactionId)) {
- return true;
- }
- if (paymentTransactionExternalKey != null && input.getTransactionExternalKey().equals(paymentTransactionExternalKey)) {
- return true;
- }
- return false;
- }
- });
- }
-
- protected Iterable<PaymentTransactionModelDao> filterPendingTransactionsForPaymentAndTransactionType(final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, final TransactionType transactionType) throws PaymentApiException {
- return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- return input.getTransactionStatus() == TransactionStatus.PENDING &&
- input.getTransactionType() == transactionType;
- }
- });
- }
-
- protected PaymentTransactionModelDao filterPendingTransactionsForTransactionKey(final Iterable<PaymentTransactionModelDao> existingPendingPaymentTransactions, final String paymentTransactionExternalKey) throws PaymentApiException {
- return Iterables.tryFind(existingPendingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- return input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
- }
- }).orNull();
- }
-
- protected void validateUniqueTransactionExternalKey(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- // If no key specified, system will allocate a unique one later, there is nothing to check
- if (paymentStateContext.getPaymentTransactionExternalKey() == null) {
- return;
- }
-
- if (Iterables.any(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
- @Override
- public boolean apply(final PaymentTransactionModelDao input) {
- // An existing transaction in a SUCCESS state
- return input.getTransactionStatus() == TransactionStatus.SUCCESS ||
- // Or, an existing transaction for a different payment (to do really well, we should also check on paymentExternalKey which is not available here)
- (paymentStateContext.getPaymentId() != null && input.getPaymentId().compareTo(paymentStateContext.getPaymentId()) != 0) ||
- // Or, an existing transaction for a different account.
- (!input.getAccountRecordId().equals(paymentStateContext.getInternalCallContext().getAccountRecordId()));
-
- }
- })) {
- throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
- }
- }
-
- // At this point, the payment id should have been populated for follow-up transactions (see PaymentAutomationRunner#run)
- protected void validatePaymentIdAndTransactionType(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
- for (final PaymentTransactionModelDao paymentTransactionModelDao : existingPaymentTransactions) {
- if (!paymentTransactionModelDao.getPaymentId().equals(paymentStateContext.getPaymentId())) {
- throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "does not belong to payment " + paymentStateContext.getPaymentId());
- }
- if (paymentStateContext.getTransactionType() != null && paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
- throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "has a transaction type of " + paymentTransactionModelDao.getTransactionType() +
- " instead of requested " + paymentStateContext.getTransactionType());
- }
- }
- }
}
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 433ed5b..d6a1e7d 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
@@ -71,7 +71,7 @@ public abstract class PaymentOperation extends OperationCallbackBase<PaymentTran
final String pluginName;
try {
pluginName = daoHelper.getPaymentProviderPluginName();
- this.plugin = daoHelper.getPaymentPluginApi(pluginName);
+ this.plugin = daoHelper.getPaymentPluginApi();
} catch (final PaymentApiException e) {
throw convertToUnknownTransactionStatusAndErroredPaymentState(e);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
index dd4f298..5ed95a9 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
@@ -21,17 +21,12 @@ import javax.inject.Inject;
import org.killbill.automaton.MissingEntryException;
import org.killbill.automaton.Operation;
-import org.killbill.automaton.OperationResult;
-import org.killbill.automaton.State;
import org.killbill.automaton.StateMachine;
import org.killbill.automaton.StateMachineConfig;
-import org.killbill.automaton.Transition;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.TransactionType;
-import org.killbill.billing.payment.glue.PaymentModule;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
/**
* This class needs to know about the payment state machine xml file. All the knowledge about the xml file is encapsulated here.
@@ -80,17 +75,12 @@ public class PaymentStateMachineHelper {
private static final String CREDIT_ERRORED = "CREDIT_ERRORED";
private static final String VOID_ERRORED = "VOID_ERRORED";
private static final String CHARGEBACK_ERRORED = "CHARGEBACK_ERRORED";
- private final StateMachineConfig stateMachineConfig;
- private final String[] errorStateNames = {AUTH_ERRORED, CAPTURE_ERRORED, PURCHASE_ERRORED, REFUND_ERRORED, CREDIT_ERRORED, VOID_ERRORED, CHARGEBACK_ERRORED};
- @Inject
- public PaymentStateMachineHelper(@javax.inject.Named(PaymentModule.STATE_MACHINE_PAYMENT) final StateMachineConfig stateMachineConfig) {
- this.stateMachineConfig = stateMachineConfig;
- }
+ private final StateMachineConfigCache stateMachineConfigCache;
- public State getState(final String stateName) throws MissingEntryException {
- final StateMachine stateMachine = stateMachineConfig.getStateMachineForState(stateName);
- return stateMachine.getState(stateName);
+ @Inject
+ public PaymentStateMachineHelper(final StateMachineConfigCache stateMachineConfigCache) {
+ this.stateMachineConfigCache = stateMachineConfigCache;
}
public String getInitStateNameForTransaction() {
@@ -181,17 +171,17 @@ public class PaymentStateMachineHelper {
}
}
- public StateMachine getStateMachineForStateName(final String stateName) throws MissingEntryException {
- return stateMachineConfig.getStateMachineForState(stateName);
+ public StateMachineConfig getStateMachineConfig(final String pluginName, final InternalCallContext internalCallContext) throws PaymentApiException {
+ return stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, internalCallContext);
}
- public Operation getOperationForTransaction(final TransactionType transactionType) throws MissingEntryException {
- final StateMachine stateMachine = getStateMachineForTransaction(transactionType);
+ public Operation getOperationForTransaction(final StateMachineConfig stateMachineConfig, final TransactionType transactionType) throws MissingEntryException {
+ final StateMachine stateMachine = getStateMachineForTransaction(stateMachineConfig, transactionType);
// Only one operation defined, this is the current PaymentStates.xml model
return stateMachine.getOperations()[0];
}
- public StateMachine getStateMachineForTransaction(final TransactionType transactionType) throws MissingEntryException {
+ private StateMachine getStateMachineForTransaction(final StateMachineConfig stateMachineConfig, final TransactionType transactionType) throws MissingEntryException {
switch (transactionType) {
case AUTHORIZE:
return stateMachineConfig.getStateMachine(AUTHORIZE_STATE_MACHINE_NAME);
@@ -216,22 +206,4 @@ public class PaymentStateMachineHelper {
public boolean isSuccessState(final String stateName) {
return stateName.endsWith("SUCCESS") || stateName.startsWith("CHARGEBACK");
}
-
- public final State fetchNextState(final String prevStateName, final boolean isSuccess) throws MissingEntryException {
- final StateMachine stateMachine = getStateMachineForStateName(prevStateName);
- final Transition transition = Iterables.tryFind(ImmutableList.copyOf(stateMachine.getTransitions()), new Predicate<Transition>() {
- @Override
- public boolean apply(final Transition input) {
- // This works because there is only one operation defined for a given state machine, which is our model for PaymentStates.xml
- return input.getInitialState().getName().equals(prevStateName) &&
- input.getOperationResult().equals(isSuccess ? OperationResult.SUCCESS : OperationResult.FAILURE);
- }
- }).orNull();
- return transition != null ? transition.getFinalState() : null;
- }
-
- public String[] getErroredStateNames() {
- return errorStateNames;
- }
-
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
index 3be74e7..0e67704 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
@@ -297,7 +297,7 @@ public class DefaultPaymentDao implements PaymentDao {
}
@Override
- public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID paymentId, final TransactionType transactionType,
+ public void updatePaymentAndTransactionOnCompletion(final UUID accountId, @Nullable final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
final String currentPaymentStateName, @Nullable final String lastPaymentSuccessStateName,
final UUID transactionId, final TransactionStatus transactionStatus,
final BigDecimal processedAmount, final Currency processedCurrency,
@@ -308,10 +308,16 @@ public class DefaultPaymentDao implements PaymentDao {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InternalCallContext contextWithUpdatedDate = contextWithUpdatedDate(context);
- entitySqlDaoWrapperFactory.become(TransactionSqlDao.class).updateTransactionStatus(transactionId.toString(),
- processedAmount, processedCurrency == null ? null : processedCurrency.toString(),
- transactionStatus == null ? null : transactionStatus.toString(),
- gatewayErrorCode, gatewayErrorMsg, contextWithUpdatedDate);
+ final TransactionSqlDao transactional = entitySqlDaoWrapperFactory.become(TransactionSqlDao.class);
+ final PaymentTransactionModelDao paymentTransactionModelDao = transactional.getById(transactionId.toString(), context);
+ transactional.updateTransactionStatus(transactionId.toString(),
+ attemptId == null ? (paymentTransactionModelDao.getAttemptId() == null ? null : paymentTransactionModelDao.getAttemptId().toString()) : attemptId.toString(),
+ processedAmount,
+ processedCurrency == null ? null : processedCurrency.toString(),
+ transactionStatus == null ? null : transactionStatus.toString(),
+ gatewayErrorCode,
+ gatewayErrorMsg,
+ contextWithUpdatedDate);
if (lastPaymentSuccessStateName != null) {
entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updateLastSuccessPaymentStateName(paymentId.toString(), currentPaymentStateName, lastPaymentSuccessStateName, contextWithUpdatedDate);
} else {
@@ -321,7 +327,6 @@ public class DefaultPaymentDao implements PaymentDao {
return null;
}
});
-
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
index 4c3eeee..dc1f8f7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
@@ -56,7 +56,7 @@ public interface PaymentDao {
public PaymentTransactionModelDao updatePaymentWithNewTransaction(UUID paymentId, PaymentTransactionModelDao paymentTransaction, InternalCallContext context);
- public void updatePaymentAndTransactionOnCompletion(UUID accountId, UUID paymentId, TransactionType transactionType, String currentPaymentStateName, String lastPaymentSuccessStateName, UUID transactionId,
+ public void updatePaymentAndTransactionOnCompletion(UUID accountId, UUID attemptId, UUID paymentId, TransactionType transactionType, String currentPaymentStateName, String lastPaymentSuccessStateName, UUID transactionId,
TransactionStatus paymentStatus, BigDecimal processedAmount, Currency processedCurrency,
String gatewayErrorCode, String gatewayErrorMsg, InternalCallContext context);
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
index d31d589..d85ac3a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
+ * 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
@@ -41,6 +42,7 @@ public interface TransactionSqlDao extends EntitySqlDao<PaymentTransactionModelD
@SqlUpdate
@Audited(ChangeType.UPDATE)
void updateTransactionStatus(@Bind("id") final String transactionId,
+ @Bind("attemptId") final String attemptId,
@Bind("processedAmount") final BigDecimal processedAmount,
@Bind("processedCurrency") final String processedCurrency,
@Bind("transactionStatus") final String transactionStatus,
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
index e27a8b2..c0180be 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
@@ -1,7 +1,7 @@
/*
- * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2010-2014 Ning, Inc.
+ * 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
@@ -19,11 +19,13 @@
package org.killbill.billing.payment.glue;
import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PaymentService;
import org.killbill.billing.payment.bus.PaymentBusEventHandler;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
-import org.killbill.billing.payment.invoice.PaymentTagHandler;
import org.killbill.billing.payment.core.janitor.Janitor;
+import org.killbill.billing.payment.invoice.PaymentTagHandler;
import org.killbill.billing.payment.retry.DefaultRetryService;
import org.killbill.billing.platform.api.LifecycleHandlerType;
import org.killbill.billing.platform.api.LifecycleHandlerType.LifecycleLevel;
@@ -48,6 +50,7 @@ public class DefaultPaymentService implements PaymentService {
private final DefaultRetryService retryService;
private final Janitor janitor;
private final PaymentExecutors paymentExecutors;
+ private final StateMachineConfigCache stateMachineConfigCache;
@Inject
public DefaultPaymentService(final PaymentBusEventHandler paymentBusEventHandler,
@@ -56,7 +59,8 @@ public class DefaultPaymentService implements PaymentService {
final DefaultRetryService retryService,
final PersistentBus eventBus,
final Janitor janitor,
- final PaymentExecutors paymentExecutors) {
+ final PaymentExecutors paymentExecutors,
+ final StateMachineConfigCache stateMachineConfigCache) {
this.paymentBusEventHandler = paymentBusEventHandler;
this.tagHandler = tagHandler;
this.eventBus = eventBus;
@@ -64,6 +68,7 @@ public class DefaultPaymentService implements PaymentService {
this.retryService = retryService;
this.janitor = janitor;
this.paymentExecutors = paymentExecutors;
+ this.stateMachineConfigCache = stateMachineConfigCache;
}
@Override
@@ -74,6 +79,12 @@ public class DefaultPaymentService implements PaymentService {
@LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
public void initialize() throws NotificationQueueAlreadyExists {
try {
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML);
+ } catch (final PaymentApiException e) {
+ log.error("Unable to load default payment state machine");
+ }
+
+ try {
eventBus.register(paymentBusEventHandler);
eventBus.register(tagHandler);
} catch (final PersistentBus.EventBusException e) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
index fc0e8f7..9b066d3 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -1,7 +1,7 @@
/*
- * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2010-2014 Ning, Inc.
+ * 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
@@ -33,6 +33,9 @@ import org.killbill.billing.payment.api.PaymentGatewayApi;
import org.killbill.billing.payment.api.PaymentService;
import org.killbill.billing.payment.bus.PaymentBusEventHandler;
import org.killbill.billing.payment.config.MultiTenantPaymentConfig;
+import org.killbill.billing.payment.caching.EhCacheStateMachineConfigCache;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
+import org.killbill.billing.payment.caching.StateMachineConfigCacheInvalidationCallback;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.PaymentGatewayProcessor;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
@@ -54,6 +57,7 @@ import org.killbill.billing.payment.retry.DefaultRetryService.DefaultRetryServic
import org.killbill.billing.payment.retry.RetryService;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
import org.killbill.billing.util.glue.KillBillModule;
import org.killbill.xmlloader.XMLLoader;
import org.skife.config.ConfigurationObjectFactory;
@@ -71,12 +75,13 @@ public class PaymentModule extends KillBillModule {
public static final String RETRYABLE_NAMED = "Retryable";
public static final String STATE_MACHINE_RETRY = "RetryStateMachine";
- public static final String STATE_MACHINE_PAYMENT = "PaymentStateMachine";
@VisibleForTesting
- static final String DEFAULT_STATE_MACHINE_RETRY_XML = "org/killbill/billing/payment/retry/RetryStates.xml";
+ public static final String DEFAULT_STATE_MACHINE_RETRY_XML = "org/killbill/billing/payment/retry/RetryStates.xml";
@VisibleForTesting
- static final String DEFAULT_STATE_MACHINE_PAYMENT_XML = "org/killbill/billing/payment/PaymentStates.xml";
+ public static final String DEFAULT_STATE_MACHINE_PAYMENT_XML = "org/killbill/billing/payment/PaymentStates.xml";
+
+ public static final String STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK = "StateMachineConfigInvalidationCallback";
public PaymentModule(final KillbillConfigSource configSource) {
super(configSource);
@@ -104,13 +109,14 @@ public class PaymentModule extends KillBillModule {
}
protected void installStateMachines() {
-
bind(StateMachineProvider.class).annotatedWith(Names.named(STATE_MACHINE_RETRY)).toInstance(new StateMachineProvider(DEFAULT_STATE_MACHINE_RETRY_XML));
bind(StateMachineConfig.class).annotatedWith(Names.named(STATE_MACHINE_RETRY)).toProvider(Key.get(StateMachineProvider.class, Names.named(STATE_MACHINE_RETRY)));
+
bind(PaymentControlStateMachineHelper.class).asEagerSingleton();
- bind(StateMachineProvider.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toInstance(new StateMachineProvider(DEFAULT_STATE_MACHINE_PAYMENT_XML));
- bind(StateMachineConfig.class).annotatedWith(Names.named(STATE_MACHINE_PAYMENT)).toProvider(Key.get(StateMachineProvider.class, Names.named(STATE_MACHINE_PAYMENT)));
+ bind(StateMachineConfigCache.class).to(EhCacheStateMachineConfigCache.class).asEagerSingleton();
+ bind(CacheInvalidationCallback.class).annotatedWith(Names.named(STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK)).to(StateMachineConfigCacheInvalidationCallback.class).asEagerSingleton();
+
bind(PaymentStateMachineHelper.class).asEagerSingleton();
bind(ControlPluginRunner.class).asEagerSingleton();
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg
index 3a91dc9..c00aee0 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/TransactionSqlDao.sql.stg
@@ -60,6 +60,7 @@ where transaction_external_key = :transactionExternalKey
updateTransactionStatus() ::= <<
update <tableName()>
set transaction_status = :transactionStatus
+, attempt_id = :attemptId
, processed_amount = :processedAmount
, processed_currency = :processedCurrency
, gateway_error_code = :gatewayErrorCode
diff --git a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
index da851a2..9031c87 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
+++ b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
@@ -441,30 +441,6 @@
<finalState>VOID_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>CAPTURE</finalStateMachine>
- <finalState>CAPTURE_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>REFUND</finalStateMachine>
- <finalState>REFUND_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
- <initialStateMachine>VOID</initialStateMachine>
- <initialState>VOID_SUCCESS</initialState>
- <finalStateMachine>CREDIT</finalStateMachine>
- <finalState>CREDIT_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>CAPTURE</initialStateMachine>
<initialState>CAPTURE_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -483,12 +459,6 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>CAPTURE</initialStateMachine>
- <initialState>CAPTURE_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>REFUND</initialStateMachine>
<initialState>REFUND_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -501,12 +471,6 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>REFUND</initialStateMachine>
- <initialState>REFUND_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>PURCHASE</initialStateMachine>
<initialState>PURCHASE_SUCCESS</initialState>
<finalStateMachine>REFUND</finalStateMachine>
@@ -519,12 +483,6 @@
<finalState>CHARGEBACK_INIT</finalState>
</linkStateMachine>
<linkStateMachine>
- <initialStateMachine>CREDIT</initialStateMachine>
- <initialState>CREDIT_SUCCESS</initialState>
- <finalStateMachine>VOID</finalStateMachine>
- <finalState>VOID_INIT</finalState>
- </linkStateMachine>
- <linkStateMachine>
<initialStateMachine>CHARGEBACK</initialStateMachine>
<initialState>CHARGEBACK_SUCCESS</initialState>
<finalStateMachine>CHARGEBACK</finalStateMachine>
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
new file mode 100644
index 0000000..03f6d8a
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin;
+import org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultAdminPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
+
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
+ private Account account;
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ super.beforeClass();
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ mockPaymentProviderPlugin.clear();
+ account = testHelper.createTestAccount("bobo@gmail.com", true);
+
+ final PaymentControlPluginApi mockPaymentControlProviderPlugin = new MockPaymentControlProviderPlugin();
+ controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
+ @Override
+ public String getPluginSymbolicName() {
+ return null;
+ }
+
+ @Override
+ public String getPluginName() {
+ return MockPaymentControlProviderPlugin.PLUGIN_NAME;
+ }
+
+ @Override
+ public String getRegistrationName() {
+ return MockPaymentControlProviderPlugin.PLUGIN_NAME;
+ }
+ },
+ mockPaymentControlProviderPlugin);
+ }
+
+ @Test(groups = "slow")
+ public void testFixPaymentTransactionState() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorMsg(), "");
+
+ adminPaymentApi.fixPaymentTransactionState(payment, payment.getTransactions().get(0), TransactionStatus.PAYMENT_FAILURE, null, "AUTH_ERRORED", ImmutableList.<PluginProperty>of(), callContext);
+
+ final PaymentModelDao refreshedPaymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao refreshedPaymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(refreshedPaymentModelDao.getStateName(), "AUTH_ERRORED");
+ // TODO Shouldn't we allow the user to override this too?
+ Assert.assertEquals(refreshedPaymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorMsg(), "");
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/551")
+ public void testFixPaymentTransactionStateNoPaymentTransactionInfoPlugin() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorMsg(), "");
+
+ try {
+ // Since no transaction status is passed, PaymentTransactionInfoPlugin should be set
+ adminPaymentApi.fixPaymentTransactionState(payment, Mockito.mock(DefaultPaymentTransaction.class), null, null, "AUTH_ERRORED", ImmutableList.<PluginProperty>of(), callContext);
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ }
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/551")
+ public void testFixPaymentTransactionStateFromPaymentTransactionInfoPlugin() throws PaymentApiException {
+ final Payment payment = paymentApi.createAuthorization(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(paymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorCode(), "");
+ Assert.assertEquals(paymentTransactionModelDao.getGatewayErrorMsg(), "");
+
+ final PaymentTransactionInfoPlugin infoPlugin = new DefaultNoOpPaymentInfoPlugin(paymentTransactionModelDao.getPaymentId(),
+ paymentTransactionModelDao.getId(),
+ paymentTransactionModelDao.getTransactionType(),
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ paymentTransactionModelDao.getEffectiveDate(),
+ paymentTransactionModelDao.getCreatedDate(),
+ PaymentPluginStatus.ERROR,
+ "error-code",
+ "error-msg");
+ final PaymentTransaction newPaymentTransaction = new DefaultPaymentTransaction(paymentTransactionModelDao.getId(),
+ paymentTransactionModelDao.getAttemptId(),
+ paymentTransactionModelDao.getTransactionExternalKey(),
+ paymentTransactionModelDao.getCreatedDate(),
+ paymentTransactionModelDao.getUpdatedDate(),
+ paymentTransactionModelDao.getPaymentId(),
+ paymentTransactionModelDao.getTransactionType(),
+ paymentTransactionModelDao.getEffectiveDate(),
+ TransactionStatus.PAYMENT_FAILURE,
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ paymentTransactionModelDao.getProcessedAmount(),
+ paymentTransactionModelDao.getProcessedCurrency(),
+ infoPlugin.getGatewayErrorCode(),
+ infoPlugin.getGatewayError(),
+ infoPlugin);
+ adminPaymentApi.fixPaymentTransactionState(payment, newPaymentTransaction, null, null, "AUTH_ERRORED", ImmutableList.<PluginProperty>of(), callContext);
+
+ final PaymentModelDao refreshedPaymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao refreshedPaymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(refreshedPaymentModelDao.getStateName(), "AUTH_ERRORED");
+ // TODO Shouldn't we allow the user to override this too?
+ Assert.assertEquals(refreshedPaymentModelDao.getLastSuccessStateName(), "AUTH_SUCCESS");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getProcessedCurrency(), Currency.EUR);
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorCode(), "error-code");
+ Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorMsg(), "error-msg");
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index 78437bc..8db97ce 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -412,10 +412,21 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.VOID);
assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+
+ try {
+ // Verify further VOIDs are prohibited (see https://github.com/killbill/killbill/issues/514)
+ paymentApi.createVoid(account, payment.getId(), UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
}
@Test(groups = "slow")
public void testCreateSuccessAuthCaptureVoidCapture() throws PaymentApiException {
+ // Overwrite the default state machine to allow void on captures
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig("org/killbill/billing/payment/PermissivePaymentStates.xml");
+
final BigDecimal authAmount = BigDecimal.TEN;
final BigDecimal captureAmount = BigDecimal.ONE;
@@ -533,7 +544,82 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testCreateSuccessAuthCaptureVoidFailed() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final BigDecimal captureAmount = BigDecimal.ONE;
+
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey = UUID.randomUUID().toString();
+ final String transactionExternalKey2 = UUID.randomUUID().toString();
+ final String transactionExternalKey3 = UUID.randomUUID().toString();
+
+ final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED,
+ paymentExternalKey, transactionExternalKey,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment.getExternalKey(), paymentExternalKey);
+ assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment.getAccountId(), account.getId());
+ assertEquals(payment.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment.getCurrency(), Currency.AED);
+ assertFalse(payment.isAuthVoided());
+
+ assertEquals(payment.getTransactions().size(), 1);
+ assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
+ assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
+ assertEquals(payment.getTransactions().get(0).getAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(authAmount), 0);
+ assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.AUTHORIZE);
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorMsg());
+ assertNotNull(payment.getTransactions().get(0).getGatewayErrorCode());
+
+ final Payment payment2 = paymentApi.createCapture(account, payment.getId(), captureAmount, Currency.AED, transactionExternalKey2,
+ ImmutableList.<PluginProperty>of(), callContext);
+
+ assertEquals(payment2.getExternalKey(), paymentExternalKey);
+ assertEquals(payment2.getPaymentMethodId(), account.getPaymentMethodId());
+ assertEquals(payment2.getAccountId(), account.getId());
+ assertEquals(payment2.getAuthAmount().compareTo(authAmount), 0);
+ assertEquals(payment2.getCapturedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payment2.getCurrency(), Currency.AED);
+ assertFalse(payment2.isAuthVoided());
+
+ assertEquals(payment2.getTransactions().size(), 2);
+ assertEquals(payment2.getTransactions().get(1).getExternalKey(), transactionExternalKey2);
+ assertEquals(payment2.getTransactions().get(1).getPaymentId(), payment.getId());
+ assertEquals(payment2.getTransactions().get(1).getAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getTransactions().get(1).getCurrency(), Currency.AED);
+ assertEquals(payment2.getTransactions().get(1).getProcessedAmount().compareTo(captureAmount), 0);
+ assertEquals(payment2.getTransactions().get(1).getProcessedCurrency(), Currency.AED);
+
+ assertEquals(payment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payment2.getTransactions().get(1).getTransactionType(), TransactionType.CAPTURE);
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorMsg());
+ assertNotNull(payment2.getTransactions().get(1).getGatewayErrorCode());
+
+ try {
+ // Voiding a capture is prohibited by default
+ paymentApi.createVoid(account, payment.getId(), transactionExternalKey3, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testCreateSuccessAuthCaptureVoidVoid() throws PaymentApiException {
+ // Overwrite the default state machine to allow void on captures
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig("org/killbill/billing/payment/PermissivePaymentStates.xml");
+
final BigDecimal authAmount = BigDecimal.TEN;
final BigDecimal captureAmount = BigDecimal.ONE;
@@ -1341,6 +1427,42 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testVerifyJanitorFromPendingDuringCompletionFlow() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final String transactionExternalKey = UUID.randomUUID().toString();
+
+ final Payment initialPayment = createPayment(TransactionType.AUTHORIZE, null, UUID.randomUUID().toString(), transactionExternalKey, authAmount, PaymentPluginStatus.PENDING);
+ Assert.assertEquals(initialPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(initialPayment.getId(), initialPayment.getTransactions().get(0).getId(), PaymentPluginStatus.PROCESSED);
+
+ try {
+ final Payment completedPayment = createPayment(TransactionType.AUTHORIZE, initialPayment.getId(), initialPayment.getExternalKey(), transactionExternalKey, authAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testVerifyJanitorFromUnknownDuringCompletionFlow() throws PaymentApiException {
+ final BigDecimal authAmount = BigDecimal.TEN;
+ final String transactionExternalKey = UUID.randomUUID().toString();
+
+ final Payment initialPayment = createPayment(TransactionType.AUTHORIZE, null, UUID.randomUUID().toString(), transactionExternalKey, authAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(initialPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(initialPayment.getId(), initialPayment.getTransactions().get(0).getId(), PaymentPluginStatus.PROCESSED);
+
+ try {
+ final Payment completedPayment = createPayment(TransactionType.AUTHORIZE, initialPayment.getId(), initialPayment.getExternalKey(), transactionExternalKey, authAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testNotifyPendingTransactionOfStateChanged() throws PaymentApiException {
final BigDecimal authAmount = BigDecimal.TEN;
@@ -1348,16 +1470,9 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
final String paymentExternalKey = "rouge";
final String transactionExternalKey = "vert";
- final Payment initialPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, authAmount, Currency.AED, paymentExternalKey, transactionExternalKey,
- ImmutableList.<PluginProperty>of(), callContext);
+ final Payment initialPayment = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, transactionExternalKey, authAmount, PaymentPluginStatus.PENDING);
- // Update the payment/transaction by hand to simulate a PENDING state.
- final PaymentTransaction paymentTransaction = initialPayment.getTransactions().get(0);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), initialPayment.getId(), TransactionType.AUTHORIZE, "AUTH_PENDING", "AUTH_PENDING",
- paymentTransaction.getId(), TransactionStatus.PENDING, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
- null, null, internalCallContext);
-
- final Payment payment = paymentApi.notifyPendingTransactionOfStateChanged(account, paymentTransaction.getId(), true, callContext);
+ final Payment payment = paymentApi.notifyPendingTransactionOfStateChanged(account, initialPayment.getTransactions().get(0).getId(), true, callContext);
assertEquals(payment.getExternalKey(), paymentExternalKey);
assertEquals(payment.getPaymentMethodId(), account.getPaymentMethodId());
@@ -1366,15 +1481,15 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
- assertEquals(payment.getCurrency(), Currency.AED);
+ assertEquals(payment.getCurrency(), Currency.USD);
assertEquals(payment.getTransactions().size(), 1);
assertEquals(payment.getTransactions().get(0).getExternalKey(), transactionExternalKey);
assertEquals(payment.getTransactions().get(0).getPaymentId(), payment.getId());
assertEquals(payment.getTransactions().get(0).getAmount().compareTo(authAmount), 0);
- assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getCurrency(), Currency.USD);
assertEquals(payment.getTransactions().get(0).getProcessedAmount().compareTo(authAmount), 0);
- assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.AED);
+ assertEquals(payment.getTransactions().get(0).getProcessedCurrency(), Currency.USD);
assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
assertEquals(payment.getTransactions().get(0).getTransactionType(), TransactionType.AUTHORIZE);
@@ -1429,7 +1544,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
ImmutableList.<PluginProperty>of(), callContext);
// Hack the Database to make it look like it was a failure
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, "AUTH_ERRORED", null,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, "AUTH_ERRORED", null,
payment.getTransactions().get(0).getId(), TransactionStatus.PLUGIN_FAILURE, null, null, null, null, internalCallContext);
final PaymentSqlDao paymentSqlDao = dbi.onDemand(PaymentSqlDao.class);
paymentSqlDao.updateLastSuccessPaymentStateName(payment.getId().toString(), "AUTH_ERRORED", null, internalCallContext);
@@ -1521,6 +1636,51 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testCompletionOfUnknownAuthorization() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment pendingPayment = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ assertNotNull(pendingPayment);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 1);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ try {
+ // Attempt to complete the payment
+ createPayment(TransactionType.AUTHORIZE, pendingPayment.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCompletionOfUnknownCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ try {
+ // Attempt to complete the payment
+ createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testCreatePurchaseWithTimeout() throws Exception {
final BigDecimal requestedAmount = BigDecimal.TEN;
final String paymentExternalKey = "ohhhh";
@@ -1575,7 +1735,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
createPayment(TransactionType.PURCHASE, null, paymentExternalKey, transactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
Assert.fail("PURCHASE transaction with same key should have failed");
} catch (final PaymentApiException expected) {
- Assert.assertEquals(expected.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ Assert.assertEquals(expected.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
}
}
@@ -1817,6 +1977,14 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
paymentTransactionExternalKey,
pluginProperties,
callContext);
+ case CAPTURE:
+ return paymentApi.createCapture(account,
+ paymentId,
+ amount,
+ amount == null ? null : account.getCurrency(),
+ paymentTransactionExternalKey,
+ pluginProperties,
+ callContext);
default:
Assert.fail();
return null;
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
index 83a2b65..cad7b12 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
@@ -88,7 +88,7 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testSimpleInvoicePaymentWithInvoiceAmount() throws Exception {
- final BigDecimal invoiceAmount = new BigDecimal("10.0011");
+ final BigDecimal invoiceAmount = BigDecimal.TEN;
final BigDecimal requestedAmount = invoiceAmount;
final BigDecimal expectedAmount = invoiceAmount;
@@ -97,8 +97,8 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testSimpleInvoicePaymentWithLowerAmount() throws Exception {
- final BigDecimal invoiceAmount = new BigDecimal("10.0011");
- final BigDecimal requestedAmount = new BigDecimal("8.0091");
+ final BigDecimal invoiceAmount = BigDecimal.TEN;
+ final BigDecimal requestedAmount = BigDecimal.ONE;
final BigDecimal expectedAmount = requestedAmount;
testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
@@ -106,8 +106,8 @@ public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testSimpleInvoicePaymentWithInvalidAmount() throws Exception {
- final BigDecimal invoiceAmount = new BigDecimal("10.0011");
- final BigDecimal requestedAmount = new BigDecimal("80.0091");
+ final BigDecimal invoiceAmount = BigDecimal.ONE;
+ final BigDecimal requestedAmount = BigDecimal.TEN;
final BigDecimal expectedAmount = null;
testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
index acb1be0..15d8dac 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
@@ -21,6 +21,7 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.control.plugin.api.OnFailurePaymentControlResult;
@@ -32,11 +33,15 @@ 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.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.payment.retry.DefaultFailureCallResult;
import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
+import org.killbill.commons.request.Request;
+import org.killbill.commons.request.RequestData;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -86,6 +91,14 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
},
testPaymentControlPluginApi);
+
+ // Required for re-entrant locks to work
+ Request.setPerThreadRequestData(new RequestData(UUID.randomUUID().toString()));
+ }
+
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
+ Request.resetPerThreadRequestData();
}
// Verify Payment control API can be used to change the paymentMethodId on the fly and this is reflected in the created Payment.
@@ -101,6 +114,378 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testCreateAuthPendingWithControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthUnknownWithControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ try {
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCapturePendingWithControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCaptureUnknownWithControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ try {
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthPendingWithControlCompleteNoControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthUnknownWithControlCompleteNoControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ try {
+ payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCapturePendingWithControlCompleteNoControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCaptureUnknownWithControlCompleteNoControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ pendingPluginProperties, PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ try {
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthPendingNoControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthUnknownNoControlCompleteWithControl() throws PaymentApiException {
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ try {
+ payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
+ payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCapturePendingNoControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING, false));
+
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateAuthSuccessCaptureUnknownNoControlCompleteWithControl() throws PaymentApiException {
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, Currency.USD, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 1);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+ final Iterable<PluginProperty> pendingPluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED, false));
+
+ payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, pendingPluginProperties, callContext);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ try {
+ payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
+ ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+ Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getTransactions().size(), 2);
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+ Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+ Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ }
+
+ @Test(groups = "slow")
public void testCreateAuthWithControlCaptureNoControl() throws PaymentApiException {
final BigDecimal requestedAmount = BigDecimal.TEN;
@@ -174,7 +559,7 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
@Override
public Iterable<PluginProperty> getAdjustedPluginProperties() {
- return ImmutableList.of();
+ return null;
}
};
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
new file mode 100644
index 0000000..9960e7a
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.xmlloader.UriAccessor;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Resources;
+import net.sf.ehcache.CacheException;
+
+public class TestStateMachineConfigCache extends PaymentTestSuiteNoDB {
+
+ private InternalTenantContext multiTenantContext;
+ private InternalTenantContext otherMultiTenantContext;
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ cacheControllerDispatcher.clearAll();
+
+ multiTenantContext = Mockito.mock(InternalTenantContext.class);
+ Mockito.when(multiTenantContext.getAccountRecordId()).thenReturn(456L);
+ Mockito.when(multiTenantContext.getTenantRecordId()).thenReturn(99L);
+
+ otherMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(otherMultiTenantContext.getAccountRecordId()).thenReturn(123L);
+ Mockito.when(otherMultiTenantContext.getTenantRecordId()).thenReturn(112233L);
+ }
+
+ @Test(groups = "fast")
+ public void testMissingPluginStateMachineConfig() throws PaymentApiException {
+ Assert.assertNotNull(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), internalCallContext));
+ Assert.assertNotNull(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), multiTenantContext));
+ Assert.assertNotNull(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), otherMultiTenantContext));
+ }
+
+ @Test(groups = "fast")
+ public void testExistingTenantStateMachineConfig() throws PaymentApiException, URISyntaxException, IOException {
+ final String pluginName = UUID.randomUUID().toString();
+
+ final InternalCallContext differentMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(differentMultiTenantContext.getTenantRecordId()).thenReturn(55667788L);
+
+ final AtomicBoolean shouldThrow = new AtomicBoolean(false);
+ final Long multiTenantRecordId = multiTenantContext.getTenantRecordId();
+ final Long otherMultiTenantRecordId = otherMultiTenantContext.getTenantRecordId();
+
+ Mockito.when(tenantInternalApi.getPluginPaymentStateMachineConfig(Mockito.eq(pluginName), Mockito.any(InternalTenantContext.class))).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(final InvocationOnMock invocation) throws Throwable {
+ if (shouldThrow.get()) {
+ throw new RuntimeException();
+ }
+ final InternalTenantContext internalContext = (InternalTenantContext) invocation.getArguments()[1];
+ if (multiTenantRecordId.equals(internalContext.getTenantRecordId())) {
+ return new String(ByteStreams.toByteArray(UriAccessor.accessUri(Resources.getResource(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML).toExternalForm())));
+ } else if (otherMultiTenantRecordId.equals(internalContext.getTenantRecordId())) {
+ return new String(ByteStreams.toByteArray(UriAccessor.accessUri(Resources.getResource(PaymentModule.DEFAULT_STATE_MACHINE_RETRY_XML).toExternalForm())));
+ } else {
+ return null;
+ }
+ }
+ });
+
+ // Verify the lookup for a non-cached tenant. No system config is set yet but EhCacheStateMachineConfigCache returns a default empty one
+ final StateMachineConfig defaultStateMachineConfig = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, differentMultiTenantContext);
+ Assert.assertNotNull(defaultStateMachineConfig);
+
+ // Make sure the cache loader isn't invoked, see https://github.com/killbill/killbill/issues/300
+ shouldThrow.set(true);
+
+ final StateMachineConfig defaultStateMachineConfig2 = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, differentMultiTenantContext);
+ Assert.assertNotNull(defaultStateMachineConfig2);
+ Assert.assertEquals(defaultStateMachineConfig2, defaultStateMachineConfig);
+
+ shouldThrow.set(false);
+
+ // Verify the lookup for this tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), multiTenantContext), defaultStateMachineConfig);
+ final StateMachineConfig result = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.assertNotNull(result);
+ Assert.assertNotEquals(result, defaultStateMachineConfig);
+ Assert.assertEquals(result.getStateMachines().length, 8);
+
+ // Verify the lookup for another tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), otherMultiTenantContext), defaultStateMachineConfig);
+ final StateMachineConfig otherResult = stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext);
+ Assert.assertNotNull(otherResult);
+ Assert.assertEquals(otherResult.getStateMachines().length, 1);
+
+ shouldThrow.set(true);
+
+ // Verify the lookup for this tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext), result);
+
+ // Verify the lookup with another context for the same tenant
+ final InternalCallContext sameMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(sameMultiTenantContext.getAccountRecordId()).thenReturn(9102L);
+ Mockito.when(sameMultiTenantContext.getTenantRecordId()).thenReturn(multiTenantRecordId);
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, sameMultiTenantContext), result);
+
+ // Verify the lookup with the other tenant
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), otherResult);
+
+ // Verify clearing the cache works
+ stateMachineConfigCache.clearPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), otherResult);
+ try {
+ stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.fail();
+ } catch (final CacheException exception) {
+ Assert.assertTrue(exception.getCause() instanceof RuntimeException);
+ }
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
new file mode 100644
index 0000000..9ead7cd
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.payment.caching;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.sf.ehcache.CacheException;
+
+public class TestStateMachineConfigCacheInvalidationCallback extends PaymentTestSuiteNoDB {
+
+ private InternalTenantContext multiTenantContext;
+ private InternalTenantContext otherMultiTenantContext;
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ cacheControllerDispatcher.clearAll();
+
+ multiTenantContext = Mockito.mock(InternalTenantContext.class);
+ Mockito.when(multiTenantContext.getAccountRecordId()).thenReturn(456L);
+ Mockito.when(multiTenantContext.getTenantRecordId()).thenReturn(99L);
+
+ otherMultiTenantContext = Mockito.mock(InternalCallContext.class);
+ Mockito.when(otherMultiTenantContext.getAccountRecordId()).thenReturn(123L);
+ Mockito.when(otherMultiTenantContext.getTenantRecordId()).thenReturn(112233L);
+ }
+
+ @Test(groups = "fast")
+ public void testInvalidation() throws Exception {
+ final String pluginName = UUID.randomUUID().toString();
+
+ final StateMachineConfig defaultPaymentStateMachineConfig = stateMachineConfigCache.getPaymentStateMachineConfig(UUID.randomUUID().toString(), internalCallContext);
+ Assert.assertNotNull(defaultPaymentStateMachineConfig);
+
+ final AtomicBoolean shouldThrow = new AtomicBoolean(false);
+
+ Mockito.when(tenantInternalApi.getPluginPaymentStateMachineConfig(Mockito.eq(pluginName), Mockito.any(InternalTenantContext.class))).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(final InvocationOnMock invocation) throws Throwable {
+ if (shouldThrow.get()) {
+ throw new RuntimeException();
+ }
+ return null;
+ }
+ });
+
+ // Prime caches
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, internalCallContext), defaultPaymentStateMachineConfig);
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), defaultPaymentStateMachineConfig);
+
+ shouldThrow.set(true);
+
+ // No exception (cached)
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, internalCallContext), defaultPaymentStateMachineConfig);
+
+ cacheInvalidationCallback.invalidateCache(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_, pluginName, multiTenantContext);
+
+ try {
+ stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
+ Assert.fail();
+ } catch (final CacheException exception) {
+ Assert.assertTrue(exception.getCause() instanceof RuntimeException);
+ }
+
+ // No exception (cached)
+ Assert.assertEquals(stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, otherMultiTenantContext), defaultPaymentStateMachineConfig);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java
index f6dcd0a..65586e8 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentAutomatonDAOHelper.java
@@ -100,7 +100,7 @@ public class TestPaymentAutomatonDAOHelper extends PaymentTestSuiteWithEmbeddedD
public void testNoPaymentMethod() throws Exception {
final PaymentAutomatonDAOHelper daoHelper = createDAOHelper(UUID.randomUUID(), paymentExternalKey, paymentTransactionExternalKey, amount, currency);
try {
- daoHelper.getPaymentProviderPlugin();
+ daoHelper.getPaymentPluginApi();
Assert.fail();
} catch (final PaymentApiException e) {
Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD.getCode());
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
index 1ef93cc..38a1e9b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
@@ -88,59 +88,6 @@ public class TestPaymentLeavingStateCallback extends PaymentTestSuiteWithEmbedde
Assert.assertEquals(paymentDao.getTransactionsForPayment(paymentId, internalCallContext).size(), 2);
}
- @Test(groups = "slow", expectedExceptions = OperationException.class)
- public void testLeaveStateForConflictingPaymentTransactionExternalKey() throws Exception {
- final UUID paymentId = UUID.randomUUID();
- setUp(paymentId);
-
- // Verify the payment has only one transaction
- final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
- Assert.assertEquals(transactions.size(), 1);
-
- final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.CAPTURE).toString();
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), paymentId, TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
- transactions.get(0).getId(), TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.BRL,
- "foo", "bar", internalCallContext);
-
- // Will validate the validateUniqueTransactionExternalKey logic for when we reuse the same payment transactionExternalKey
- callback.leavingState(state);
-
- }
-
- @Test(groups = "slow", expectedExceptions = OperationException.class)
- public void testLeaveStateForConflictingPaymentTransactionExternalKeyAcrossAccounts() throws Exception {
- final UUID paymentId = UUID.randomUUID();
- setUp(paymentId);
-
- // Verify the payment has only one transaction
- final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
- Assert.assertEquals(transactions.size(), 1);
-
- final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.CAPTURE).toString();
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), paymentId, TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
- transactions.get(0).getId(), TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.BRL,
- "foo", "bar", internalCallContext);
-
- paymentStateContext = new PaymentStateContext(true,
- paymentId,
- null,
- null,
- paymentStateContext.getPaymentExternalKey(),
- paymentStateContext.getPaymentTransactionExternalKey(),
- paymentStateContext.getTransactionType(),
- paymentStateContext.getAccount(),
- paymentStateContext.getPaymentMethodId(),
- paymentStateContext.getAmount(),
- paymentStateContext.getCurrency(),
- paymentStateContext.shouldLockAccountAndDispatch(),
- paymentStateContext.getOverridePluginOperationResult(),
- paymentStateContext.getProperties(),
- internalCallContext,
- callContext);
-
- callback.leavingState(state);
- }
-
private void verifyPaymentTransaction() {
Assert.assertNotNull(paymentStateContext.getPaymentTransactionModelDao().getPaymentId());
Assert.assertEquals(paymentStateContext.getPaymentTransactionModelDao().getTransactionExternalKey(), paymentStateContext.getPaymentTransactionExternalKey());
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
index 2e642c9..e34c632 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
@@ -209,7 +209,7 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
callContext);
final PaymentAutomatonDAOHelper daoHelper = Mockito.mock(PaymentAutomatonDAOHelper.class);
- Mockito.when(daoHelper.getPaymentProviderPlugin()).thenReturn(null);
+ Mockito.when(daoHelper.getPaymentPluginApi()).thenReturn(null);
return new PluginOperationTest(daoHelper, locker, paymentPluginDispatcher, paymentConfig, paymentStateContext);
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
index 5f87219..4f3b429 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
@@ -80,9 +80,6 @@ import static org.testng.Assert.fail;
public class TestRetryablePayment extends PaymentTestSuiteNoDB {
@Inject
- @Named(PaymentModule.STATE_MACHINE_PAYMENT)
- private StateMachineConfig stateMachineConfig;
- @Inject
@Named(PaymentModule.STATE_MACHINE_RETRY)
private StateMachineConfig retryStateMachineConfig;
@Inject
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
index 3f314c8..0798535 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -221,7 +221,7 @@ public class MockPaymentDao implements PaymentDao {
}
@Override
- public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID paymentId, final TransactionType transactionType,
+ public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
final String currentPaymentStateName, final String lastSuccessPaymentStateName, final UUID transactionId,
final TransactionStatus paymentStatus, final BigDecimal processedAmount, final Currency processedCurrency,
final String gatewayErrorCode, final String gatewayErrorMsg, final InternalCallContext context) {
@@ -232,6 +232,7 @@ public class MockPaymentDao implements PaymentDao {
}
final PaymentTransactionModelDao transaction = transactions.get(transactionId);
if (transaction != null) {
+ transaction.setAttemptId(attemptId);
transaction.setTransactionStatus(paymentStatus);
transaction.setProcessedAmount(processedAmount);
transaction.setProcessedCurrency(processedCurrency);
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
index 5b56ec4..4d6a938 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
@@ -65,6 +65,7 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final String gatewayErrorCode = UUID.randomUUID().toString().substring(0, 5);
final String gatewayErrorMsg = UUID.randomUUID().toString();
paymentDao.updatePaymentAndTransactionOnCompletion(accountId,
+ specifiedSecondPaymentTransactionModelDao.getAttemptId(),
specifiedSecondPaymentTransactionModelDao.getPaymentId(),
specifiedFirstPaymentTransactionModelDao.getTransactionType(),
"SOME_ERRORED_STATE",
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
index 91a98e1..a195593 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -165,7 +165,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(savedPayment.getId(), internalCallContext);
assertEquals(transactions.size(), 2);
- paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
+ paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedTransactionModelDao2.getAttemptId(), savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
BigDecimal.ONE, Currency.USD, null, "nothing", internalCallContext);
final PaymentModelDao savedPayment4 = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
@@ -188,7 +188,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
assertNull(savedTransactionModelDao4.getGatewayErrorCode());
assertEquals(savedTransactionModelDao4.getGatewayErrorMsg(), "nothing");
- paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", null, transactionModelDao2.getId(), TransactionStatus.SUCCESS,
+ paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedTransactionModelDao2.getAttemptId(), savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", null, transactionModelDao2.getId(), TransactionStatus.SUCCESS,
BigDecimal.ONE, Currency.USD, null, "nothing", internalCallContext);
final PaymentModelDao savedPayment4Again = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
@@ -196,7 +196,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
assertEquals(savedPayment4Again.getStateName(), "AUTH_ABORTED");
assertEquals(savedPayment4Again.getLastSuccessStateName(), "AUTH_SUCCESS");
- paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
+ paymentDao.updatePaymentAndTransactionOnCompletion(accountId, savedTransactionModelDao2.getAttemptId(), savedPayment.getId(), savedTransactionModelDao2.getTransactionType(), "AUTH_ABORTED", "AUTH_SUCCESS", transactionModelDao2.getId(), TransactionStatus.SUCCESS,
BigDecimal.ONE, Currency.USD, null, "nothing", internalCallContext);
final PaymentModelDao savedPayment4Final = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
@@ -300,7 +300,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final Iterable<PaymentTransactionModelDao> transactions1 = paymentDao.getByTransactionStatusAcrossTenants(ImmutableList.of(TransactionStatus.PENDING), newTime, initialTime, 0L, 3L);
for (PaymentTransactionModelDao paymentTransaction : transactions1) {
final String newPaymentState = "XXX_FAILED";
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), paymentTransaction.getAttemptId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
paymentTransaction.getId(), TransactionStatus.PAYMENT_FAILURE, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
paymentTransaction.getGatewayErrorCode(), paymentTransaction.getGatewayErrorMsg(), internalCallContext);
}
@@ -317,7 +317,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
final Iterable<PaymentTransactionModelDao> transactions2 = paymentDao.getByTransactionStatusAcrossTenants(ImmutableList.of(TransactionStatus.PENDING), clock.getUTCNow(), initialTime, 0L, 1L);
for (PaymentTransactionModelDao paymentTransaction : transactions2) {
final String newPaymentState = "XXX_FAILED";
- paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
+ paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(), paymentTransaction.getAttemptId(), payment.getId(), paymentTransaction.getTransactionType(), newPaymentState, payment.getLastSuccessStateName(),
paymentTransaction.getId(), TransactionStatus.PAYMENT_FAILURE, paymentTransaction.getProcessedAmount(), paymentTransaction.getProcessedCurrency(),
paymentTransaction.getGatewayErrorCode(), paymentTransaction.getGatewayErrorMsg(), internalCallContext);
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
index 1b8eecb..b50ce2c 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
@@ -18,12 +18,15 @@
package org.killbill.billing.payment;
+import javax.inject.Named;
+
import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentGatewayApi;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
import org.killbill.billing.payment.core.PaymentProcessor;
@@ -32,11 +35,14 @@ import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
import org.killbill.billing.payment.dao.MockPaymentDao;
import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.payment.glue.TestPaymentModuleNoDB;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.payment.retry.DefaultRetryService;
import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.bus.api.PersistentBus;
@@ -50,6 +56,8 @@ import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import static org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin.PLUGIN_NAME;
+
public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
@Inject
@@ -86,6 +94,13 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
protected CacheControllerDispatcher cacheControllerDispatcher;
@Inject
protected PaymentExecutors paymentExecutors;
+ @Inject
+ protected StateMachineConfigCache stateMachineConfigCache;
+ @Inject
+ @Named(PaymentModule.STATE_MACHINE_CONFIG_INVALIDATION_CALLBACK)
+ protected CacheInvalidationCallback cacheInvalidationCallback;
+ @Inject
+ protected TenantInternalApi tenantInternalApi;
@Override
protected KillbillConfigSource getConfigSource() {
@@ -103,6 +118,9 @@ public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
@BeforeMethod(groups = "fast")
public void beforeMethod() throws Exception {
+ stateMachineConfigCache.clearPaymentStateMachineConfig(PLUGIN_NAME, internalCallContext);
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML);
+
eventBus.start();
paymentExecutors.initialize();
((MockPaymentDao) paymentDao).reset();
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
index 655b60a..73cc4ea 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -23,13 +23,16 @@ import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.control.plugin.api.PaymentControlPluginApi;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.AdminPaymentApi;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentGatewayApi;
+import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
-import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.payment.glue.TestPaymentModuleWithEmbeddedDB;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
@@ -47,6 +50,8 @@ import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import static org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin.PLUGIN_NAME;
+
public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
@Inject
@@ -66,6 +71,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
@Inject
protected PaymentApi paymentApi;
@Inject
+ protected AdminPaymentApi adminPaymentApi;
+ @Inject
protected PaymentGatewayApi paymentGatewayApi;
@Inject
protected AccountInternalApi accountApi;
@@ -79,6 +86,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
protected PaymentExecutors paymentExecutors;
@Inject
protected NonEntityDao nonEntityDao;
+ @Inject
+ protected StateMachineConfigCache stateMachineConfigCache;
@Override
protected KillbillConfigSource getConfigSource() {
@@ -96,11 +105,14 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
@BeforeMethod(groups = "slow")
public void beforeMethod() throws Exception {
super.beforeMethod();
+
+ stateMachineConfigCache.clearPaymentStateMachineConfig(PLUGIN_NAME, internalCallContext);
+ stateMachineConfigCache.loadDefaultPaymentStateMachineConfig(PaymentModule.DEFAULT_STATE_MACHINE_PAYMENT_XML);
+
paymentExecutors.initialize();
eventBus.start();
Profiling.resetPerThreadProfilingData();
clock.resetDeltaFromReality();
-
}
@AfterMethod(groups = "slow")
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index 8adabea..1f81aeb 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -50,6 +50,7 @@ import org.killbill.billing.util.entity.DefaultPagination;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.clock.Clock;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -388,6 +389,21 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
return getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.REFUND, refundAmount, currency, properties);
}
+ public void overridePaymentTransactionPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final PaymentPluginStatus paymentPluginStatus) throws PaymentPluginApiException {
+ final List<PaymentTransactionInfoPlugin> existingTransactions = paymentTransactions.get(kbPaymentId.toString());
+ PaymentTransactionInfoPlugin paymentTransactionInfoPlugin = null;
+ for (final PaymentTransactionInfoPlugin existingTransaction : existingTransactions) {
+ if (existingTransaction.getKbTransactionPaymentId().equals(kbTransactionId)) {
+ paymentTransactionInfoPlugin = existingTransaction;
+ break;
+ }
+ }
+ Preconditions.checkNotNull(paymentTransactionInfoPlugin);
+
+ final Iterable<PluginProperty> pluginProperties = ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, paymentPluginStatus.toString(), false));
+ getPaymentTransactionInfoPluginResult(kbPaymentId, kbTransactionId, TransactionType.AUTHORIZE, paymentTransactionInfoPlugin.getAmount(), paymentTransactionInfoPlugin.getCurrency(), pluginProperties);
+ }
+
private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginResult(final UUID kbPaymentId, final UUID kbTransactionId, final TransactionType type, @Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> pluginProperties) throws PaymentPluginApiException {
if (makePluginWaitSomeMilliseconds.get() > 0) {
try {
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index aad5f13..8051f80 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -290,7 +290,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to UNKNOWN
final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT_PLUGIN_ERROR);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
"foo", "bar", internalCallContext);
testListener.assertListenerStatus();
@@ -322,7 +322,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to UNKNOWN
final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT_PLUGIN_ERROR);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
"foo", "bar", internalCallContext);
testListener.assertListenerStatus();
@@ -367,7 +367,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to UNKNOWN
final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT_PLUGIN_ERROR);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
"foo", "bar", internalCallContext);
testListener.assertListenerStatus();
@@ -400,7 +400,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
// Artificially move the transaction status to PENDING
final String paymentStateName = paymentSMHelper.getPendingStateForTransaction(TransactionType.AUTHORIZE).toString();
testListener.pushExpectedEvent(NextEvent.PAYMENT);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(),
"loup", "chat", internalCallContext);
testListener.assertListenerStatus();
@@ -439,7 +439,7 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
testListener.pushExpectedEvent(NextEvent.PAYMENT);
- paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+ paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), null, payment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
payment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(),
"loup", "chat", internalCallContext);
testListener.assertListenerStatus();
diff --git a/payment/src/test/resources/org/killbill/billing/payment/PermissivePaymentStates.xml b/payment/src/test/resources/org/killbill/billing/payment/PermissivePaymentStates.xml
new file mode 100644
index 0000000..99d4bb9
--- /dev/null
+++ b/payment/src/test/resources/org/killbill/billing/payment/PermissivePaymentStates.xml
@@ -0,0 +1,541 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<stateMachineConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="StateMachineConfig.xsd">
+
+ <stateMachines>
+ <stateMachine name="BIG_BANG">
+ <states>
+ <state name="BIG_BANG_INIT"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>BIG_BANG_INIT</initialState>
+ <operation>OP_DUMMY</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>BIG_BANG_INIT</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_DUMMY"/>
+ </operations>
+ </stateMachine>
+ <stateMachine name="AUTHORIZE">
+ <states>
+ <state name="AUTH_INIT"/>
+ <state name="AUTH_PENDING"/>
+ <state name="AUTH_SUCCESS"/>
+ <state name="AUTH_FAILED"/>
+ <state name="AUTH_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>AUTH_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>AUTH_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>AUTH_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_PENDING</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>AUTH_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_PENDING</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>AUTH_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_PENDING</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>AUTH_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>AUTH_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_AUTHORIZE"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="CAPTURE">
+ <states>
+ <state name="CAPTURE_INIT"/>
+ <state name="CAPTURE_PENDING"/>
+ <state name="CAPTURE_SUCCESS"/>
+ <state name="CAPTURE_FAILED"/>
+ <state name="CAPTURE_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CAPTURE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CAPTURE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>CAPTURE_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_PENDING</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CAPTURE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_PENDING</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CAPTURE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_PENDING</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CAPTURE_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>CAPTURE_INIT</initialState>
+ <operation>OP_CAPTURE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CAPTURE_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_CAPTURE"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="PURCHASE">
+ <states>
+ <state name="PURCHASE_INIT"/>
+ <state name="PURCHASE_PENDING"/>
+ <state name="PURCHASE_SUCCESS"/>
+ <state name="PURCHASE_FAILED"/>
+ <state name="PURCHASE_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>PURCHASE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>PURCHASE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>PURCHASE_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_PENDING</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>PURCHASE_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_PENDING</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>PURCHASE_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_PENDING</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>PURCHASE_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>PURCHASE_INIT</initialState>
+ <operation>OP_PURCHASE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>PURCHASE_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_PURCHASE"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="REFUND">
+ <states>
+ <state name="REFUND_INIT"/>
+ <state name="REFUND_PENDING"/>
+ <state name="REFUND_SUCCESS"/>
+ <state name="REFUND_FAILED"/>
+ <state name="REFUND_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>REFUND_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>REFUND_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>REFUND_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_PENDING</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>REFUND_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_PENDING</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>REFUND_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_PENDING</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>REFUND_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>REFUND_INIT</initialState>
+ <operation>OP_REFUND</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>REFUND_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_REFUND"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="CREDIT">
+ <states>
+ <state name="CREDIT_INIT"/>
+ <state name="CREDIT_PENDING"/>
+ <state name="CREDIT_SUCCESS"/>
+ <state name="CREDIT_FAILED"/>
+ <state name="CREDIT_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CREDIT_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CREDIT_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>CREDIT_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_PENDING</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CREDIT_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_PENDING</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CREDIT_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_PENDING</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CREDIT_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>CREDIT_INIT</initialState>
+ <operation>OP_CREDIT</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CREDIT_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_CREDIT"/>
+ </operations>
+ </stateMachine>
+
+ <stateMachine name="VOID">
+ <states>
+ <state name="VOID_INIT"/>
+ <state name="VOID_PENDING"/>
+ <state name="VOID_SUCCESS"/>
+ <state name="VOID_FAILED"/>
+ <state name="VOID_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>VOID_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>VOID_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>PENDING</operationResult>
+ <finalState>VOID_PENDING</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_PENDING</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>VOID_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_PENDING</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>VOID_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_PENDING</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>VOID_ERRORED</finalState>
+ </transition>
+ <transition>
+ <initialState>VOID_INIT</initialState>
+ <operation>OP_VOID</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>VOID_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_VOID"/>
+ </operations>
+ </stateMachine>
+ <stateMachine name="CHARGEBACK">
+ <states>
+ <state name="CHARGEBACK_INIT"/>
+ <state name="CHARGEBACK_SUCCESS"/>
+ <state name="CHARGEBACK_FAILED"/>
+ <state name="CHARGEBACK_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>CHARGEBACK_INIT</initialState>
+ <operation>OP_CHARGEBACK</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>CHARGEBACK_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>CHARGEBACK_INIT</initialState>
+ <operation>OP_CHARGEBACK</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>CHARGEBACK_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>CHARGEBACK_INIT</initialState>
+ <operation>OP_CHARGEBACK</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>CHARGEBACK_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_CHARGEBACK"/>
+ </operations>
+ </stateMachine>
+ </stateMachines>
+
+ <linkStateMachines>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>AUTHORIZE</finalStateMachine>
+ <finalState>AUTH_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>PURCHASE</finalStateMachine>
+ <finalState>PURCHASE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>CREDIT</finalStateMachine>
+ <finalState>CREDIT_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>AUTHORIZE</initialStateMachine>
+ <initialState>AUTH_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>AUTHORIZE</initialStateMachine>
+ <initialState>AUTH_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>VOID</initialStateMachine>
+ <initialState>VOID_SUCCESS</initialState>
+ <finalStateMachine>CREDIT</finalStateMachine>
+ <finalState>CREDIT_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>CAPTURE</finalStateMachine>
+ <finalState>CAPTURE_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CAPTURE</initialStateMachine>
+ <initialState>CAPTURE_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>REFUND</initialStateMachine>
+ <initialState>REFUND_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>PURCHASE</initialStateMachine>
+ <initialState>PURCHASE_SUCCESS</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>PURCHASE</initialStateMachine>
+ <initialState>PURCHASE_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CREDIT</initialStateMachine>
+ <initialState>CREDIT_SUCCESS</initialState>
+ <finalStateMachine>VOID</finalStateMachine>
+ <finalState>VOID_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CHARGEBACK</initialStateMachine>
+ <initialState>CHARGEBACK_SUCCESS</initialState>
+ <finalStateMachine>CHARGEBACK</finalStateMachine>
+ <finalState>CHARGEBACK_INIT</finalState>
+ </linkStateMachine>
+ <linkStateMachine>
+ <initialStateMachine>CHARGEBACK</initialStateMachine>
+ <initialState>CHARGEBACK_FAILED</initialState>
+ <finalStateMachine>REFUND</finalStateMachine>
+ <finalState>REFUND_INIT</finalState>
+ </linkStateMachine>
+ </linkStateMachines>
+</stateMachineConfig>
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 3bc767e..c28d3c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.101</version>
+ <version>0.104</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.17.1-SNAPSHOT</version>
profiles/killbill/pom.xml 5(+5 -0)
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index d441c3a..60a32dd 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -60,6 +60,11 @@
<scope>compile</scope>
</dependency>
<dependency>
+ <groupId>com.jayway.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>com.palominolabs.metrics</groupId>
<artifactId>metrics-guice</artifactId>
<scope>runtime</scope>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
index 66bdcf1..552a6a1 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
@@ -60,6 +60,12 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
protected static final String reason = "i am god";
protected static final String comment = "no comment";
+ protected static RequestOptions requestOptions = RequestOptions.builder()
+ .withCreatedBy(createdBy)
+ .withReason(reason)
+ .withComment(comment)
+ .build();
+
protected KillBillClient killBillClient;
protected KillBillHttpClient killBillHttpClient;
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
index 6b39e26..5972d5f 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
@@ -43,6 +43,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.util.api.AuditLevel;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -66,6 +67,10 @@ public class TestAccount extends TestJaxrsBase {
public void beforeMethod() throws Exception {
super.beforeMethod();
mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(PLUGIN_NAME);
+ }
+
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
mockPaymentProviderPlugin.clear();
}
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 d12b8d3..5c21ce8 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
@@ -43,6 +43,7 @@ import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
import org.killbill.billing.payment.provider.MockPaymentControlProviderPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -69,7 +70,6 @@ public class TestPayment extends TestJaxrsBase {
public void beforeMethod() throws Exception {
super.beforeMethod();
mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(PLUGIN_NAME);
- mockPaymentProviderPlugin.clear();
mockPaymentControlProviderPlugin = new MockPaymentControlProviderPlugin();
controlPluginRegistry.registerService(new OSGIServiceDescriptor() {
@@ -90,6 +90,11 @@ public class TestPayment extends TestJaxrsBase {
}, mockPaymentControlProviderPlugin);
}
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
+ mockPaymentProviderPlugin.clear();
+ }
+
@Test(groups = "slow")
public void testWithFailedPayment() throws Exception {
final Account account = createAccountWithDefaultPaymentMethod();
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java
index 54a51cc..f2539d8 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTenantKV.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -17,32 +17,151 @@
package org.killbill.billing.jaxrs;
+import java.math.BigDecimal;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.RequestOptions;
+import org.killbill.billing.client.model.Account;
+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.PaymentTransaction;
+import org.killbill.billing.client.model.PluginProperty;
+import org.killbill.billing.client.model.Tenant;
import org.killbill.billing.client.model.TenantKey;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.tenant.api.TenantKV;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
+import com.jayway.awaitility.Awaitility;
+import com.jayway.awaitility.Duration;
public class TestTenantKV extends TestJaxrsBase {
@Test(groups = "slow", description = "Upload and retrieve a per plugin config")
public void testPerTenantPluginConfig() throws Exception {
-
final String pluginName = "PLUGIN_FOO";
final String pluginPath = Resources.getResource("plugin.yml").getPath();
final TenantKey tenantKey0 = killBillClient.registerPluginConfigurationForTenant(pluginName, pluginPath, createdBy, reason, comment);
Assert.assertEquals(tenantKey0.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
- final TenantKey tenantKey1 = killBillClient.getPluginConfigurationForTenant(pluginName);
+ final TenantKey tenantKey1 = killBillClient.getPluginConfigurationForTenant(pluginName);
Assert.assertEquals(tenantKey1.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
Assert.assertEquals(tenantKey1.getValues().size(), 1);
killBillClient.unregisterPluginConfigurationForTenant(pluginName, createdBy, reason, comment);
- final TenantKey tenantKey2 = killBillClient.getPluginConfigurationForTenant(pluginName);
+ final TenantKey tenantKey2 = killBillClient.getPluginConfigurationForTenant(pluginName);
Assert.assertEquals(tenantKey2.getKey(), TenantKV.TenantKey.PLUGIN_CONFIG_.toString() + pluginName);
Assert.assertEquals(tenantKey2.getValues().size(), 0);
}
+ @Test(groups = "slow", description = "Upload and retrieve a per plugin payment state machine config")
+ public void testPerTenantPluginPaymentStateMachineConfig() throws Exception {
+ // Create another tenant - it will have a different state machine
+ final Tenant otherTenantWithDifferentStateMachine = new Tenant();
+ otherTenantWithDifferentStateMachine.setApiKey(UUID.randomUUID().toString());
+ otherTenantWithDifferentStateMachine.setApiSecret(UUID.randomUUID().toString());
+ killBillClient.createTenant(otherTenantWithDifferentStateMachine, requestOptions);
+ final RequestOptions requestOptionsOtherTenant = requestOptions.extend()
+ .withTenantApiKey(otherTenantWithDifferentStateMachine.getApiKey())
+ .withTenantApiSecret(otherTenantWithDifferentStateMachine.getApiSecret())
+ .build();
+
+ // Verify initial state
+ final TenantKey emptyTenantKey = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
+ Assert.assertEquals(emptyTenantKey.getValues().size(), 0);
+ final TenantKey emptyTenantKeyOtherTenant = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ Assert.assertEquals(emptyTenantKeyOtherTenant.getValues().size(), 0);
+
+ final String stateMachineConfigPath = Resources.getResource("SimplePaymentStates.xml").getPath();
+ final TenantKey tenantKey0 = killBillClient.registerPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, stateMachineConfigPath, requestOptionsOtherTenant);
+ Assert.assertEquals(tenantKey0.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
+
+ // Verify only the other tenant has the new state machine
+ final TenantKey emptyTenantKey1 = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptions);
+ Assert.assertEquals(emptyTenantKey1.getValues().size(), 0);
+ final TenantKey tenantKey1OtherTenant = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ Assert.assertEquals(tenantKey1OtherTenant.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
+ Assert.assertEquals(tenantKey1OtherTenant.getValues().size(), 1);
+
+ // Create an auth in both tenant
+ final Payment payment = createComboPaymentTransaction(requestOptions);
+ final Payment paymentOtherTenant = createComboPaymentTransaction(requestOptionsOtherTenant);
+
+ // Void in the first tenant (allowed by the default state machine)
+ final Payment voidPayment = killBillClient.voidPayment(payment.getPaymentId(), payment.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptions);
+ Assert.assertEquals(voidPayment.getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
+ Assert.assertEquals(voidPayment.getTransactions().get(1).getStatus(), TransactionStatus.SUCCESS.toString());
+
+ // Void in the other tenant (disallowed)
+ try {
+ killBillClient.voidPayment(paymentOtherTenant.getPaymentId(), paymentOtherTenant.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptionsOtherTenant);
+ Assert.fail();
+ } catch (final KillBillClientException e) {
+ Assert.assertEquals((int) e.getBillingException().getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ }
+
+ // Remove the custom state machine
+ killBillClient.unregisterPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ final TenantKey tenantKey2 = killBillClient.getPluginPaymentStateMachineConfigurationForTenant(PLUGIN_NAME, requestOptionsOtherTenant);
+ Assert.assertEquals(tenantKey2.getKey(), TenantKV.TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + PLUGIN_NAME);
+ Assert.assertEquals(tenantKey2.getValues().size(), 0);
+
+ final AtomicReference<Payment> voidPaymentOtherTenant2Ref = new AtomicReference<Payment>();
+ Awaitility.await()
+ .atMost(8, TimeUnit.SECONDS)
+ .pollInterval(Duration.TWO_SECONDS)
+ .until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ // The void should now go through
+ try {
+ final Payment voidPaymentOtherTenant2 = killBillClient.voidPayment(paymentOtherTenant.getPaymentId(), paymentOtherTenant.getPaymentExternalKey(), UUID.randomUUID().toString(), ImmutableList.<String>of(), ImmutableMap.<String, String>of(), requestOptionsOtherTenant);
+ voidPaymentOtherTenant2Ref.set(voidPaymentOtherTenant2);
+ return voidPaymentOtherTenant2 != null;
+ } catch (final KillBillClientException e) {
+ // Invalidation hasn't happened yet
+ return false;
+ }
+ }
+ });
+ Assert.assertEquals(voidPaymentOtherTenant2Ref.get().getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
+ Assert.assertEquals(voidPaymentOtherTenant2Ref.get().getTransactions().get(1).getStatus(), TransactionStatus.SUCCESS.toString());
+ }
+
+ private Payment createComboPaymentTransaction(final RequestOptions requestOptions) throws KillBillClientException {
+ 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 authTransactionExternalKey = UUID.randomUUID().toString();
+ final PaymentTransaction authTransactionJson = new PaymentTransaction();
+ authTransactionJson.setAmount(BigDecimal.TEN);
+ authTransactionJson.setCurrency(accountJson.getCurrency());
+ authTransactionJson.setPaymentExternalKey(UUID.randomUUID().toString());
+ authTransactionJson.setTransactionExternalKey(authTransactionExternalKey);
+ authTransactionJson.setTransactionType("AUTHORIZE");
+
+ final ComboPaymentTransaction comboAuthorization = new ComboPaymentTransaction(accountJson, paymentMethodJson, authTransactionJson, ImmutableList.<PluginProperty>of(), ImmutableList.<PluginProperty>of());
+ final Payment payment = killBillClient.createPayment(comboAuthorization, ImmutableMap.<String, String>of(), requestOptions);
+ Assert.assertEquals(payment.getTransactions().get(0).getStatus(), TransactionStatus.SUCCESS.toString());
+
+ return payment;
+ }
}
diff --git a/profiles/killbill/src/test/resources/SimplePaymentStates.xml b/profiles/killbill/src/test/resources/SimplePaymentStates.xml
new file mode 100644
index 0000000..abf8e46
--- /dev/null
+++ b/profiles/killbill/src/test/resources/SimplePaymentStates.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2016 Groupon, Inc
+ ~ Copyright 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.
+ -->
+
+<stateMachineConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="StateMachineConfig.xsd">
+
+ <stateMachines>
+ <stateMachine name="BIG_BANG">
+ <states>
+ <state name="BIG_BANG_INIT"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>BIG_BANG_INIT</initialState>
+ <operation>OP_DUMMY</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>BIG_BANG_INIT</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_DUMMY"/>
+ </operations>
+ </stateMachine>
+ <stateMachine name="AUTHORIZE">
+ <states>
+ <state name="AUTH_INIT"/>
+ <state name="AUTH_SUCCESS"/>
+ <state name="AUTH_FAILED"/>
+ <state name="AUTH_ERRORED"/>
+ </states>
+ <transitions>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>SUCCESS</operationResult>
+ <finalState>AUTH_SUCCESS</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>FAILURE</operationResult>
+ <finalState>AUTH_FAILED</finalState>
+ </transition>
+ <transition>
+ <initialState>AUTH_INIT</initialState>
+ <operation>OP_AUTHORIZE</operation>
+ <operationResult>EXCEPTION</operationResult>
+ <finalState>AUTH_ERRORED</finalState>
+ </transition>
+ </transitions>
+ <operations>
+ <operation name="OP_AUTHORIZE"/>
+ </operations>
+ </stateMachine>
+ </stateMachines>
+
+ <linkStateMachines>
+ <linkStateMachine>
+ <initialStateMachine>BIG_BANG</initialStateMachine>
+ <initialState>BIG_BANG_INIT</initialState>
+ <finalStateMachine>AUTHORIZE</finalStateMachine>
+ <finalState>AUTH_INIT</finalState>
+ </linkStateMachine>
+ </linkStateMachines>
+</stateMachineConfig>
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
index 50923c6..7fb088e 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * 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
@@ -109,11 +109,17 @@ public class DefaultTenantInternalApi implements TenantInternalApi {
}
@Override
+ public String getPluginPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) {
+ final String pluginConfigKey = TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_ + pluginName;
+ final List<String> values = tenantDao.getTenantValueForKey(pluginConfigKey, tenantContext);
+ return getUniqueValue(values, "payment state machine for plugin " + pluginConfigKey, tenantContext);
+ }
+
+ @Override
public List<String> getTenantValuesForKey(final String key, final InternalTenantContext tenantContext) {
return tenantDao.getTenantValueForKey(key, tenantContext);
}
-
@Override
public Tenant getTenantByApiKey(final String key) throws TenantApiException {
final TenantModelDao tenant = tenantDao.getTenantByApiKey(key);
@@ -123,8 +129,6 @@ public class DefaultTenantInternalApi implements TenantInternalApi {
return new DefaultTenant(tenant);
}
-
-
private String getUniqueValue(final List<String> values, final String msg, final InternalTenantContext tenantContext) {
if (values.isEmpty()) {
return null;
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
index 8bae33c..cc48190 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
@@ -17,9 +17,8 @@
package org.killbill.billing.tenant.api;
-import java.util.HashMap;
+import java.util.Collection;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@@ -46,8 +45,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
+import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
/**
* This class manages the callbacks that have been registered when per tenant objects have been inserted into the
@@ -65,7 +66,7 @@ public class TenantCacheInvalidation {
private static final Logger logger = LoggerFactory.getLogger(TenantCacheInvalidation.class);
- private final Map<TenantKey, CacheInvalidationCallback> cache;
+ private final Multimap<TenantKey, CacheInvalidationCallback> cache;
private final TenantBroadcastDao broadcastDao;
private final TenantConfig tenantConfig;
private final PersistentBus eventBus;
@@ -80,7 +81,7 @@ public class TenantCacheInvalidation {
@Named(DefaultTenantModule.NO_CACHING_TENANT) final TenantDao tenantDao,
final PersistentBus eventBus,
final TenantConfig tenantConfig) {
- this.cache = new HashMap<TenantKey, CacheInvalidationCallback>();
+ this.cache = HashMultimap.<TenantKey, CacheInvalidationCallback>create();
this.broadcastDao = broadcastDao;
this.tenantConfig = tenantConfig;
this.tenantDao = tenantDao;
@@ -122,12 +123,11 @@ public class TenantCacheInvalidation {
}
public void registerCallback(final TenantKey key, final CacheInvalidationCallback value) {
- if (!cache.containsKey(key)) {
- cache.put(key, value);
- }
+ cache.put(key, value);
+
}
- public CacheInvalidationCallback getCacheInvalidation(final TenantKey key) {
+ public Collection<CacheInvalidationCallback> getCacheInvalidations(final TenantKey key) {
return cache.get(key);
}
@@ -176,10 +176,12 @@ public class TenantCacheInvalidation {
try {
final TenantKeyAndCookie tenantKeyAndCookie = extractTenantKeyAndCookie(cur.getType());
if (tenantKeyAndCookie != null) {
- final CacheInvalidationCallback callback = parent.getCacheInvalidation(tenantKeyAndCookie.getTenantKey());
- if (callback != null) {
+ final Collection<CacheInvalidationCallback> callbacks = parent.getCacheInvalidations(tenantKeyAndCookie.getTenantKey());
+ if (!callbacks.isEmpty()) {
final InternalTenantContext tenantContext = new InternalTenantContext(cur.getTenantRecordId());
- callback.invalidateCache(tenantKeyAndCookie.getTenantKey(), tenantKeyAndCookie.getCookie(), tenantContext);
+ for (final CacheInvalidationCallback callback : callbacks) {
+ callback.invalidateCache(tenantKeyAndCookie.getTenantKey(), tenantKeyAndCookie.getCookie(), tenantContext);
+ }
final Long tenantKvsTargetRecordId = cur.getTargetRecordId();
final BusInternalEvent event;
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
index be914f5..5fec9ea 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
@@ -62,6 +62,7 @@ public class DefaultTenantUserApi implements TenantUserApi {
.add(TenantKey.INVOICE_TEMPLATE)
.add(TenantKey.INVOICE_TRANSLATION_)
.add(TenantKey.PLUGIN_CONFIG_)
+ .add(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_)
.add(TenantKey.PUSH_NOTIFICATION_CB).build();
private final TenantDao tenantDao;
diff --git a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
index f20c9a6..a70556c 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
@@ -32,6 +32,7 @@ public @interface Cachable {
String AUDIT_LOG_CACHE_NAME = "audit-log";
String AUDIT_LOG_VIA_HISTORY_CACHE_NAME = "audit-log-via-history";
String TENANT_CATALOG_CACHE_NAME = "tenant-catalog";
+ String TENANT_PAYMENT_STATE_MACHINE_CONFIG_CACHE_NAME = "tenant-payment-state-machine-config";
String TENANT_OVERDUE_CONFIG_CACHE_NAME = "tenant-overdue-config";
String TENANT_CONFIG_CACHE_NAME = "tenant-config";
String TENANT_KV_CACHE_NAME = "tenant-kv";
@@ -65,6 +66,9 @@ public @interface Cachable {
/* Tenant catalog cache */
TENANT_CATALOG(TENANT_CATALOG_CACHE_NAME, false),
+ /* Tenant payment state machine config cache */
+ TENANT_PAYMENT_STATE_MACHINE_CONFIG(TENANT_PAYMENT_STATE_MACHINE_CONFIG_CACHE_NAME, false),
+
/* Tenant overdue config cache */
TENANT_OVERDUE_CONFIG(TENANT_OVERDUE_CONFIG_CACHE_NAME, false),
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
index bf2f6b1..bdf1302 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
@@ -22,10 +22,14 @@ import java.util.Map;
import javax.inject.Inject;
import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
// Kill Bill generic cache dispatcher
public class CacheControllerDispatcher {
+ private static final Logger logger = LoggerFactory.getLogger(CacheControllerDispatcher.class);
+
public static final String CACHE_KEY_SEPARATOR = "::";
private final Map<CacheType, CacheController<Object, Object>> caches;
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
index 7ce311c..305e6f2 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
@@ -64,7 +64,8 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
final TenantOverdueConfigCacheLoader tenantOverdueConfigCacheLoader,
final TenantKVCacheLoader tenantKVCacheLoader,
final TenantCacheLoader tenantCacheLoader,
- final OverriddenPlanCacheLoader overriddenPlanCacheLoader) {
+ final OverriddenPlanCacheLoader overriddenPlanCacheLoader,
+ final TenantStateMachineConfigCacheLoader tenantStateMachineConfigCacheLoader) {
this.metricRegistry = metricRegistry;
this.cacheConfig = cacheConfig;
cacheLoaders.add(accountCacheLoader);
@@ -81,6 +82,7 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
cacheLoaders.add(tenantKVCacheLoader);
cacheLoaders.add(tenantCacheLoader);
cacheLoaders.add(overriddenPlanCacheLoader);
+ cacheLoaders.add(tenantStateMachineConfigCacheLoader);
}
@Override
@@ -100,6 +102,11 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
final Ehcache cache = cacheManager.getEhcache(cacheLoader.getCacheType().getCacheName());
+ if (cache == null) {
+ logger.warn("Cache for cacheName='{}' not configured - check your ehcache.xml", cacheLoader.getCacheType().getCacheName());
+ continue;
+ }
+
// Make sure we start from a clean state - this is mainly useful for tests
for (final CacheLoader existingCacheLoader : cache.getRegisteredCacheLoaders()) {
cache.unregisterCacheLoader(existingCacheLoader);
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
new file mode 100644
index 0000000..c6d5489
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 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.util.cache;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class TenantStateMachineConfigCacheLoader extends BaseCacheLoader {
+
+ private static final Pattern PATTERN = Pattern.compile(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + "(.*)");
+ private static final Logger log = LoggerFactory.getLogger(TenantStateMachineConfigCacheLoader.class);
+
+ private final TenantInternalApi tenantApi;
+
+ @Inject
+ public TenantStateMachineConfigCacheLoader(final TenantInternalApi tenantApi) {
+ super();
+ this.tenantApi = tenantApi;
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final String[] parts = ((String) key).split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+ final String rawKey = parts[0];
+ final Matcher matcher = PATTERN.matcher(rawKey);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Unexpected key " + rawKey);
+ }
+ final String pluginName = matcher.group(1);
+ final String tenantRecordId = parts[1];
+
+ final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
+ final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0];
+
+ final InternalTenantContext internalTenantContext = new InternalTenantContext(Long.valueOf(tenantRecordId));
+ final String stateMachineConfigXML = tenantApi.getPluginPaymentStateMachineConfig(pluginName, internalTenantContext);
+ if (stateMachineConfigXML == null) {
+ return null;
+ }
+
+ try {
+ log.info("Loading config state machine cache for pluginName='{}', tenantRecordId='{}'", pluginName, internalTenantContext.getTenantRecordId());
+ return callback.loadStateMachineConfig(stateMachineConfigXML);
+ } catch (final PaymentApiException e) {
+ throw new IllegalStateException(String.format("Failed to de-serialize state machine config for tenantRecordId='%s'", internalTenantContext.getTenantRecordId()), e);
+ }
+ }
+
+ public interface LoaderCallback {
+
+ public Object loadStateMachineConfig(final String stateMachineConfigXML) throws PaymentApiException;
+ }
+}
util/src/main/resources/ehcache.xml 18(+16 -2)
diff --git a/util/src/main/resources/ehcache.xml b/util/src/main/resources/ehcache.xml
index 6d9c27f..cd6b114 100644
--- a/util/src/main/resources/ehcache.xml
+++ b/util/src/main/resources/ehcache.xml
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- ~ Copyright 2010-2013 Ning, Inc.
+ ~ Copyright 2010-2014 Ning, Inc.
+ ~ Copyright 2014-2016 Groupon, Inc
+ ~ Copyright 2014-2016 The Billing Project, LLC
~
- ~ Ning licenses this file to you under the Apache License, version 2.0
+ ~ 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:
~
@@ -219,6 +221,18 @@
properties=""/>
</cache>
+ <cache name="tenant-payment-state-machine-config"
+ maxElementsInMemory="100"
+ maxElementsOnDisk="0"
+ overflowToDisk="false"
+ diskPersistent="false"
+ memoryStoreEvictionPolicy="LFU"
+ statistics="true"
+ >
+ <cacheEventListenerFactory
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
+ properties=""/>
+ </cache>
</ehcache>