killbill-memoizeit
Changes
invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java 2(+1 -1)
invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java 35(+16 -19)
invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java 2(+1 -1)
Details
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
index fa36373..5f9d7b5 100644
--- a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
@@ -37,9 +37,9 @@ public interface InvoiceInternalApi {
public BigDecimal getAccountBalance(UUID accountId, InternalTenantContext context);
- public void notifyOfPayment(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, String transactionExternalKey, DateTime paymentDate, boolean success, InternalCallContext context) throws InvoiceApiException;
+ public void recordPaymentAttemptInit(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, String transactionExternalKey, DateTime paymentDate, InternalCallContext context) throws InvoiceApiException;
- public void notifyOfPayment(InvoicePayment invoicePayment, InternalCallContext context) throws InvoiceApiException;
+ public void recordPaymentAttemptCompletion(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, String transactionExternalKey, DateTime paymentDate, boolean success, InternalCallContext context) throws InvoiceApiException;
public InvoicePayment getInvoicePaymentForAttempt(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
@@ -59,10 +59,10 @@ public interface InvoiceInternalApi {
* @return the created invoice payment object associated with this refund
* @throws InvoiceApiException
*/
- public InvoicePayment createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
+ public InvoicePayment recordRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
String transactionExternalKey, InternalCallContext context) throws InvoiceApiException;
- public InvoicePayment createChargeback(UUID paymentId, BigDecimal amount, Currency currency, InternalCallContext context) throws InvoiceApiException;
+ public InvoicePayment recordChargeback(UUID paymentId, BigDecimal amount, Currency currency, InternalCallContext context) throws InvoiceApiException;
/**
* Rebalance CBA for account which have credit and unpaid invoices
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 e79eb43..2f714dd 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
@@ -18,6 +18,7 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -25,6 +26,7 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
@@ -41,8 +43,14 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
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.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
+import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.skife.jdbi.v2.tweak.VoidHandleCallback;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -388,7 +396,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
// Verify links for payment 1
Assert.assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("4.00")), 0);
Assert.assertNull(invoice1.getPayments().get(0).getLinkedInvoicePaymentId());
- Assert.assertNull(invoice1.getPayments().get(0).getPaymentCookieId());
+ Assert.assertEquals(invoice1.getPayments().get(0).getPaymentCookieId(), payment1.getTransactions().get(0).getExternalKey());
Assert.assertEquals(invoice1.getPayments().get(0).getPaymentId(), payment1.getId());
Assert.assertEquals(invoice1.getPayments().get(0).getType(), InvoicePaymentType.ATTEMPT);
Assert.assertTrue(invoice1.getPayments().get(0).isSuccess());
@@ -396,7 +404,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
// Verify links for payment 2
Assert.assertEquals(invoice1.getPayments().get(1).getAmount().compareTo(new BigDecimal("6.00")), 0);
Assert.assertNull(invoice1.getPayments().get(1).getLinkedInvoicePaymentId());
- Assert.assertNull(invoice1.getPayments().get(1).getPaymentCookieId());
+ Assert.assertEquals(invoice1.getPayments().get(1).getPaymentCookieId(), payment2.getTransactions().get(0).getExternalKey());
Assert.assertEquals(invoice1.getPayments().get(1).getPaymentId(), payment2.getId());
Assert.assertEquals(invoice1.getPayments().get(1).getType(), InvoicePaymentType.ATTEMPT);
Assert.assertTrue(invoice1.getPayments().get(1).isSuccess());
@@ -482,4 +490,79 @@ public class TestInvoicePayment extends TestIntegrationBase {
assertEquals(payments2.get(0).getTransactions().get(1).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
assertEquals(payments2.get(0).getTransactions().get(1).getProcessedCurrency(), Currency.USD);
}
+
+ @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));
+
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ 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 {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice2 = 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);
+
+ // Invoice is fully paid
+ final Payment originalPayment = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(originalPayment.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(originalPayment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(originalPayment.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+
+ final PaymentTransaction originalTransaction = originalPayment.getTransactions().get(0);
+
+ // Let 's hack invoice_payment table by hand to simulate a non completion of the payment (onSuccessCall was never called)
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ handle.execute("update invoice_payments set success = false where payment_cookie_id = ?", originalTransaction.getExternalKey());
+ return null;
+ }
+ });
+
+ final Invoice updateInvoice2 = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ // Invoice now shows as unpaid
+ Assert.assertEquals(updateInvoice2.getBalance().compareTo(originalPayment.getPurchasedAmount()), 0);
+ Assert.assertEquals(updateInvoice2.getPayments().size(), 1);
+ Assert.assertEquals(updateInvoice2.getPayments().get(0).getPaymentCookieId(), originalTransaction.getExternalKey());
+
+ //
+ // Now trigger invoice payment again (no new payment should be made as code should detect broken state and fix it by itself)
+ // We expect an INVOICE_PAYMENT that indicates the invoice was repaired, and also an exception because plugin aborts payment call since there is nothing to do.
+ //
+ busHandler.pushExpectedEvents(NextEvent.INVOICE_PAYMENT);
+ final List<PluginProperty> properties = new ArrayList<PluginProperty>();
+ final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, updateInvoice2.getId().toString(), false);
+ properties.add(prop1);
+ try {
+ paymentApi.createPurchaseWithPaymentControl(account, account.getPaymentMethodId(), null, updateInvoice2.getBalance(), updateInvoice2.getCurrency(), UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), properties, PAYMENT_OPTIONS, refreshedCallContext());
+ Assert.fail("The payment should not succeed (and yet it will repair the broken state....)");
+ } catch (final PaymentApiException expected) {
+ Assert.assertEquals(expected.getCode(), ErrorCode.PAYMENT_PLUGIN_EXCEPTION.getCode());
+ }
+ assertListenerStatus();
+
+ final Invoice updateInvoice3 = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ Assert.assertEquals(updateInvoice3.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(updateInvoice3.getPayments().size(), 1);
+ Assert.assertEquals(updateInvoice3.getPayments().get(0).getPaymentCookieId(), originalTransaction.getExternalKey());
+ Assert.assertTrue(updateInvoice3.getPayments().get(0).isSuccess());
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(payments.size(), 1);
+ Assert.assertEquals(payments.get(0).getTransactions().size(), 1);
+
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
index fc0cbb8..811c11c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
@@ -46,7 +46,7 @@ public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
@Override
public List<InvoicePayment> getInvoicePayments(final UUID paymentId, final TenantContext context) {
- return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getInvoicePayments(paymentId, internalCallContextFactory.createInternalTenantContext(paymentId, ObjectType.PAYMENT, context)),
+ return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getInvoicePaymentsByPaymentId(paymentId, internalCallContextFactory.createInternalTenantContext(paymentId, ObjectType.PAYMENT, context)),
new Function<InvoicePaymentModelDao, InvoicePayment>() {
@Override
public InvoicePayment apply(final InvoicePaymentModelDao input) {
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 4575749..6cd672a 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
@@ -94,14 +94,16 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
}
@Override
- public void notifyOfPayment(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);
- notifyOfPayment(invoicePayment, context);
+ public void recordPaymentAttemptInit(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final String transactionExternalKey, final DateTime paymentDate, final InternalCallContext context) throws InvoiceApiException {
+ final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, transactionExternalKey, false);
+ dao.notifyOfPaymentInit(new InvoicePaymentModelDao(invoicePayment), context);
}
+
@Override
- public void notifyOfPayment(final InvoicePayment invoicePayment, final InternalCallContext context) throws InvoiceApiException {
- dao.notifyOfPayment(new InvoicePaymentModelDao(invoicePayment), context);
+ 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);
+ dao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(invoicePayment), context);
}
@Override
@@ -121,7 +123,7 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
}
@Override
- public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final String transactionExternalKey, final InternalCallContext context) throws InvoiceApiException {
+ public InvoicePayment recordRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final String transactionExternalKey, final InternalCallContext context) throws InvoiceApiException {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new InvoiceApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL, paymentId, amount);
}
@@ -144,7 +146,7 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
}
@Override
- public InvoicePayment createChargeback(final UUID paymentId, final BigDecimal amount, final Currency currency, final InternalCallContext context) throws InvoiceApiException {
+ public InvoicePayment recordChargeback(final UUID paymentId, final BigDecimal amount, final Currency currency, final InternalCallContext context) throws InvoiceApiException {
return new DefaultInvoicePayment(dao.postChargeback(paymentId, amount, currency, context));
}
@@ -160,20 +162,15 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
}
private InvoicePayment getInvoicePayment(final UUID paymentId, final InvoicePaymentType type, final InternalTenantContext context) throws InvoiceApiException {
- final Collection<InvoicePayment> invoicePayments = Collections2.transform(dao.getInvoicePayments(paymentId, context), new Function<InvoicePaymentModelDao, InvoicePayment>() {
- @Override
- public InvoicePayment apply(final InvoicePaymentModelDao input) {
- return new DefaultInvoicePayment(input);
- }
- });
- if (invoicePayments.isEmpty()) {
- return null;
- }
- return Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
+
+ final List<InvoicePaymentModelDao> invoicePayments = dao.getInvoicePaymentsByPaymentId(paymentId, context);
+ final InvoicePaymentModelDao resultOrNull = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
@Override
- public boolean apply(final InvoicePayment input) {
- return input.getType() == type;
+ public boolean apply(final InvoicePaymentModelDao input) {
+ return input.getType() == type &&
+ input.getSuccess();
}
}).orNull();
+ return resultOrNull != null ? new DefaultInvoicePayment(resultOrNull) : null;
}
}
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 dbf1ec2..bac4b81 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
@@ -411,7 +411,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
@Override
- public List<InvoicePaymentModelDao> getInvoicePayments(final UUID paymentId, final InternalTenantContext context) {
+ public List<InvoicePaymentModelDao> getInvoicePaymentsByPaymentId(final UUID paymentId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoicePaymentModelDao>>() {
@Override
public List<InvoicePaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
@@ -672,32 +672,61 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
});
}
+
+ @Override
+ public void notifyOfPaymentInit(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+ notifyOfPaymentCompletionInternal(invoicePayment, false, context);
+ }
+
+
@Override
- public void notifyOfPayment(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+ 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) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
final InvoicePaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
-
- // If the payment id is null, the payment wasn't attempted (e.g. no payment method). We don't record an attempt but send an event nonetheless (e.g. for Overdue)
- if (invoicePayment.getPaymentId() != null) {
- final List<InvoicePaymentModelDao> invoicePayments = transactional.getInvoicePayments(invoicePayment.getPaymentId().toString(), context);
+ //
+ // In case of notifyOfPaymentInit we always want to record the row with success = false
+ // Otherwise, if the payment id is null, the payment wasn't attempted (e.g. no payment method so we don't record an attempt but send
+ // an event nonetheless (e.g. for Overdue)
+ //
+ if (!completion || invoicePayment.getPaymentId() != null) {
+ //
+ // extract entries by invoiceId (which is always set, as opposed to paymentId) and then filter based on type and
+ // paymentCookieId = transactionExternalKey
+ //
+ final List<InvoicePaymentModelDao> invoicePayments = transactional.getPaymentsForInvoice(invoicePayment.getInvoiceId().toString(), context);
final InvoicePaymentModelDao existingAttempt = Iterables.tryFind(invoicePayments, new Predicate<InvoicePaymentModelDao>() {
@Override
public boolean apply(final InvoicePaymentModelDao input) {
- return input.getType() == InvoicePaymentType.ATTEMPT;
+ return input.getType() == InvoicePaymentType.ATTEMPT &&
+ input.getPaymentCookieId().equals(invoicePayment.getPaymentCookieId());
}
}).orNull();
+
if (existingAttempt == null) {
transactional.create(invoicePayment, context);
} else if (!existingAttempt.getSuccess() && invoicePayment.getSuccess()) {
- transactional.updateAttempt(existingAttempt.getRecordId(), invoicePayment.getPaymentDate().toDate(), invoicePayment.getAmount(), invoicePayment.getCurrency(), invoicePayment.getProcessedCurrency(), context);
+ transactional.updateAttempt(existingAttempt.getRecordId(),
+ invoicePayment.getPaymentId().toString(),
+ invoicePayment.getPaymentDate().toDate(),
+ invoicePayment.getAmount(),
+ invoicePayment.getCurrency(),
+ invoicePayment.getProcessedCurrency(),
+ invoicePayment.getPaymentCookieId(),
+ null,
+ context);
}
}
- final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), entitySqlDaoWrapperFactory.getHandle());
- notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, invoicePayment, accountId, context.getUserToken(), context);
-
+ if (completion) {
+ final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), entitySqlDaoWrapperFactory.getHandle());
+ notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, invoicePayment, accountId, context.getUserToken(), context);
+ }
return null;
}
});
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
index effce52..3ee47be 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
@@ -32,6 +32,7 @@ import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.EntityDao;
@@ -58,7 +59,7 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
UUID getInvoiceIdByPaymentId(UUID paymentId, InternalTenantContext context);
- List<InvoicePaymentModelDao> getInvoicePayments(UUID paymentId, InternalTenantContext context);
+ List<InvoicePaymentModelDao> getInvoicePaymentsByPaymentId(UUID paymentId, InternalTenantContext context);
List<InvoicePaymentModelDao> getInvoicePaymentsByAccount(InternalTenantContext context);
@@ -131,7 +132,9 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
*/
void deleteCBA(UUID accountId, UUID invoiceId, UUID invoiceItemId, InternalCallContext context) throws InvoiceApiException;
- void notifyOfPayment(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
+ void notifyOfPaymentInit(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
+
+ void notifyOfPaymentCompletion(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
/**
* @param accountId the account for which we need to rebalance the CBA
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
index 279aea3..7b8c5ad 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
@@ -69,12 +69,14 @@ public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePaymentModelDa
@BindBean final InternalTenantContext context);
-
@SqlUpdate
void updateAttempt(@Bind("recordId") Long recordId,
+ @Bind("paymentId") final String paymentId,
@Bind("paymentDate") final Date paymentDate,
@Bind("amount") final BigDecimal amount,
@Bind("currency") final Currency currency,
@Bind("processedCurrency") final Currency processedCurrency,
+ @Bind("paymentCookieId") final String paymentCookieId,
+ @Bind("linkedInvoicePaymentId") final String linkedInvoicePaymentId,
@BindBean final InternalTenantContext context);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
index 102dc1f..8402c5b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
@@ -45,7 +45,7 @@ public class DefaultInvoicePayment extends EntityBase implements InvoicePayment
public DefaultInvoicePayment(final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
final BigDecimal amount, final Currency currency, final Currency processedCurrency, final String paymentCookieId, final Boolean isSuccess) {
- this(UUIDs.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, null, null, isSuccess);
+ this(UUIDs.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, paymentCookieId, null, isSuccess);
}
public DefaultInvoicePayment(final UUID id, final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
index b9e4dab..9925595 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
@@ -109,10 +109,14 @@ getChargebacksByPaymentId() ::= <<
updateAttempt() ::= <<
UPDATE <tableName()>
- SET success = true,
+ SET payment_id := :paymentId,
payment_date = :paymentDate,
amount = :amount,
- processed_currency = :processedCurrency
+ currency = :currency,
+ processed_currency = :processedCurrency,
+ payment_cookie_id = :paymentCookieId,
+ linked_invoice_payment_id := :linkedInvoicePaymentId,
+ success = true
WHERE record_id = :recordId
<AND_CHECK_TENANT("")>
;
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
index 0af1018..8b7bbfb 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
@@ -114,7 +114,7 @@ public class TestDefaultInvoicePaymentApi extends InvoiceTestSuiteWithEmbeddedDB
Assert.assertEquals(initialInvoiceBalance.compareTo(BigDecimal.ZERO), 0);
// Create a full refund with no adjustment
- final InvoicePayment refund = invoiceInternalApi.createRefund(payment.getPaymentId(), refundAmount, adjusted, invoiceItemIdsWithAmounts,
+ final InvoicePayment refund = invoiceInternalApi.recordRefund(payment.getPaymentId(), refundAmount, adjusted, invoiceItemIdsWithAmounts,
UUID.randomUUID().toString(), internalCallContext);
Assert.assertEquals(refund.getAmount().compareTo(refundAmount.negate()), 0);
Assert.assertEquals(refund.getCurrency(), CURRENCY);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
index 6c1c27e..2164386 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
@@ -27,7 +27,6 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
-import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
@@ -213,7 +212,7 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice,
}
@Override
- public List<InvoicePaymentModelDao> getInvoicePayments(final UUID paymentId, final InternalTenantContext context) {
+ public List<InvoicePaymentModelDao> getInvoicePaymentsByPaymentId(final UUID paymentId, final InternalTenantContext context) {
final List<InvoicePaymentModelDao> result = new LinkedList<InvoicePaymentModelDao>();
synchronized (monitor) {
for (final InvoicePaymentModelDao payment : payments.values()) {
@@ -243,7 +242,7 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice,
}
@Override
- public void notifyOfPayment(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+ public void notifyOfPaymentCompletion(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
synchronized (monitor) {
payments.put(invoicePayment.getId(), invoicePayment);
}
@@ -362,4 +361,11 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice,
public void deleteCBA(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final InternalCallContext context) throws InvoiceApiException {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public void notifyOfPaymentInit(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+ synchronized (monitor) {
+ payments.put(invoicePayment.getId(), invoicePayment);
+ }
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index 29629bd..6bbfbe3 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -32,7 +32,6 @@ import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
-import org.killbill.billing.account.api.DefaultAccount;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.catalog.DefaultPrice;
import org.killbill.billing.catalog.MockInternationalPrice;
@@ -151,8 +150,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final BigDecimal paymentAmount = new BigDecimal("11.00");
final UUID paymentId = UUID.randomUUID();
- final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow().plusDays(12), paymentAmount, Currency.USD, Currency.USD, null, true);
- invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+ final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow().plusDays(12), paymentAmount, Currency.USD, Currency.USD, "cookie", true);
+ invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
final InvoiceModelDao retrievedInvoice = invoiceDao.getById(invoiceId, context);
assertNotNull(retrievedInvoice);
@@ -1324,8 +1323,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
// SECOND CREATE THE PAYMENT
final BigDecimal paymentAmount = new BigDecimal("239.00");
final UUID paymentId = UUID.randomUUID();
- final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow(), paymentAmount, Currency.USD, Currency.USD, null, true);
- invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+ final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, clock.getUTCNow(), paymentAmount, Currency.USD, Currency.USD, "cookie", true);
+ invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
// AND THEN THIRD THE REFUND
final Map<UUID, BigDecimal> invoiceItemMap = new HashMap<UUID, BigDecimal>();
@@ -1474,9 +1473,9 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final UUID paymentId = UUID.randomUUID();
final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), clock.getUTCNow().plusDays(12), new BigDecimal("10.0"),
- Currency.USD, Currency.USD, null, true);
+ Currency.USD, Currency.USD, "cookie", true);
- invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+ invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
// Create invoice 2
// Scenario: single item
@@ -1535,8 +1534,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final UUID paymentId = UUID.randomUUID();
final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice1.getId(), clock.getUTCNow().plusDays(12), paymentAmount,
- Currency.USD, Currency.USD, null, true);
- invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+ Currency.USD, Currency.USD, "cookie", true);
+ invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
// Create invoice 2
// Scenario: single item
@@ -1636,15 +1635,15 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final UUID paymentId = UUID.randomUUID();
- final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, null, false);
- invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment), context);
+ final DefaultInvoicePayment defaultInvoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, "cookie", false);
+ invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment), context);
final InvoiceModelDao retrievedInvoice1 = invoiceDao.getById(invoice.getId(), context);
assertEquals(retrievedInvoice1.getInvoicePayments().size(), 1);
assertEquals(retrievedInvoice1.getInvoicePayments().get(0).getSuccess(), Boolean.FALSE);
- final DefaultInvoicePayment defaultInvoicePayment2 = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, null, true);
- invoiceDao.notifyOfPayment(new InvoicePaymentModelDao(defaultInvoicePayment2), context);
+ final DefaultInvoicePayment defaultInvoicePayment2 = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoice.getId(), clock.getUTCNow().plusDays(12), BigDecimal.TEN, Currency.USD, Currency.USD, "cookie", true);
+ invoiceDao.notifyOfPaymentCompletion(new InvoicePaymentModelDao(defaultInvoicePayment2), context);
final InvoiceModelDao retrievedInvoice2 = invoiceDao.getById(invoice.getId(), context);
assertEquals(retrievedInvoice2.getInvoicePayments().size(), 1);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
index 9f9a536..f64b7ea 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
@@ -135,7 +135,8 @@ public class InvoiceTestUtils {
Mockito.when(payment.getProcessedCurrency()).thenReturn(currency);
Mockito.when(payment.isSuccess()).thenReturn(true);
- invoicePaymentApi.notifyOfPayment(payment, callContext);
+ invoicePaymentApi.recordPaymentAttemptCompletion(payment.getInvoiceId(), payment.getAmount(), payment.getCurrency(), payment.getProcessedCurrency(), payment.getPaymentId(), payment.getPaymentCookieId(),
+ payment.getPaymentDate(), payment.isSuccess(), callContext);
return payment;
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java
index 9ee85d0..a7b2c9a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoicePaymentResource.java
@@ -125,7 +125,7 @@ public class InvoicePaymentResource extends JaxRsResourceBase {
final InvoicePayment invoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
@Override
public boolean apply(final InvoicePayment input) {
- return input.getType() == InvoicePaymentType.ATTEMPT;
+ return input.getType() == InvoicePaymentType.ATTEMPT && input.isSuccess();
}
}).orNull();
final UUID invoiceId = invoicePayment != null ? invoicePayment.getInvoiceId() : null;
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
index 845bfd0..dcb2738 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceResource.java
@@ -611,9 +611,11 @@ public class InvoiceResource extends JaxRsResourceBase {
(payment.getPaymentMethodId() != null ? UUID.fromString(payment.getPaymentMethodId()) : account.getPaymentMethodId());
final UUID invoiceId = UUID.fromString(payment.getTargetInvoiceId());
+
final Payment result = createPurchaseForInvoice(account, invoiceId, payment.getPurchasedAmount(), paymentMethodId, externalPayment, pluginProperties, callContext);
- // STEPH should that live in InvoicePayment instead?
- return uriBuilder.buildResponse(uriInfo, InvoicePaymentResource.class, "getInvoicePayment", result.getId());
+ return result != null ?
+ uriBuilder.buildResponse(uriInfo, InvoicePaymentResource.class, "getInvoicePayment", result.getId()) :
+ Response.status(Status.NO_CONTENT).build();
}
@TimedResource
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 3958b72..ca71075 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -450,9 +450,16 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
final PluginProperty invoiceProperty = new PluginProperty("IPCD_INVOICE_ID" /* InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID (contract with plugin) */,
invoiceId.toString(), false);
properties.add(invoiceProperty);
-
- return paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, null, amountToPay, account.getCurrency(), paymentExternalKey, transactionExternalKey,
- properties, createInvoicePaymentControlPluginApiPaymentOptions(externalPayment), callContext);
+ try {
+ return paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, null, amountToPay, account.getCurrency(), paymentExternalKey, transactionExternalKey,
+ properties, createInvoicePaymentControlPluginApiPaymentOptions(externalPayment), callContext);
+ } catch (final PaymentApiException e) {
+ if (e.getCode() == ErrorCode.PAYMENT_PLUGIN_EXCEPTION.getCode() &&
+ e.getMessage().contains("Aborted Payment for invoice")) {
+ return null;
+ }
+ throw e;
+ }
}
protected PaymentOptions createInvoicePaymentControlPluginApiPaymentOptions(final boolean isExternalPayment) {
@@ -496,7 +503,7 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
final InvoicePayment invoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
@Override
public boolean apply(final InvoicePayment input) {
- return input.getPaymentId().equals(payment.getId()) && input.getType() == InvoicePaymentType.ATTEMPT;
+ return input.isSuccess() && input.getPaymentId().equals(payment.getId()) && input.getType() == InvoicePaymentType.ATTEMPT;
}
}).orNull();
return invoicePayment != null ? invoicePayment.getInvoiceId() : null;
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 42bd2be..412a211 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
@@ -46,6 +46,7 @@ import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
@@ -164,15 +165,15 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
invoicePaymentAmount = paymentControlContext.getAmount();
}
log.debug("Notifying invoice of successful payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
- invoiceApi.notifyOfPayment(invoiceId,
- invoicePaymentAmount,
- paymentControlContext.getCurrency(),
- paymentControlContext.getProcessedCurrency(),
- paymentControlContext.getPaymentId(),
- paymentControlContext.getTransactionExternalKey(),
- paymentControlContext.getCreatedDate(),
- true,
- internalContext);
+ invoiceApi.recordPaymentAttemptCompletion(invoiceId,
+ invoicePaymentAmount,
+ paymentControlContext.getCurrency(),
+ paymentControlContext.getProcessedCurrency(),
+ paymentControlContext.getPaymentId(),
+ paymentControlContext.getTransactionExternalKey(),
+ paymentControlContext.getCreatedDate(),
+ true,
+ internalContext);
}
break;
@@ -180,7 +181,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
- invoiceApi.createRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
+ invoiceApi.recordRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
break;
case CHARGEBACK:
@@ -204,7 +205,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
currency = linkedInvoicePayment.getCurrency();
}
- invoiceApi.createChargeback(paymentControlContext.getPaymentId(), amount, currency, internalContext);
+ invoiceApi.recordChargeback(paymentControlContext.getPaymentId(), amount, currency, internalContext);
}
break;
@@ -229,16 +230,16 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
final UUID invoiceId = getInvoiceId(pluginProperties);
try {
log.debug("Notifying invoice of failed payment: id={}, amount={}, currency={}, invoiceId={}", paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), paymentControlContext.getCurrency(), invoiceId);
- invoiceApi.notifyOfPayment(invoiceId,
- BigDecimal.ZERO,
- paymentControlContext.getCurrency(),
- // processed currency may be null so we use currency; processed currency will be updated if/when payment succeeds
- paymentControlContext.getCurrency(),
- paymentControlContext.getPaymentId(),
- paymentControlContext.getTransactionExternalKey(),
- paymentControlContext.getCreatedDate(),
- false,
- internalContext);
+ invoiceApi.recordPaymentAttemptCompletion(invoiceId,
+ BigDecimal.ZERO,
+ paymentControlContext.getCurrency(),
+ // processed currency may be null so we use currency; processed currency will be updated if/when payment succeeds
+ paymentControlContext.getCurrency(),
+ paymentControlContext.getPaymentId(),
+ paymentControlContext.getTransactionExternalKey(),
+ paymentControlContext.getCreatedDate(),
+ false,
+ internalContext);
} catch (final InvoiceApiException e) {
log.error("InvoicePaymentControlPluginApi onFailureCall failed ton update invoice for attemptId = " + paymentControlContext.getAttemptPaymentId() + ", transactionType = " + transactionType, e);
}
@@ -277,7 +278,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
private PriorPaymentControlResult getPluginPurchaseResult(final PaymentControlContext paymentControlPluginContext, final Iterable<PluginProperty> pluginProperties, final InternalCallContext internalContext) throws PaymentControlApiException {
try {
final UUID invoiceId = getInvoiceId(pluginProperties);
- final Invoice invoice = rebalanceAndGetInvoice(invoiceId, internalContext);
+ final Invoice invoice = getAndSanitizeInvoice(invoiceId, internalContext);
final BigDecimal requestedAmount = validateAndComputePaymentAmount(invoice, paymentControlPluginContext.getAmount(), paymentControlPluginContext.isApiPayment());
final boolean isAborted = requestedAmount.compareTo(BigDecimal.ZERO) == 0;
@@ -287,11 +288,27 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
if (paymentControlPluginContext.isApiPayment() && isAborted) {
throw new PaymentControlApiException("Abort purchase call: ", new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION,
- String.format("Payment for invoice %s aborted : invoice balance is = %s, requested payment amount is = %s",
+ String.format("Aborted Payment for invoice %s : invoice balance is = %s, requested payment amount is = %s",
invoice.getId(),
invoice.getBalance(),
paymentControlPluginContext.getAmount())));
} else {
+
+ //
+ // Insert attempt row with a success = false status to implement a two-phase commit strategy and guard against scenario where payment would go through
+ // but onSuccessCall callback never gets called (leaving the place for a double payment if user retries the operation)
+ //
+ invoiceApi.recordPaymentAttemptInit(invoice.getId(),
+ BigDecimal.ZERO,
+ paymentControlPluginContext.getCurrency(),
+ paymentControlPluginContext.getCurrency(),
+ // Likely to be null, but we don't care as we use the transactionExternalKey
+ // to match the operation in the checkForIncompleteInvoicePaymentAndRepair logic below
+ paymentControlPluginContext.getPaymentId(),
+ paymentControlPluginContext.getTransactionExternalKey(),
+ paymentControlPluginContext.getCreatedDate(),
+ internalContext);
+
return new DefaultPriorPaymentControlResult(isAborted, requestedAmount);
}
} catch (final InvoiceApiException e) {
@@ -495,11 +512,64 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
}));
}
- private Invoice rebalanceAndGetInvoice(final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {
+ private Invoice getAndSanitizeInvoice(final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {
final Invoice invoicePriorRebalancing = invoiceApi.getInvoiceById(invoiceId, context);
invoiceApi.consumeExistingCBAOnAccountWithUnpaidInvoices(invoicePriorRebalancing.getAccountId(), context);
final Invoice invoice = invoiceApi.getInvoiceById(invoiceId, context);
- return invoice;
+
+ if (checkForIncompleteInvoicePaymentAndRepair(invoice, context)) {
+ // Fetch new repaired 'invoice'
+ return invoiceApi.getInvoiceById(invoiceId, context);
+ } else {
+ return invoice;
+ }
+ }
+
+ private boolean checkForIncompleteInvoicePaymentAndRepair(final Invoice invoice, final InternalCallContext internalContext) throws InvoiceApiException {
+
+ final List<InvoicePayment> invoicePayments = invoice.getPayments();
+
+ // Look for ATTEMPT matching that invoiceId that are not successful and extract matching paymentTransaction
+ final InvoicePayment incompleteInvoicePayment = Iterables.tryFind(invoicePayments, new Predicate<InvoicePayment>() {
+ @Override
+ public boolean apply(final InvoicePayment input) {
+ return input.getType() == InvoicePaymentType.ATTEMPT && !input.isSuccess();
+ }
+ }).orNull();
+
+ // If such (incomplete) paymentTransaction exists, verify the state of the payment transaction
+ if (incompleteInvoicePayment != null) {
+ final String transactionExternalKey = incompleteInvoicePayment.getPaymentCookieId();
+ final List<PaymentTransactionModelDao> transactions = paymentDao.getPaymentTransactionsByExternalKey(transactionExternalKey, internalContext);
+ final PaymentTransactionModelDao successfulTransaction = Iterables.tryFind(transactions, new Predicate<PaymentTransactionModelDao>() {
+ @Override
+ public boolean apply(final PaymentTransactionModelDao input) {
+ //
+ // In reality this is more tricky because the matching transaction could be an UNKNOWN or PENDING (unsupported by the plugin) state
+ // In case of UNKNOWN, we don't know what to do: fixing it could result in not paying, and not fixing it could result in double payment
+ // Current code ignores it, which means we might end up in doing a double payment in that very edgy scenario, and customer would have to request a refund.
+ //
+ return input.getTransactionStatus() == TransactionStatus.SUCCESS;
+ }
+ }).orNull();
+
+ if (successfulTransaction != null) {
+ log.info(String.format("Detected an incomplete invoicePayment row for invoiceId='%s' and transactionExternalKey='%s', will correct status", invoice.getId(), successfulTransaction.getTransactionExternalKey()));
+
+ invoiceApi.recordPaymentAttemptCompletion(invoice.getId(),
+ successfulTransaction.getAmount(),
+ successfulTransaction.getCurrency(),
+ successfulTransaction.getProcessedCurrency(),
+ successfulTransaction.getPaymentId(),
+ successfulTransaction.getTransactionExternalKey(),
+ successfulTransaction.getCreatedDate(),
+ true,
+ internalContext);
+ return true;
+
+ }
+ }
+ return false;
}
private BigDecimal validateAndComputePaymentAmount(final Invoice invoice, @Nullable final BigDecimal inputAmount, final boolean isApiPayment) {
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index f193612..712ce3b 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -178,7 +178,7 @@ public class TestInvoice extends TestJaxrsBase {
" <tr>\n" +
" <td colspan=2 />\n" +
" <td align=right><strong>invoiceAmountPaid</strong></td>\n" +
- " <td align=right><strong>0</strong></td>\n" +
+ " <td align=right><strong>0.00</strong></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td colspan=2 />\n" +