Skip to content

Commit

Permalink
Add support for cards stored outside the Vault
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre-Alexandre Meyer <pierre@mouraf.org>
  • Loading branch information
pierre committed Sep 16, 2019
1 parent 251bc56 commit 29ee43f
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 19 deletions.
8 changes: 5 additions & 3 deletions README.md
Expand Up @@ -37,14 +37,16 @@ The following properties are optional:
Tokenization
------------

To avoid sending the full PAN to Kill Bill, your front-end application should tokenize first the card in Qualpay using the [Add a Customer](https://www.qualpay.com/developer/api/customer-vault/add-a-customer) Vault API.
To avoid sending the full PAN to Kill Bill, your front-end application should tokenize first the card in Qualpay using either the [Add a Customer](https://www.qualpay.com/developer/api/customer-vault/add-a-customer) Vault API (recommended) or [Tokenize Card](https://www.qualpay.com/developer/api/payment-gateway/tokenize-card) API. The Vault API is recommended as some functionality (such as retrieving the payment method details from Qualpay or deleting the card in Qualpay) won't be available when using the Gateway API.

Qualpay will return a customer id that needs to be set as a the custom field `QUALPAY_CUSTOMER_ID` on the Kill Bill account. You can then trigger a [refresh](https://killbill.github.io/slate/#account-refresh-account-payment-methods) to sync back all card in the Vault to Kill Bill.
When using the Vault API, Qualpay will return a customer id that needs to be set as a the custom field `QUALPAY_CUSTOMER_ID` on the Kill Bill account. You can then trigger a [refresh](https://killbill.github.io/slate/#account-refresh-account-payment-methods) to sync back all cards in the Vault to Kill Bill.

When using the Payment Gateway API, you need to [add the payment method](https://killbill.github.io/slate/#account-add-a-payment-method) directly by passing the card id as the `card_id` plugin property.

Migration
---------

When migrating to Kill Bill, you need to create one Kill Bill account for each of your customers and set the `QUALPAY_CUSTOMER_ID` custom field. Similar to the tokenization step above, you must then trigger a refresh of the payment methods for each account.
To migrate to Kill Bill, you first need to create one Kill Bill account for each of your customers and follow the tokenization step(s) above for each account.

Development
-----------
Expand Down
Expand Up @@ -130,15 +130,20 @@ public void addPaymentMethod(final UUID kbAccountId,
final Iterable<PluginProperty> properties,
final CallContext context) throws PaymentPluginApiException {
final String qualpayCustomerIdMaybeNull = getCustomerIdNoException(kbAccountId, context);
final String cardIdMaybeNull = PluginProperties.findPluginPropertyValue("card_id", properties);

final String qualpayId;
if (qualpayCustomerIdMaybeNull != null && paymentMethodProps.getExternalPaymentMethodId() != null) {
// The customer and payment method already exist (sync code path), we just need to update our tables
qualpayId = paymentMethodProps.getExternalPaymentMethodId();
} else if (qualpayCustomerIdMaybeNull == null && paymentMethodProps.getExternalPaymentMethodId() != null) {
// Invalid sync path
throw new PaymentPluginApiException("USER", "Specified Qualpay card id but missing QUALPAY_CUSTOMER_ID custom field");
} else if (cardIdMaybeNull != null) {
// Card was tokenized via the Payment Gateway API - we will simply store the card locally
qualpayId = cardIdMaybeNull;
} else {
// We need to create a new payment method, either on a new customer or on an existing one
// We need to create a new payment method, either on a new customer or on an existing one (for testing or for companies with a tokenization proxy)
final ApiClient apiClient = buildApiClient(context, true);
final CustomerVaultApi customerVaultApi = new CustomerVaultApi(apiClient);

Expand Down Expand Up @@ -441,7 +446,7 @@ public GatewayNotification processNotification(final String notification, final

private abstract static class TransactionExecutor<T> {

public T execute(final Account account, final QualpayPaymentMethodsRecord paymentMethodsRecord) throws ApiException {
public T execute(final Account account, final QualpayPaymentMethodsRecord paymentMethodsRecord) throws ApiException, SQLException {
throw new UnsupportedOperationException();

}
Expand All @@ -460,26 +465,20 @@ private PaymentTransactionInfoPlugin executeInitialTransaction(final Transaction
final Currency currency,
final Iterable<PluginProperty> properties,
final CallContext context) throws PaymentPluginApiException {
final String customerId = getCustomerIdNoException(kbAccountId, context);
return executeInitialTransaction(transactionType,
new TransactionExecutor<GatewayResponse>() {
@Override
public GatewayResponse execute(final Account account, final QualpayPaymentMethodsRecord paymentMethodsRecord) throws ApiException {
public GatewayResponse execute(final Account account, final QualpayPaymentMethodsRecord paymentMethodsRecord) throws ApiException, SQLException {
final ApiClient apiClient = buildApiClient(context, false);
final PGApi pgApi = new PGApi(apiClient);

final PGApiTransactionRequest pgApiTransactionRequest = new PGApiTransactionRequest();
pgApiTransactionRequest.setMerchantId(getMerchantId(context));
pgApiTransactionRequest.setAmtTran(amount.doubleValue());
pgApiTransactionRequest.setTranCurrency(CurrencyUnit.of(currency.toString()).getNumeric3Code());
if (customerId != null) {
pgApiTransactionRequest.setCustomerId(customerId);
} else {
// TODO Implement non-Vault path
pgApiTransactionRequest.setTokenize(true);
pgApiTransactionRequest.setCardId(null);
pgApiTransactionRequest.setAvsZip(null);
}

final QualpayPaymentMethodsRecord paymentMethod = dao.getPaymentMethod(kbPaymentMethodId, context.getTenantId());
pgApiTransactionRequest.setCardId(paymentMethod.getQualpayId());

final List<PGApiLineItem> lineItems = new ArrayList<PGApiLineItem>(1);
final PGApiLineItem lineItem = new PGApiLineItem();
Expand Down Expand Up @@ -535,6 +534,8 @@ private PaymentTransactionInfoPlugin executeInitialTransaction(final Transaction
response = transactionExecutor.execute(account, nonNullPaymentMethodsRecord);
} catch (final ApiException e) {
throw new PaymentPluginApiException("Error connecting to Qualpay", e);
} catch (final SQLException e) {
throw new PaymentPluginApiException("Unable to submit payment, we encountered a database error", e);
}
}

Expand Down Expand Up @@ -590,7 +591,8 @@ private PaymentTransactionInfoPlugin executeFollowUpTransaction(final Transactio
}
}

private Long getMerchantId(final TenantContext context) {
@VisibleForTesting
Long getMerchantId(final TenantContext context) {
final QualpayConfigProperties qualpayConfigProperties = qualpayConfigPropertiesConfigurationHandler.getConfigurable(context.getTenantId());
return Long.valueOf(MoreObjects.firstNonNull(qualpayConfigProperties.getMerchantId(), "0"));
}
Expand Down
Expand Up @@ -24,6 +24,7 @@
import java.util.Map;

import com.google.gson.reflect.TypeToken;
import io.swagger.client.model.AddBillingCardRequest;
import io.swagger.client.model.GatewayResponse;
import qpPlatform.ApiClient;
import qpPlatform.ApiException;
Expand All @@ -40,6 +41,10 @@ public PGApi(final ApiClient apiClient) {
this.apiClient.setBasePath(apiClient.getBasePath().replace("/platform", ""));
}

public GatewayResponse tokenize(final AddBillingCardRequest body) throws ApiException {
return createPGTransactionWithHttpInfo("/pg/tokenize", body);
}

public GatewayResponse authorize(final PGApiTransactionRequest body) throws ApiException {
return createPGTransactionWithHttpInfo("/pg/auth", body);
}
Expand Down
Expand Up @@ -36,6 +36,7 @@
import org.killbill.billing.plugin.api.PluginProperties;
import org.killbill.billing.plugin.api.core.PluginCustomField;
import org.killbill.billing.plugin.api.payment.PluginPaymentMethodPlugin;
import org.killbill.billing.plugin.qualpay.client.PGApi;
import org.killbill.billing.util.api.CustomFieldApiException;
import org.killbill.billing.util.customfield.CustomField;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -179,7 +180,7 @@ public void testSuccessfulAuthCapture() throws PaymentPluginApiException, ApiExc
}

@Test(groups = "slow")
public void testSuccessfulAuthVoid() throws PaymentPluginApiException, ApiException, PaymentApiException {
public void testSuccessfulAuthVoid() throws PaymentPluginApiException, PaymentApiException {
final UUID kbPaymentMethodId = createQualpayCustomerWithCreditCardAndReturnKBPaymentMethodId();

final Payment payment = TestUtils.buildPayment(account.getId(), account.getPaymentMethodId(), account.getCurrency(), killbillApi);
Expand All @@ -206,7 +207,7 @@ public void testSuccessfulAuthVoid() throws PaymentPluginApiException, ApiExcept
}

@Test(groups = "slow")
public void testSuccessfulPurchaseRefund() throws PaymentPluginApiException, ApiException, PaymentApiException {
public void testSuccessfulPurchaseRefund() throws PaymentPluginApiException, PaymentApiException {
final UUID kbPaymentMethodId = createQualpayCustomerWithCreditCardAndReturnKBPaymentMethodId();

final Payment payment = TestUtils.buildPayment(account.getId(), account.getPaymentMethodId(), account.getCurrency(), killbillApi);
Expand Down Expand Up @@ -237,7 +238,7 @@ public void testSuccessfulPurchaseRefund() throws PaymentPluginApiException, Api
}

@Test(groups = "slow")
public void testSuccessfulPurchaseMultiplePartialRefunds() throws PaymentPluginApiException, ApiException, PaymentApiException {
public void testSuccessfulPurchaseMultiplePartialRefunds() throws PaymentPluginApiException, PaymentApiException {
final UUID kbPaymentMethodId = createQualpayCustomerWithCreditCardAndReturnKBPaymentMethodId();

final Payment payment = TestUtils.buildPayment(account.getId(), account.getPaymentMethodId(), account.getCurrency(), killbillApi);
Expand Down Expand Up @@ -315,6 +316,42 @@ public void testSuccessfulPurchaseMultiplePartialRefunds() throws PaymentPluginA
assertEquals(paymentTransactionInfoPlugin4.size(), 4);
}

@Test(groups = "slow")
public void testVerifyAddPaymentMethodPurchaseNoVault() throws PaymentPluginApiException, ApiException, PaymentApiException {
// Directly tokenize the card
final String cardId = tokenizeCreditCard();

final UUID kbAccountId = account.getId();
assertEquals(qualpayPaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.<PluginProperty>of(), context).size(), 0);

// Add the payment method
final UUID kbPaymentMethodId = UUID.randomUUID();
qualpayPaymentPluginApi.addPaymentMethod(kbAccountId,
kbPaymentMethodId,
new PluginPaymentMethodPlugin(kbPaymentMethodId, null, false, ImmutableList.<PluginProperty>of()),
false,
PluginProperties.buildPluginProperties(ImmutableMap.of("card_id", cardId)),
context);
final List<PaymentMethodInfoPlugin> paymentMethods = qualpayPaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.<PluginProperty>of(), context);
assertEquals(paymentMethods.size(), 1);
assertEquals(paymentMethods.get(0).getAccountId(), kbAccountId);
assertEquals(paymentMethods.get(0).getExternalPaymentMethodId(), cardId);

// Verify the card id can be used
final Payment payment = TestUtils.buildPayment(account.getId(), account.getPaymentMethodId(), account.getCurrency(), killbillApi);
final PaymentTransaction purchaseTransaction = TestUtils.buildPaymentTransaction(payment, TransactionType.PURCHASE, BigDecimal.TEN, payment.getCurrency());
final PaymentTransactionInfoPlugin purchaseInfoPlugin = qualpayPaymentPluginApi.purchasePayment(account.getId(),
payment.getId(),
purchaseTransaction.getId(),
paymentMethods.get(0).getPaymentMethodId(),
purchaseTransaction.getAmount(),
purchaseTransaction.getCurrency(),
ImmutableList.of(),
context);
TestUtils.updatePaymentTransaction(purchaseTransaction, purchaseInfoPlugin);
verifyPaymentTransactionInfoPlugin(payment, purchaseTransaction, purchaseInfoPlugin, PaymentPluginStatus.PROCESSED);
}

private void verifyPaymentTransactionInfoPlugin(final Payment payment,
final PaymentTransaction paymentTransaction,
final PaymentTransactionInfoPlugin paymentTransactionInfoPlugin,
Expand Down Expand Up @@ -379,4 +416,19 @@ private String createQualpayCustomerWithCreditCard() throws PaymentPluginApiExce

return customFieldUserApi.getCustomFieldsForAccountType(kbAccountId, ObjectType.ACCOUNT, context).get(0).getFieldValue();
}

private String tokenizeCreditCard() throws ApiException {
final AddBillingCardRequest billingCardsItem = new AddBillingCardRequest();
billingCardsItem.setCardNumber("4111111111111111");
billingCardsItem.setExpDate("0420");
billingCardsItem.setCvv2("152");
billingCardsItem.setBillingFirstName("John");
billingCardsItem.setBillingLastName("Doe");
billingCardsItem.setBillingZip("94402");
billingCardsItem.setMerchantId(qualpayPaymentPluginApi.getMerchantId(context));

final ApiClient apiClient = qualpayPaymentPluginApi.buildApiClient(context, true);
final PGApi pgApi = new PGApi(apiClient);
return pgApi.tokenize(billingCardsItem).getCardId();
}
}

0 comments on commit 29ee43f

Please sign in to comment.