killbill-aplcache
Changes
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java 2(+2 -0)
payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java 37(+37 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java 47(+47 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java 24(+20 -4)
payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java 154(+136 -18)
payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java 41(+30 -11)
Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
index 7444840..f9d1984 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
@@ -24,8 +24,6 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
-import javax.annotation.Nullable;
-
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
@@ -48,10 +46,12 @@ import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentOptions;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.mockito.Mockito;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.tweak.HandleCallback;
import org.testng.Assert;
@@ -68,8 +68,6 @@ import static org.testng.Assert.assertTrue;
public class TestInvoicePayment extends TestIntegrationBase {
-
-
@Test(groups = "slow")
public void testCancellationEOTWithInvoiceItemAdjustmentsOnInvoiceWithMultipleItems() throws Exception {
final int billingDay = 1;
@@ -129,12 +127,8 @@ public class TestInvoicePayment extends TestIntegrationBase {
Assert.assertEquals(fourthInvoice.getInvoiceItems().size(), 1);
invoiceChecker.checkInvoice(account.getId(), 4, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2016, 11, 1), new LocalDate(2016, 12, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-
-
}
-
-
@Test(groups = "slow")
public void testPartialPaymentByPaymentPlugin() throws Exception {
// 2012-05-01T00:03:42.000Z
@@ -600,6 +594,85 @@ public class TestInvoicePayment extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testWithPendingPaymentThenSuccess() throws Exception {
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ paymentPlugin.makeNextPaymentPending();
+
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Transition the payment to success
+ final List<String> paymentControlPluginNames = ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ final PaymentOptions paymentOptions = Mockito.mock(PaymentOptions.class);
+ Mockito.when(paymentOptions.getPaymentControlPluginNames()).thenReturn(paymentControlPluginNames);
+
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ paymentApi.notifyPendingTransactionOfStateChangedWithPaymentControl(account, payments.get(0).getTransactions().get(0).getId(), true, paymentOptions, callContext);
+ assertListenerStatus();
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertTrue(invoice2.getPayments().get(0).isSuccess());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+ }
+
+ @Test(groups = "slow")
public void testWithIncompletePaymentAttempt() throws Exception {
// 2012-05-01T00:03:42.000Z
clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index d7eb9fd..90f868b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -100,7 +100,6 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
dao.notifyOfPaymentInit(new InvoicePaymentModelDao(invoicePayment), context);
}
-
@Override
public void recordPaymentAttemptCompletion(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final String transactionExternalKey, final DateTime paymentDate, final boolean success, final InternalCallContext context) throws InvoiceApiException {
final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, transactionExternalKey, success);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index fc59743..d0ca343 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -732,13 +732,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
notifyOfPaymentCompletionInternal(invoicePayment, false, context);
}
-
@Override
public void notifyOfPaymentCompletion(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
notifyOfPaymentCompletionInternal(invoicePayment, true, context);
}
- public void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
+ private void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
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 b4bb5f4..c98a029 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
@@ -722,7 +722,50 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
@Override
public Payment notifyPendingTransactionOfStateChangedWithPaymentControl(final Account account, final UUID paymentTransactionId, final boolean isSuccess, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
- throw new IllegalStateException("Not implemented");
+ final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
+ if (paymentControlPluginNames.isEmpty()) {
+ return notifyPendingTransactionOfStateChanged(account, paymentTransactionId, isSuccess, callContext);
+ }
+
+ checkNotNullParameter(account, "account");
+ checkNotNullParameter(paymentTransactionId, "paymentTransactionId");
+
+ final String transactionType = "NOTIFY_STATE_CHANGE";
+ Payment payment = null;
+ PaymentTransaction paymentTransaction = null;
+ PaymentApiException exception = null;
+ try {
+ logEnterAPICall(log, transactionType, account, null, null, paymentTransactionId, null, null, null, null, null, paymentControlPluginNames);
+
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+ payment = pluginControlPaymentProcessor.notifyPendingPaymentOfStateChanged(IS_API_PAYMENT, account, paymentTransactionId, isSuccess, paymentControlPluginNames, callContext, internalCallContext);
+
+ paymentTransaction = Iterables.<PaymentTransaction>tryFind(payment.getTransactions(),
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getId().equals(paymentTransactionId);
+ }
+ }).orNull();
+ return payment;
+ } catch (final PaymentApiException e) {
+ exception = e;
+ throw e;
+ } finally {
+ logExitAPICall(log,
+ transactionType,
+ account,
+ payment != null ? payment.getPaymentMethodId() : null,
+ payment != null ? payment.getId() : null,
+ paymentTransaction != null ? paymentTransaction.getId() : null,
+ paymentTransaction != null ? paymentTransaction.getProcessedAmount() : null,
+ paymentTransaction != null ? paymentTransaction.getProcessedCurrency() : null,
+ payment != null ? payment.getExternalKey() : null,
+ paymentTransaction != null ? paymentTransaction.getExternalKey() : null,
+ paymentTransaction != null ? paymentTransaction.getTransactionStatus() : null,
+ paymentControlPluginNames,
+ exception);
+ }
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
index a33e4e0..2aa31ed 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
@@ -150,8 +150,10 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
final boolean isApiPayment = true; // unclear
final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(),
isApiPayment,
+ null,
transaction.getPaymentId(),
attempt.getPaymentExternalKey(),
+ transaction.getId(),
transaction.getTransactionExternalKey(),
transaction.getTransactionType(),
account,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
index 22e12ff..736fa84 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
@@ -46,6 +46,7 @@ import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner.
import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dao.PluginPropertySerializer;
import org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
@@ -198,6 +199,42 @@ public class PluginControlPaymentProcessor extends ProcessorBase {
callContext, internalCallContext);
}
+ public Payment notifyPendingPaymentOfStateChanged(final boolean isApiPayment, final Account account, final UUID paymentTransactionId, final boolean isSuccess, final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentTransactionId, internalCallContext);
+ final List<PaymentAttemptModelDao> attempts = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey(), internalCallContext);
+ final PaymentAttemptModelDao attempt = Iterables.find(attempts,
+ new Predicate<PaymentAttemptModelDao>() {
+ @Override
+ public boolean apply(final PaymentAttemptModelDao input) {
+ return input.getTransactionId().equals(paymentTransactionId);
+ }
+ });
+
+ final Iterable<PluginProperty> pluginProperties;
+ try {
+ pluginProperties = PluginPropertySerializer.deserialize(attempt.getPluginProperties());
+ } catch (final PluginPropertySerializerException e) {
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, String.format("Unable to deserialize payment attemptId='%s' properties", attempt.getId()));
+ }
+
+ return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
+ isSuccess,
+ paymentTransactionModelDao.getTransactionType(),
+ ControlOperation.NOTIFICATION_OF_STATE_CHANGE,
+ account,
+ attempt.getPaymentMethodId(),
+ paymentTransactionModelDao.getPaymentId(),
+ attempt.getPaymentExternalKey(),
+ paymentTransactionId,
+ paymentTransactionModelDao.getTransactionExternalKey(),
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ pluginProperties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
public Payment createChargeback(final boolean isApiPayment, final Account account, final UUID paymentId, final String transactionExternalKey, final BigDecimal amount, final Currency currency,
final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java
new file mode 100644
index 0000000..de2bfbf
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java
@@ -0,0 +1,47 @@
+/*
+ * 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.core.sm.control;
+
+import org.killbill.automaton.OperationResult;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.dispatcher.PluginDispatcher;
+import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.commons.locker.GlobalLocker;
+
+public class NotificationOfStateChangeControlOperation extends OperationControlCallback {
+
+ public NotificationOfStateChangeControlOperation(final GlobalLocker locker,
+ final PluginDispatcher<OperationResult> paymentPluginDispatcher,
+ final PaymentConfig paymentConfig,
+ final PaymentStateControlContext paymentStateContext,
+ final PaymentProcessor paymentProcessor,
+ final ControlPluginRunner controlPluginRunner) {
+ super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner);
+ }
+
+ @Override
+ protected Payment doCallSpecificOperationCallback() throws PaymentApiException {
+ return paymentProcessor.notifyPendingPaymentOfStateChanged(paymentStateControlContext.getAccount(),
+ paymentStateControlContext.getTransactionId(),
+ paymentStateControlContext.isSuccess(),
+ paymentStateControlContext.getCallContext(),
+ paymentStateControlContext.getInternalCallContext());
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
index 2841907..ecae4b4 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
@@ -40,16 +40,28 @@ import com.google.common.collect.Iterables;
public class PaymentStateControlContext extends PaymentStateContext {
+ private final Boolean isSuccess;
+
private DateTime retryDate;
private List<String> paymentControlPluginNames;
private Payment result;
- public PaymentStateControlContext(@Nullable final List<String> paymentControlPluginNames, final boolean isApiPayment, @Nullable final UUID paymentId, final String paymentExternalKey,
- @Nullable final String paymentTransactionExternalKey, final TransactionType transactionType,
- final Account account, @Nullable final UUID paymentMethodId, final BigDecimal amount, final Currency currency,
+ public PaymentStateControlContext(@Nullable final List<String> paymentControlPluginNames,
+ final boolean isApiPayment,
+ final Boolean isSuccess,
+ @Nullable final UUID paymentId,
+ final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ @Nullable final String paymentTransactionExternalKey,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ final BigDecimal amount,
+ final Currency currency,
final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext, final CallContext callContext) {
- super(isApiPayment, paymentId, null, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
+ super(isApiPayment, paymentId, transactionId, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
this.paymentControlPluginNames = paymentControlPluginNames;
+ this.isSuccess = isSuccess;
}
public DateTime getRetryDate() {
@@ -72,6 +84,10 @@ public class PaymentStateControlContext extends PaymentStateContext {
this.result = result;
}
+ public Boolean isSuccess() {
+ return isSuccess;
+ }
+
public PaymentTransaction getCurrentTransaction() {
if (result == null || result.getTransactions() == null) {
return null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
index 975c4b8..aa05dfa 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
@@ -54,6 +54,7 @@ import org.killbill.billing.payment.core.sm.control.CreditControlOperation;
import org.killbill.billing.payment.core.sm.control.DefaultControlCompleted;
import org.killbill.billing.payment.core.sm.control.DefaultControlInitiated;
import org.killbill.billing.payment.core.sm.control.NoopControlInitiated;
+import org.killbill.billing.payment.core.sm.control.NotificationOfStateChangeControlOperation;
import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
import org.killbill.billing.payment.core.sm.control.PurchaseControlOperation;
import org.killbill.billing.payment.core.sm.control.RefundControlOperation;
@@ -83,7 +84,8 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
CREDIT,
PURCHASE,
REFUND,
- VOID
+ VOID,
+ NOTIFICATION_OF_STATE_CHANGE
}
protected final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry;
@@ -107,25 +109,138 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
this.paymentConfig = paymentConfig;
}
- public Payment run(final boolean isApiPayment, final TransactionType transactionType, final ControlOperation controlOperation, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
+ public Payment run(final boolean isApiPayment,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ return run(paymentControlStateMachineHelper.getInitialState(),
+ isApiPayment,
+ null,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ null,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
+ public Payment run(final boolean isApiPayment,
+ final Boolean isSuccess,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ return run(paymentControlStateMachineHelper.getInitialState(),
+ isApiPayment,
+ isSuccess,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ transactionId,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
+ public Payment run(final State state,
+ final boolean isApiPayment,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- return run(paymentControlStateMachineHelper.getInitialState(), isApiPayment, transactionType, controlOperation, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
- amount, currency, properties, paymentControlPluginNames, callContext, internalCallContext);
+ return run(state,
+ isApiPayment,
+ null,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ null,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
}
- public Payment run(final State state, final boolean isApiPayment, final TransactionType transactionType, final ControlOperation controlOperation, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
+ public Payment run(final State state,
+ final boolean isApiPayment,
+ final Boolean isSuccess,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- final PaymentStateControlContext paymentStateContext = createContext(isApiPayment, transactionType, account, paymentMethodId,
- paymentId, paymentExternalKey,
+ final PaymentStateControlContext paymentStateContext = createContext(isApiPayment,
+ isSuccess,
+ transactionType,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ transactionId,
paymentTransactionExternalKey,
- amount, currency,
- properties, paymentControlPluginNames, callContext, internalCallContext);
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
try {
final OperationCallback callback = createOperationCallback(controlOperation, paymentStateContext);
final LeavingStateCallback leavingStateCallback = new DefaultControlInitiated(this, paymentStateContext, paymentDao, paymentControlStateMachineHelper.getInitialState(), paymentControlStateMachineHelper.getRetriedState(), transactionType);
@@ -171,11 +286,11 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
}
@VisibleForTesting
- PaymentStateControlContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
+ PaymentStateControlContext createContext(final boolean isApiPayment, final Boolean isSuccess, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId, @Nullable final String paymentExternalKey,@Nullable final UUID transactionId, final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> properties,
final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- return new PaymentStateControlContext(paymentControlPluginNames, isApiPayment, paymentId, paymentExternalKey, paymentTransactionExternalKey, transactionType, account,
+ return new PaymentStateControlContext(paymentControlPluginNames, isApiPayment, isSuccess, paymentId, paymentExternalKey, transactionId, paymentTransactionExternalKey, transactionType, account,
paymentMethodId, amount, currency, properties, internalCallContext, callContext);
}
@@ -207,6 +322,9 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
case CHARGEBACK_REVERSAL:
callback = new ChargebackReversalControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner);
break;
+ case NOTIFICATION_OF_STATE_CHANGE:
+ callback = new NotificationOfStateChangeControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner);
+ break;
default:
throw new IllegalStateException("Unsupported control operation " + controlOperation);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
index 01b15f1..258c365 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
@@ -19,6 +19,7 @@ package org.killbill.billing.payment.dispatcher;
import java.util.Map;
import java.util.Random;
+import java.util.UUID;
import java.util.concurrent.Callable;
import org.apache.shiro.mgt.SecurityManager;
@@ -44,7 +45,12 @@ public class CallableWithRequestData<T> implements Callable<T> {
final Subject subject,
final Map<String, String> mdcContextMap,
final Callable<T> delegate) {
- this.requestData = requestData;
+ if (requestData == null) {
+ // To make locks re-entrant (for the Janitor), we need a request id
+ this.requestData = new RequestData(UUID.randomUUID().toString());
+ } else {
+ this.requestData = requestData;
+ }
this.random = random;
this.securityManager = securityManager;
this.subject = subject;
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index b8bc30f..18404d2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -76,6 +76,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
@@ -176,16 +177,34 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
log.warn("processedCurrency='{}' of invoice paymentId='{}' doesn't match invoice currency='{}', assuming it is a full payment", paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
invoicePaymentAmount = paymentControlContext.getAmount();
}
- log.debug("Notifying invoice of successful paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
- invoiceApi.recordPaymentAttemptCompletion(invoiceId,
- invoicePaymentAmount,
- paymentControlContext.getCurrency(),
- paymentControlContext.getProcessedCurrency(),
- paymentControlContext.getPaymentId(),
- paymentControlContext.getTransactionExternalKey(),
- paymentControlContext.getCreatedDate(),
- true,
- internalContext);
+
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentControlContext.getTransactionId(), internalContext);
+ // If it's not SUCCESS, it is PENDING
+ final boolean success = paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS;
+ log.debug("Notifying invoice of {} paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", success ? "successful" : "pending", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
+
+ if (success) {
+ invoiceApi.recordPaymentAttemptCompletion(invoiceId,
+ invoicePaymentAmount,
+ paymentControlContext.getCurrency(),
+ paymentControlContext.getProcessedCurrency(),
+ paymentControlContext.getPaymentId(),
+ paymentControlContext.getTransactionExternalKey(),
+ paymentControlContext.getCreatedDate(),
+ success,
+ internalContext);
+ } else {
+ // For PENDING payments, we re-call recordPaymentAttemptInit to simply update the current
+ // entry in invoice_payments (update the payment id, processed amount, etc.)
+ invoiceApi.recordPaymentAttemptInit(invoiceId,
+ invoicePaymentAmount,
+ paymentControlContext.getCurrency(),
+ paymentControlContext.getProcessedCurrency(),
+ paymentControlContext.getPaymentId(),
+ paymentControlContext.getTransactionExternalKey(),
+ paymentControlContext.getCreatedDate(),
+ internalContext);
+ }
}
break;
@@ -355,7 +374,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
// but onSuccessCall callback never gets called (leaving the place for a double payment if user retries the operation)
//
invoiceApi.recordPaymentAttemptInit(invoice.getId(),
- BigDecimal.ZERO,
+ MoreObjects.firstNonNull(paymentControlPluginContext.getAmount(), BigDecimal.ZERO),
paymentControlPluginContext.getCurrency(),
paymentControlPluginContext.getCurrency(),
// Likely to be null, but we don't care as we use the transactionExternalKey
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
index 386ed00..24ae2a7 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
@@ -74,13 +74,13 @@ public class MockRetryablePaymentAutomatonRunner extends PluginControlPaymentAut
}
@Override
- PaymentStateControlContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
+ PaymentStateControlContext createContext(final boolean isApiPayment, final Boolean isSuccess, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, @Nullable final UUID transactionId, final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount, @Nullable final Currency currency,
final Iterable<PluginProperty> properties,
final List<String> pluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
if (context == null) {
- return super.createContext(isApiPayment, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
+ return super.createContext(isApiPayment, isSuccess, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, transactionId, paymentTransactionExternalKey,
amount, currency, properties, pluginNames, callContext, internalCallContext);
} else {
return context;
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 4f3b429..b868c18 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
@@ -177,7 +177,9 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
new PaymentStateControlContext(ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME),
true,
null,
+ null,
paymentExternalKey,
+ null,
paymentTransactionExternalKey,
TransactionType.AUTHORIZE,
account,
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 1f81aeb..b86f476 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
@@ -73,6 +73,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
private final AtomicBoolean makeNextPaymentFailWithCancellation = new AtomicBoolean(false);
private final AtomicBoolean makeNextPaymentFailWithException = new AtomicBoolean(false);
private final AtomicBoolean makeAllPaymentsFailWithError = new AtomicBoolean(false);
+ private final AtomicBoolean makeNextPaymentPending = new AtomicBoolean(false);
private final AtomicInteger makePluginWaitSomeMilliseconds = new AtomicInteger(0);
private final AtomicReference<BigDecimal> overrideNextProcessedAmount = new AtomicReference<BigDecimal>();
private final AtomicReference<Currency> overrideNextProcessedCurrency = new AtomicReference<Currency>();
@@ -200,6 +201,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeAllPaymentsFailWithError.set(false);
makeNextPaymentFailWithError.set(false);
makeNextPaymentFailWithCancellation.set(false);
+ makeNextPaymentPending.set(false);
makePluginWaitSomeMilliseconds.set(0);
overrideNextProcessedAmount.set(null);
paymentMethods.clear();
@@ -212,6 +214,10 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeNextPaymentFailWithError.set(true);
}
+ public void makeNextPaymentPending() {
+ makeNextPaymentPending.set(true);
+ }
+
public void makeNextPaymentFailWithCancellation() {
makeNextPaymentFailWithCancellation.set(true);
}
@@ -432,6 +438,8 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
status = PaymentPluginStatus.ERROR;
} else if (makeNextPaymentFailWithCancellation.getAndSet(false)) {
status = PaymentPluginStatus.CANCELED;
+ } else if (makeNextPaymentPending.getAndSet(false)) {
+ status = PaymentPluginStatus.PENDING;
} else {
status = PaymentPluginStatus.PROCESSED;
}