org.killbill.billing.plugin.adyen.api.AdyenPaymentPluginApi.java Source code

Java tutorial

Introduction

Here is the source code for org.killbill.billing.plugin.adyen.api.AdyenPaymentPluginApi.java

Source

/*
 * 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.plugin.adyen.api;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Nullable;
import javax.xml.bind.JAXBException;

import org.joda.time.DateTime;
import org.killbill.adyen.recurring.RecurringDetail;
import org.killbill.adyen.recurring.ServiceException;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.osgi.libs.killbill.OSGIConfigPropertiesService;
import org.killbill.billing.osgi.libs.killbill.OSGIKillbillAPI;
import org.killbill.billing.osgi.libs.killbill.OSGIKillbillLogService;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PaymentMethod;
import org.killbill.billing.payment.api.PaymentMethodPlugin;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.plugin.api.GatewayNotification;
import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
import org.killbill.billing.plugin.adyen.api.mapping.PaymentInfoMappingService;
import org.killbill.billing.plugin.adyen.client.AdyenConfigProperties;
import org.killbill.billing.plugin.adyen.client.model.HppCompletedResult;
import org.killbill.billing.plugin.adyen.client.model.PaymentData;
import org.killbill.billing.plugin.adyen.client.model.PaymentInfo;
import org.killbill.billing.plugin.adyen.client.model.PaymentModificationResponse;
import org.killbill.billing.plugin.adyen.client.model.PaymentServiceProviderResult;
import org.killbill.billing.plugin.adyen.client.model.PurchaseResult;
import org.killbill.billing.plugin.adyen.client.model.SplitSettlementData;
import org.killbill.billing.plugin.adyen.client.model.SplitSettlementData.Item;
import org.killbill.billing.plugin.adyen.client.model.UserData;
import org.killbill.billing.plugin.adyen.client.model.paymentinfo.WebPaymentFrontend;
import org.killbill.billing.plugin.adyen.client.notification.AdyenNotificationHandler;
import org.killbill.billing.plugin.adyen.client.notification.AdyenNotificationService;
import org.killbill.billing.plugin.adyen.client.payment.exception.SignatureGenerationException;
import org.killbill.billing.plugin.adyen.client.payment.service.AdyenPaymentServiceProviderHostedPaymentPagePort;
import org.killbill.billing.plugin.adyen.client.payment.service.AdyenPaymentServiceProviderPort;
import org.killbill.billing.plugin.adyen.client.recurring.AdyenRecurringClient;
import org.killbill.billing.plugin.adyen.core.AdyenActivator;
import org.killbill.billing.plugin.adyen.core.AdyenConfigPropertiesConfigurationHandler;
import org.killbill.billing.plugin.adyen.core.AdyenConfigurationHandler;
import org.killbill.billing.plugin.adyen.core.AdyenHostedPaymentPageConfigurationHandler;
import org.killbill.billing.plugin.adyen.core.AdyenRecurringConfigurationHandler;
import org.killbill.billing.plugin.adyen.core.KillbillAdyenNotificationHandler;
import org.killbill.billing.plugin.adyen.dao.AdyenDao;
import org.killbill.billing.plugin.adyen.dao.gen.tables.AdyenPaymentMethods;
import org.killbill.billing.plugin.adyen.dao.gen.tables.AdyenResponses;
import org.killbill.billing.plugin.adyen.dao.gen.tables.records.AdyenPaymentMethodsRecord;
import org.killbill.billing.plugin.adyen.dao.gen.tables.records.AdyenResponsesRecord;
import org.killbill.billing.plugin.api.PluginProperties;
import org.killbill.billing.plugin.api.payment.PluginPaymentPluginApi;
import org.killbill.billing.plugin.util.KillBillMoney;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.clock.Clock;
import org.osgi.service.log.LogService;

import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import static org.killbill.billing.plugin.adyen.api.mapping.UserDataMappingService.toUserData;

public class AdyenPaymentPluginApi extends
        PluginPaymentPluginApi<AdyenResponsesRecord, AdyenResponses, AdyenPaymentMethodsRecord, AdyenPaymentMethods> {

    // Shared properties
    public static final String PROPERTY_PAYMENT_PROCESSOR_ACCOUNT_ID = "paymentProcessorAccountId";
    public static final String PROPERTY_ACQUIRER = "acquirer";
    public static final String PROPERTY_ACQUIRER_MID = "acquirerMID";
    public static final String PROPERTY_SELECTED_BRAND = "selectedBrand";
    public static final String PROPERTY_INSTALLMENTS = "installments";
    public static final String SPLIT_SETTLEMENT_DATA_ITEM = "splitSettlementDataItem";
    public static final String PROPERTY_RECURRING_TYPE = "recurringType";
    public static final String PROPERTY_CAPTURE_DELAY_HOURS = "captureDelayHours";
    /**
     * Cont auth disabled validation on adyens side (no cvc required). We practically tell them that the payment data is valid.
     * Should be given as "true" or "false".
     */
    public static final String PROPERTY_CONTINUOUS_AUTHENTICATION = "contAuth";

    // API
    public static final String PROPERTY_RECURRING_DETAIL_ID = "recurringDetailId";

    // 3-D Secure
    public static final String PROPERTY_PA_RES = "PaRes";
    public static final String PROPERTY_MD = "MD";
    public static final String PROPERTY_TERM_URL = "TermUrl";
    public static final String PROPERTY_USER_AGENT = "userAgent";
    public static final String PROPERTY_ACCEPT_HEADER = "acceptHeader";
    public static final String PROPERTY_THREE_D_THRESHOLD = "threeDThreshold";
    public static final String PROPERTY_MPI_DATA_DIRECTORY_RESPONSE = "mpiDataDirectoryResponse";
    public static final String PROPERTY_MPI_DATA_AUTHENTICATION_RESPONSE = "mpiDataAuthenticationResponse";
    public static final String PROPERTY_MPI_DATA_CAVV = "mpiDataCavv";
    public static final String PROPERTY_MPI_DATA_CAVV_ALGORITHM = "mpiDataCavvAlgorithm";
    public static final String PROPERTY_MPI_DATA_XID = "mpiDataXid";
    public static final String PROPERTY_MPI_DATA_ECI = "mpiDataEci";
    public static final String PROPERTY_MPI_IMPLEMENTATION_TYPE = "mpiImplementationType";

    // Credit cards
    public static final String PROPERTY_CC_ISSUER_COUNTRY = "issuerCountry";
    public static final String PROPERTY_CC_ENCRYPTED_JSON = "encryptedJson";

    // SEPA
    public static final String PROPERTY_DD_HOLDER_NAME = "ddHolderName";
    public static final String PROPERTY_DD_ACCOUNT_NUMBER = "ddNumber";
    public static final String PROPERTY_DD_BANK_IDENTIFIER_CODE = "ddBic";
    // ELV only (processed as SEPA)
    public static final String PROPERTY_ELV_BLZ = "elvBlz";

    // User data
    public static final String PROPERTY_FIRST_NAME = "firstName";
    public static final String PROPERTY_LAST_NAME = "lastName";
    public static final String PROPERTY_IP = "ip";
    public static final String PROPERTY_CUSTOMER_LOCALE = "customerLocale";
    public static final String PROPERTY_CUSTOMER_ID = "customerId";
    public static final String PROPERTY_EMAIL = "email";

    // HPP
    public static final String PROPERTY_CREATE_PENDING_PAYMENT = "createPendingPayment";
    public static final String PROPERTY_AUTH_MODE = "authMode";
    public static final String PROPERTY_PAYMENT_METHOD_ID = "paymentMethodId";
    public static final String PROPERTY_PAYMENT_EXTERNAL_KEY = "paymentExternalKey";
    public static final String PROPERTY_RESULT_URL = "resultUrl";
    public static final String PROPERTY_SERVER_URL = "serverUrl";
    public static final String PROPERTY_SHIP_BEFORE_DATE = "shipBeforeDate";
    public static final String PROPERTY_SKIN_CODE = "skin";
    public static final String PROPERTY_ORDER_DATA = "orderData";
    public static final String PROPERTY_SESSION_VALIDITY = "sessionValidity";
    public static final String PROPERTY_MERCHANT_RETURN_DATA = "merchantReturnData";
    public static final String PROPERTY_ALLOWED_METHODS = "allowedMethods";
    public static final String PROPERTY_BLOCKED_METHODS = "blockedMethods";
    public static final String PROPERTY_BRAND_CODE = "brandCode";
    public static final String PROPERTY_ISSUER_ID = "issuerId";
    public static final String PROPERTY_OFFER_EMAIL = "offerEmail";
    public static final String PROPERTY_HPP_TARGET = "hppTarget";
    public static final String PROPERTY_LOOKUP_DIRECTORY = "lookupDirectory";

    // Internals
    public static final String PROPERTY_ADDITIONAL_DATA = "additionalData";
    public static final String PROPERTY_EVENT_CODE = "eventCode";
    public static final String PROPERTY_EVENT_DATE = "eventDate";
    public static final String PROPERTY_MERCHANT_ACCOUNT_CODE = "merchantAccountCode";
    public static final String PROPERTY_MERCHANT_REFERENCE = "merchantReference";
    public static final String PROPERTY_OPERATIONS = "operations";
    public static final String PROPERTY_ORIGINAL_REFERENCE = "originalReference";
    public static final String PROPERTY_PAYMENT_METHOD = "paymentMethod";
    public static final String PROPERTY_PSP_REFERENCE = "pspReference";
    public static final String PROPERTY_REASON = "reason";
    public static final String PROPERTY_SUCCESS = "success";
    public static final String PROPERTY_FROM_HPP = "fromHPP";
    public static final String PROPERTY_FROM_HPP_TRANSACTION_STATUS = "fromHPPTransactionStatus";
    public static final String PROPERTY_PA_REQ = "PaReq";
    public static final String PROPERTY_DCC_AMOUNT_VALUE = "dccAmount";
    public static final String PROPERTY_DCC_AMOUNT_CURRENCY = "dccCurrency";
    public static final String PROPERTY_DCC_SIGNATURE = "dccSignature";
    public static final String PROPERTY_ISSUER_URL = "issuerUrl";

    private final AdyenConfigurationHandler adyenConfigurationHandler;
    private final AdyenHostedPaymentPageConfigurationHandler adyenHppConfigurationHandler;
    private final AdyenRecurringConfigurationHandler adyenRecurringConfigurationHandler;
    private final AdyenConfigPropertiesConfigurationHandler adyenConfigPropertiesConfigurationHandler;
    private final AdyenDao dao;
    private final AdyenNotificationService adyenNotificationService;

    public AdyenPaymentPluginApi(final AdyenConfigurationHandler adyenConfigurationHandler,
            final AdyenConfigPropertiesConfigurationHandler adyenConfigPropertiesConfigurationHandler,
            final AdyenHostedPaymentPageConfigurationHandler adyenHppConfigurationHandler,
            final AdyenRecurringConfigurationHandler adyenRecurringConfigurationHandler,
            final OSGIKillbillAPI killbillApi, final OSGIConfigPropertiesService osgiConfigPropertiesService,
            final OSGIKillbillLogService logService, final Clock clock, final AdyenDao dao) throws JAXBException {
        super(killbillApi, osgiConfigPropertiesService, logService, clock, dao);
        this.adyenConfigurationHandler = adyenConfigurationHandler;
        this.adyenHppConfigurationHandler = adyenHppConfigurationHandler;
        this.adyenRecurringConfigurationHandler = adyenRecurringConfigurationHandler;
        this.adyenConfigPropertiesConfigurationHandler = adyenConfigPropertiesConfigurationHandler;
        this.dao = dao;

        final AdyenNotificationHandler adyenNotificationHandler = new KillbillAdyenNotificationHandler(killbillApi,
                dao, clock);
        //noinspection RedundantTypeArguments
        this.adyenNotificationService = new AdyenNotificationService(
                ImmutableList.<AdyenNotificationHandler>of(adyenNotificationHandler));
    }

    @Override
    public List<PaymentTransactionInfoPlugin> getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId,
            final Iterable<PluginProperty> properties, final TenantContext context)
            throws PaymentPluginApiException {
        final List<PaymentTransactionInfoPlugin> transactions = super.getPaymentInfo(kbAccountId, kbPaymentId,
                properties, context);
        if (transactions.isEmpty()) {
            // We don't know about this payment (maybe it was aborted in a control plugin)
            return transactions;
        }

        final ExpiredPaymentPolicy expiredPaymentPolicy = expiredPaymentPolicy(context);
        if (expiredPaymentPolicy.isExpired(transactions)) {
            cancelExpiredPayment(expiredPaymentPolicy.latestTransaction(transactions), context);
            // reload payment
            return super.getPaymentInfo(kbAccountId, kbPaymentId, properties, context);
        }
        return transactions;
    }

    private void cancelExpiredPayment(PaymentTransactionInfoPlugin expiredTransaction,
            final TenantContext context) {
        final List<PluginProperty> updatedStatusProperties = PluginProperties.buildPluginProperties(ImmutableMap
                .builder().put(PROPERTY_FROM_HPP_TRANSACTION_STATUS, PaymentPluginStatus.CANCELED.toString())
                .put("message", "Payment Expired - Cancelled by Janitor").build());

        try {
            dao.updateResponse(expiredTransaction.getKbTransactionPaymentId(),
                    PluginProperties.merge(expiredTransaction.getProperties(), updatedStatusProperties),
                    context.getTenantId());
        } catch (final SQLException e) {
            logService.log(LogService.LOG_ERROR, "Unable to update canceled payment", e);
        }
    }

    private ExpiredPaymentPolicy expiredPaymentPolicy(final TenantContext context) {
        return new ExpiredPaymentPolicy(clock, getConfigProperties(context));
    }

    @Override
    protected PaymentTransactionInfoPlugin buildPaymentTransactionInfoPlugin(
            final AdyenResponsesRecord adyenResponsesRecord) {
        return new AdyenPaymentTransactionInfoPlugin(adyenResponsesRecord);
    }

    @Override
    protected PaymentMethodPlugin buildPaymentMethodPlugin(final AdyenPaymentMethodsRecord paymentMethodsRecord) {
        return new AdyenPaymentMethodPlugin(paymentMethodsRecord);
    }

    @Override
    protected PaymentMethodInfoPlugin buildPaymentMethodInfoPlugin(
            final AdyenPaymentMethodsRecord paymentMethodsRecord) {
        return new AdyenPaymentMethodInfoPlugin(paymentMethodsRecord);
    }

    @Override
    protected String getPaymentMethodId(final AdyenPaymentMethodsRecord paymentMethodsRecord) {
        return paymentMethodsRecord.getKbPaymentMethodId();
    }

    @Override
    public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId,
            final Iterable<PluginProperty> properties, final CallContext context) throws PaymentPluginApiException {
        // Retrieve our currently known payment method
        final AdyenPaymentMethodsRecord adyenPaymentMethodsRecord;
        try {
            adyenPaymentMethodsRecord = dao.getPaymentMethod(kbPaymentMethodId, context.getTenantId());
        } catch (final SQLException e) {
            throw new PaymentPluginApiException("Unable to retrieve payment method", e);
        }

        if (adyenPaymentMethodsRecord.getToken() != null) {
            // Retrieve the associated country for that shopper (and the corresponding merchant account)
            final Account account = getAccount(kbAccountId, context);
            final String pluginPropertyCountry = PluginProperties.findPluginPropertyValue(PROPERTY_COUNTRY,
                    properties);
            final String countryCode = pluginPropertyCountry == null ? account.getCountry() : pluginPropertyCountry;
            final String merchantAccount = getMerchantAccount(countryCode, properties, context);

            final Map additionalData = AdyenDao.fromAdditionalData(adyenPaymentMethodsRecord.getAdditionalData());
            Object customerId = additionalData.get(PROPERTY_CUSTOMER_ID);
            if (customerId == null) {
                customerId = MoreObjects.firstNonNull(account.getExternalKey(), account.getId());
            }

            final AdyenRecurringClient adyenRecurringClient = adyenRecurringConfigurationHandler
                    .getConfigurable(context.getTenantId());
            try {
                adyenRecurringClient.revokeRecurringDetails(customerId.toString(), merchantAccount);
            } catch (final ServiceException e) {
                throw new PaymentPluginApiException("Unable to revoke recurring details in Adyen", e);
            }
        }

        super.deletePaymentMethod(kbAccountId, kbPaymentMethodId, properties, context);
    }

    @Override
    public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway,
            final Iterable<PluginProperty> properties, final CallContext context) throws PaymentPluginApiException {
        // If refreshFromGateway isn't set, simply read our tables
        if (!refreshFromGateway) {
            return super.getPaymentMethods(kbAccountId, refreshFromGateway, properties, context);
        }

        // Retrieve our currently known payment methods
        final List<AdyenPaymentMethodsRecord> existingPaymentMethods;
        try {
            existingPaymentMethods = dao.getPaymentMethods(kbAccountId, context.getTenantId());
        } catch (final SQLException e) {
            throw new PaymentPluginApiException("Unable to retrieve existing payment methods", e);
        }

        // We cannot retrieve recurring details from Adyen without a shopper reference
        if (existingPaymentMethods.isEmpty()) {
            return super.getPaymentMethods(kbAccountId, refreshFromGateway, properties, context);
        }

        // Retrieve the associated country for that shopper (and the corresponding merchant account)
        final Account account = getAccount(kbAccountId, context);
        final String pluginPropertyCountry = PluginProperties.findPluginPropertyValue(PROPERTY_COUNTRY, properties);
        final String countryCode = pluginPropertyCountry == null ? account.getCountry() : pluginPropertyCountry;
        final String merchantAccount = getMerchantAccount(countryCode, properties, context);

        for (final AdyenPaymentMethodsRecord record : Lists
                .<AdyenPaymentMethodsRecord>reverse(existingPaymentMethods)) {
            if (record.getToken() != null) {
                // Immutable in Adyen -- nothing to do
                continue;
            }

            final Map additionalData = AdyenDao.fromAdditionalData(record.getAdditionalData());

            Object customerId = additionalData.get(PROPERTY_CUSTOMER_ID);
            if (customerId == null) {
                customerId = MoreObjects.firstNonNull(account.getExternalKey(), account.getId());
            }

            Object recurringType = additionalData.get(PROPERTY_RECURRING_TYPE);
            if (recurringType == null) {
                recurringType = MoreObjects.firstNonNull(
                        PluginProperties.findPluginPropertyValue(PROPERTY_RECURRING_TYPE, properties), "RECURRING");
            }

            final AdyenRecurringClient adyenRecurringClient = adyenRecurringConfigurationHandler
                    .getConfigurable(context.getTenantId());

            final List<RecurringDetail> recurringDetailList;
            try {
                recurringDetailList = adyenRecurringClient.getRecurringDetailList(customerId.toString(),
                        merchantAccount, recurringType.toString());
            } catch (final ServiceException e) {
                logService.log(LogService.LOG_ERROR, "Unable to retrieve recurring details in Adyen", e);
                continue;
            }
            for (final RecurringDetail recurringDetail : recurringDetailList) {
                final AdyenResponsesRecord formerResponse;
                try {
                    formerResponse = dao.getResponse(recurringDetail.getFirstPspReference());
                } catch (final SQLException e) {
                    logService.log(LogService.LOG_ERROR, "Unable to retrieve adyen response", e);
                    continue;
                }
                if (formerResponse == null) {
                    continue;
                }

                final Payment payment;
                try {
                    payment = killbillAPI.getPaymentApi().getPayment(
                            UUID.fromString(formerResponse.getKbPaymentId()), false, false, properties, context);
                } catch (final PaymentApiException e) {
                    logService.log(LogService.LOG_ERROR,
                            "Unable to retrieve Payment for externalKey " + recurringDetail.getFirstPspReference(),
                            e);
                    continue;
                }
                if (payment.getPaymentMethodId().toString().equals(record.getKbPaymentMethodId())) {
                    try {
                        dao.setPaymentMethodToken(record.getKbPaymentMethodId(),
                                recurringDetail.getRecurringDetailReference(), context.getTenantId().toString());
                    } catch (final SQLException e) {
                        logService.log(LogService.LOG_ERROR, "Unable to update token", e);
                        continue;
                    }
                }
            }
        }

        return super.getPaymentMethods(kbAccountId, false, properties, context);
    }

    @Override
    public PaymentTransactionInfoPlugin authorizePayment(final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount,
            final Currency currency, final Iterable<PluginProperty> properties, final CallContext context)
            throws PaymentPluginApiException {
        final AdyenResponsesRecord adyenResponsesRecord;
        try {
            adyenResponsesRecord = dao.updateResponse(kbTransactionId, properties, context.getTenantId());
        } catch (final SQLException e) {
            throw new PaymentPluginApiException(
                    "HPP notification came through, but we encountered a database error", e);
        }

        final boolean isHPPCompletion = adyenResponsesRecord != null && Boolean.valueOf(MoreObjects.firstNonNull(
                AdyenDao.fromAdditionalData(adyenResponsesRecord.getAdditionalData()).get(PROPERTY_FROM_HPP), false)
                .toString());
        if (!isHPPCompletion) {
            // We don't have any record for that payment: we want to trigger an actual authorization call (or complete a 3D-S authorization)
            return executeInitialTransaction(TransactionType.AUTHORIZE, kbAccountId, kbPaymentId, kbTransactionId,
                    kbPaymentMethodId, amount, currency, properties, context);
        } else {
            // We already have a record for that payment transaction and we just updated the response row with additional properties
            // (the API can be called for instance after the user is redirected back from the HPP to store the PSP reference)
        }

        return buildPaymentTransactionInfoPlugin(adyenResponsesRecord);
    }

    @Override
    public PaymentTransactionInfoPlugin capturePayment(final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount,
            final Currency currency, final Iterable<PluginProperty> properties, final CallContext context)
            throws PaymentPluginApiException {
        return executeFollowUpTransaction(TransactionType.CAPTURE,
                new TransactionExecutor<PaymentModificationResponse>() {
                    @Override
                    public PaymentModificationResponse execute(final String merchantAccount,
                            final PaymentData paymentData, final String pspReference,
                            final SplitSettlementData splitSettlementData) {
                        final AdyenPaymentServiceProviderPort port = adyenConfigurationHandler
                                .getConfigurable(context.getTenantId());
                        return port.capture(merchantAccount, paymentData, pspReference, splitSettlementData);
                    }
                }, kbAccountId, kbPaymentId, kbTransactionId, kbPaymentMethodId, amount, currency, properties,
                context);
    }

    @Override
    public PaymentTransactionInfoPlugin purchasePayment(final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount,
            final Currency currency, final Iterable<PluginProperty> properties, final CallContext context)
            throws PaymentPluginApiException {
        final AdyenResponsesRecord adyenResponsesRecord;
        try {
            adyenResponsesRecord = dao.updateResponse(kbTransactionId, properties, context.getTenantId());
        } catch (final SQLException e) {
            throw new PaymentPluginApiException(
                    "HPP notification came through, but we encountered a database error", e);
        }

        if (adyenResponsesRecord == null) {
            // We don't have any record for that payment: we want to trigger an actual purchase (auto-capture) call
            final String captureDelayHours = PluginProperties.getValue(PROPERTY_CAPTURE_DELAY_HOURS, "0",
                    properties);
            final Iterable<PluginProperty> overriddenProperties = PluginProperties.merge(properties,
                    ImmutableList.<PluginProperty>of(
                            new PluginProperty(PROPERTY_CAPTURE_DELAY_HOURS, captureDelayHours, false)));
            return executeInitialTransaction(TransactionType.PURCHASE, kbAccountId, kbPaymentId, kbTransactionId,
                    kbPaymentMethodId, amount, currency, overriddenProperties, context);
        } else {
            // We already have a record for that payment transaction and we just updated the response row with additional properties
            // (the API can be called for instance after the user is redirected back from the HPP to store the PSP reference)
        }

        return buildPaymentTransactionInfoPlugin(adyenResponsesRecord);
    }

    @Override
    public PaymentTransactionInfoPlugin voidPayment(final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final UUID kbPaymentMethodId, final Iterable<PluginProperty> properties,
            final CallContext context) throws PaymentPluginApiException {
        return executeFollowUpTransaction(TransactionType.VOID,
                new TransactionExecutor<PaymentModificationResponse>() {
                    @Override
                    public PaymentModificationResponse execute(final String merchantAccount,
                            final PaymentData paymentData, final String pspReference,
                            final SplitSettlementData splitSettlementData) {
                        final AdyenPaymentServiceProviderPort port = adyenConfigurationHandler
                                .getConfigurable(context.getTenantId());
                        return port.cancel(merchantAccount, paymentData, pspReference, splitSettlementData);
                    }
                }, kbAccountId, kbPaymentId, kbTransactionId, kbPaymentMethodId, null, null, properties, context);
    }

    @Override
    public PaymentTransactionInfoPlugin creditPayment(final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount,
            final Currency currency, final Iterable<PluginProperty> properties, final CallContext context)
            throws PaymentPluginApiException {
        // See https://docs.adyen.com/developers/api-manual#carddepositcardfundtransfercft
        return executeInitialTransaction(TransactionType.CREDIT, kbAccountId, kbPaymentId, kbTransactionId,
                kbPaymentMethodId, amount, currency, properties, context);
    }

    @Override
    public PaymentTransactionInfoPlugin refundPayment(final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final UUID kbPaymentMethodId, final BigDecimal amount,
            final Currency currency, final Iterable<PluginProperty> properties, final CallContext context)
            throws PaymentPluginApiException {
        return executeFollowUpTransaction(TransactionType.REFUND,
                new TransactionExecutor<PaymentModificationResponse>() {
                    @Override
                    public PaymentModificationResponse execute(final String merchantAccount,
                            final PaymentData paymentData, final String pspReference,
                            final SplitSettlementData splitSettlementData) {
                        final AdyenPaymentServiceProviderPort providerPort = adyenConfigurationHandler
                                .getConfigurable(context.getTenantId());
                        return providerPort.refund(merchantAccount, paymentData, pspReference, splitSettlementData);
                    }
                }, kbAccountId, kbPaymentId, kbTransactionId, kbPaymentMethodId, amount, currency, properties,
                context);
    }

    // HPP

    @Override
    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId,
            final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties,
            final CallContext context) throws PaymentPluginApiException {
        //noinspection unchecked
        final Iterable<PluginProperty> mergedProperties = PluginProperties.merge(customFields, properties);

        final Account account = getAccount(kbAccountId, context);
        final String countryCode = getCountryCode(account, null, properties);
        final String merchantAccount = getMerchantAccount(countryCode, properties, context);

        final String amountString = PluginProperties.findPluginPropertyValue(PROPERTY_AMOUNT, mergedProperties);
        Preconditions.checkState(!Strings.isNullOrEmpty(amountString), "amount not specified");
        final BigDecimal amount = new BigDecimal(amountString);
        final String currencyString = PluginProperties.findPluginPropertyValue(PROPERTY_CURRENCY, properties);
        final Currency currency = currencyString == null ? account.getCurrency() : Currency.valueOf(currencyString);
        Preconditions.checkState(currency != null, "currency not specified");

        final PaymentData paymentData = buildPaymentData(merchantAccount, countryCode, account, amount, currency,
                mergedProperties, context);
        final UserData userData = toUserData(account, mergedProperties);

        final boolean shouldCreatePendingPayment = Boolean.valueOf(
                PluginProperties.findPluginPropertyValue(PROPERTY_CREATE_PENDING_PAYMENT, mergedProperties));
        Payment pendingPayment = null;
        if (shouldCreatePendingPayment) {
            final boolean authMode = Boolean
                    .valueOf(PluginProperties.findPluginPropertyValue(PROPERTY_AUTH_MODE, mergedProperties));
            final String paymentMethodIdString = PluginProperties
                    .findPluginPropertyValue(PROPERTY_PAYMENT_METHOD_ID, mergedProperties);
            final UUID paymentMethodId = paymentMethodIdString == null ? null
                    : UUID.fromString(paymentMethodIdString);
            pendingPayment = createPendingPayment(authMode, account, paymentMethodId, paymentData, context);
        }

        final String merchantReference = pendingPayment == null ? paymentData.getPaymentTransactionExternalKey()
                : pendingPayment.getTransactions().get(0).getExternalKey();
        try {
            // Need to store on disk the mapping payment <-> user because Adyen's notification won't provide the latter
            //noinspection unchecked
            dao.addHppRequest(kbAccountId, pendingPayment == null ? null : pendingPayment.getId(),
                    pendingPayment == null ? null : pendingPayment.getTransactions().get(0).getId(),
                    merchantReference, PluginProperties.toMap(mergedProperties), clock.getUTCNow(),
                    context.getTenantId());
        } catch (final SQLException e) {
            throw new PaymentPluginApiException("Unable to store HPP request", e);
        }

        final AdyenPaymentServiceProviderHostedPaymentPagePort hostedPaymentPagePort = adyenHppConfigurationHandler
                .getConfigurable(context.getTenantId());

        final SplitSettlementData splitSettlementData = buildSplitSettlementData(currency, properties);

        final Map formParameter;
        try {
            formParameter = hostedPaymentPagePort.getFormParameter(merchantAccount, paymentData, userData,
                    splitSettlementData);
        } catch (final SignatureGenerationException e) {
            throw new PaymentPluginApiException("Unable to generate signature", e);
        }

        // Safe cast
        final WebPaymentFrontend webPaymentFrontend = (WebPaymentFrontend) paymentData.getPaymentInfo();

        final boolean withDirectory = Boolean
                .valueOf(PluginProperties.findPluginPropertyValue(PROPERTY_LOOKUP_DIRECTORY, mergedProperties));
        if (withDirectory) {
            final Map directory = hostedPaymentPagePort.getDirectory(merchantAccount, amount, currency,
                    merchantReference, webPaymentFrontend.getSkinCode(), webPaymentFrontend.getSessionValidity(),
                    paymentData.getPaymentInfo().getCountry());
            formParameter.put("directory", MoreObjects.firstNonNull(directory, ImmutableMap.of()));
        }

        final String target = webPaymentFrontend.getBrandCode() != null
                ? getConfigProperties(context).getHppSkipDetailsTarget()
                : getConfigProperties(context).getHppTarget();
        final String hppTarget = PluginProperties.getValue(PROPERTY_HPP_TARGET, target, properties);
        return new AdyenHostedPaymentPageFormDescriptor(kbAccountId, hppTarget,
                PluginProperties.buildPluginProperties(formParameter));
    }

    @Override
    public GatewayNotification processNotification(final String notification,
            final Iterable<PluginProperty> properties, final CallContext context) throws PaymentPluginApiException {
        final String notificationResponse = adyenNotificationService.handleNotifications(notification);
        return new AdyenGatewayNotification(notificationResponse);
    }

    private abstract static class TransactionExecutor<T> {

        public T execute(final String merchantAccount, final PaymentData paymentData, final UserData userData,
                final SplitSettlementData splitSettlementData) {
            throw new UnsupportedOperationException();
        }

        public T execute(final String merchantAccount, final PaymentData paymentData, final String pspReference,
                final SplitSettlementData splitSettlementData) {
            throw new UnsupportedOperationException();
        }
    }

    private PaymentTransactionInfoPlugin executeInitialTransaction(final TransactionType transactionType,
            final UUID kbAccountId, final UUID kbPaymentId, final UUID kbTransactionId,
            final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency,
            final Iterable<PluginProperty> properties, final CallContext context) throws PaymentPluginApiException {
        return executeInitialTransaction(transactionType, new TransactionExecutor<PurchaseResult>() {
            @Override
            public PurchaseResult execute(final String merchantAccount, final PaymentData paymentData,
                    final UserData userData, final SplitSettlementData splitSettlementData) {
                final AdyenPaymentServiceProviderPort adyenPort = adyenConfigurationHandler
                        .getConfigurable(context.getTenantId());
                final AdyenResponsesRecord existingAuth = previousAdyenResponseRecord(kbPaymentId,
                        kbTransactionId.toString(), context);
                if (existingAuth != null) {
                    // We are completing a 3D-S payment
                    final String originalMerchantAccount = getMerchantAccountFromRecord(existingAuth);
                    return adyenPort.authorize3DSecure(
                            originalMerchantAccount != null ? originalMerchantAccount : merchantAccount,
                            paymentData, userData, splitSettlementData);
                } else {
                    // We are creating a new transaction (AUTHORIZE, PURCHASE or CREDIT)
                    if (transactionType == TransactionType.CREDIT) {
                        return adyenPort.credit(merchantAccount, paymentData, userData, splitSettlementData);
                    } else {
                        return adyenPort.authorise(merchantAccount, paymentData, userData, splitSettlementData);
                    }
                }
            }
        }, kbAccountId, kbPaymentId, kbTransactionId, kbPaymentMethodId, amount, currency, properties, context);
    }

    private PaymentTransactionInfoPlugin executeInitialTransaction(final TransactionType transactionType,
            final TransactionExecutor<PurchaseResult> transactionExecutor, final UUID kbAccountId,
            final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId,
            final BigDecimal amount, final Currency currency, final Iterable<PluginProperty> properties,
            final TenantContext context) throws PaymentPluginApiException {
        final Account account = getAccount(kbAccountId, context);
        final AdyenPaymentMethodsRecord nonNullPaymentMethodsRecord = getAdyenPaymentMethodsRecord(
                kbPaymentMethodId, context);
        final String countryCode = getCountryCode(account, nonNullPaymentMethodsRecord, properties);
        final String merchantAccount = getMerchantAccount(countryCode, properties, context);

        final boolean fromHPP = Boolean
                .valueOf(PluginProperties.findPluginPropertyValue(PROPERTY_FROM_HPP, properties));
        if (fromHPP) {
            // We are either processing a notification (see KillbillAdyenNotificationHandler), creating a PENDING payment for HPP (see buildFormDescriptor) or recording a payment post HPP redirect
            return getPaymentTransactionInfoPluginForHPP(transactionType, kbAccountId, kbPaymentId, kbTransactionId,
                    amount, currency, properties, context);
        }

        // Pull extra properties from the payment method (such as the customerId)
        final Iterable<PluginProperty> additionalPropertiesFromRecord = buildPaymentMethodPlugin(
                nonNullPaymentMethodsRecord).getProperties();
        //noinspection unchecked
        final Iterable<PluginProperty> mergedProperties = PluginProperties.merge(additionalPropertiesFromRecord,
                properties);
        final PaymentData paymentData = buildPaymentData(merchantAccount, countryCode, account, kbPaymentId,
                kbTransactionId, nonNullPaymentMethodsRecord, amount, currency, mergedProperties, context);
        final UserData userData = toUserData(account, mergedProperties);
        final SplitSettlementData splitSettlementData = buildSplitSettlementData(currency, properties);
        final DateTime utcNow = clock.getUTCNow();

        final PurchaseResult response;
        if (shouldSkipAdyen(properties)) {
            response = new PurchaseResult(PaymentServiceProviderResult.AUTHORISED, null, null, "skip_gw",
                    PaymentServiceProviderResult.AUTHORISED.getResponses()[0],
                    paymentData.getPaymentTransactionExternalKey(),
                    ImmutableMap.<String, String>of("skipGw", "true",
                            AdyenPaymentPluginApi.PROPERTY_MERCHANT_ACCOUNT_CODE, merchantAccount,
                            "merchantReference", paymentData.getPaymentTransactionExternalKey(),
                            "fromHPPTransactionStatus", "PROCESSED"));
        } else {
            response = transactionExecutor.execute(merchantAccount, paymentData, userData, splitSettlementData);
        }

        try {
            dao.addResponse(kbAccountId, kbPaymentId, kbTransactionId, transactionType, amount, currency, response,
                    utcNow, context.getTenantId());
            return new AdyenPaymentTransactionInfoPlugin(kbPaymentId, kbTransactionId, transactionType, amount,
                    currency, utcNow, response);
        } catch (final SQLException e) {
            throw new PaymentPluginApiException(
                    "Payment went through, but we encountered a database error. Payment details: "
                            + response.toString(),
                    e);
        }
    }

    private PaymentTransactionInfoPlugin executeFollowUpTransaction(final TransactionType transactionType,
            final TransactionExecutor<PaymentModificationResponse> transactionExecutor, final UUID kbAccountId,
            final UUID kbPaymentId, final UUID kbTransactionId, final UUID kbPaymentMethodId,
            @Nullable final BigDecimal amount, @Nullable final Currency currency,
            final Iterable<PluginProperty> properties, final TenantContext context)
            throws PaymentPluginApiException {
        final Account account = getAccount(kbAccountId, context);
        final AdyenPaymentMethodsRecord nonNullPaymentMethodsRecord = getAdyenPaymentMethodsRecord(
                kbPaymentMethodId, context);
        final String countryCode = getCountryCode(account, nonNullPaymentMethodsRecord, properties);

        final boolean fromHPP = Boolean
                .valueOf(PluginProperties.findPluginPropertyValue(PROPERTY_FROM_HPP, properties));
        if (fromHPP) {
            // We are processing a notification (see KillbillAdyenNotificationHandler)
            return getPaymentTransactionInfoPluginForHPP(transactionType, kbAccountId, kbPaymentId, kbTransactionId,
                    amount, currency, properties, context);
        }

        final AdyenResponsesRecord previousResponse;
        try {
            previousResponse = dao.getSuccessfulAuthorizationResponse(kbPaymentId, context.getTenantId());
            if (previousResponse == null) {
                throw new PaymentPluginApiException(null,
                        "Unable to retrieve previous payment response for kbTransactionId " + kbTransactionId);
            }
        } catch (final SQLException e) {
            throw new PaymentPluginApiException(
                    "Unable to retrieve previous payment response for kbTransactionId " + kbTransactionId, e);
        }

        final String merchantAccount = getMerchantAccount(countryCode, previousResponse, properties, context);
        final PaymentData paymentData = buildPaymentData(merchantAccount, countryCode, account, kbPaymentId,
                kbTransactionId, nonNullPaymentMethodsRecord, amount, currency, properties, context);
        final SplitSettlementData splitSettlementData = buildSplitSettlementData(currency, properties);
        final DateTime utcNow = clock.getUTCNow();

        final PaymentModificationResponse response;
        if (shouldSkipAdyen(properties)) {
            response = new PaymentModificationResponse(PaymentServiceProviderResult.PENDING.getResponses()[0],
                    (String) null,
                    ImmutableMap.<Object, Object>of("skipGw", "true", "merchantAccountCode", merchantAccount,
                            "merchantReference", paymentData.getPaymentTransactionExternalKey(),
                            "fromHPPTransactionStatus", "PROCESSED"));
        } else {
            response = transactionExecutor.execute(merchantAccount, paymentData, previousResponse.getPspReference(),
                    splitSettlementData);
        }

        final Optional<PaymentServiceProviderResult> paymentServiceProviderResult;
        if (response.isTechnicallySuccessful()) {
            paymentServiceProviderResult = Optional.of(PaymentServiceProviderResult.RECEIVED);
        } else {
            paymentServiceProviderResult = Optional.<PaymentServiceProviderResult>absent();
        }

        try {
            dao.addResponse(kbAccountId, kbPaymentId, kbTransactionId, transactionType, amount, currency, response,
                    utcNow, context.getTenantId());
            return new AdyenPaymentTransactionInfoPlugin(kbPaymentId, kbTransactionId, transactionType, amount,
                    currency, paymentServiceProviderResult, utcNow, response);
        } catch (final SQLException e) {
            throw new PaymentPluginApiException(
                    "Payment went through, but we encountered a database error. Payment details: "
                            + (response.toString()),
                    e);
        }
    }

    private boolean shouldSkipAdyen(final Iterable<PluginProperty> properties) {
        return "true".equals(PluginProperties.findPluginPropertyValue("skipGw", properties))
                || "true".equals(PluginProperties.findPluginPropertyValue("skip_gw", properties));
    }

    private String getCountryCode(final AccountData account,
            @Nullable final AdyenPaymentMethodsRecord paymentMethodsRecord,
            final Iterable<PluginProperty> properties) {
        String country = PluginProperties.getValue(PROPERTY_COUNTRY,
                paymentMethodsRecord == null ? null : paymentMethodsRecord.getCountry(), properties);
        if (country == null && account != null) {
            country = account.getCountry();
        }
        return country;
    }

    private SplitSettlementData buildSplitSettlementData(final Currency currency,
            final Iterable<PluginProperty> pluginProperties) {
        final Map<Short, BigDecimal> amounts = new HashMap<Short, BigDecimal>();
        final Map<Short, String> groups = new HashMap<Short, String>();
        final Map<Short, String> references = new HashMap<Short, String>();
        final Map<Short, String> types = new HashMap<Short, String>();
        for (final PluginProperty pluginProperty : pluginProperties) {
            if (pluginProperty.getKey().startsWith(SPLIT_SETTLEMENT_DATA_ITEM)
                    && pluginProperty.getValue() != null) {
                final String[] parts = pluginProperty.getKey().split("\\.");
                final Short itemNb = Short.parseShort(parts[1]);
                final String suffix = parts[2];

                final String value = pluginProperty.getValue().toString();
                if ("amount".equals(suffix)) {
                    // In major units
                    amounts.put(itemNb, new BigDecimal(value));
                } else if ("group".equals(suffix)) {
                    groups.put(itemNb, value);
                } else if ("reference".equals(suffix)) {
                    references.put(itemNb, value);
                } else if ("type".equals(suffix)) {
                    types.put(itemNb, value);
                }
            }
        }

        final List<Item> items = new LinkedList<Item>();
        for (final Short itemNb : amounts.keySet()) {
            final String type = types.get(itemNb);
            if (type != null) {
                items.add(new SplitSettlementData.Item(
                        KillBillMoney.toMinorUnits(currency.toString(), amounts.get(itemNb)),
                        MoreObjects.firstNonNull(groups.get(itemNb), type),
                        MoreObjects.firstNonNull(references.get(itemNb), type), type));
            }
        }

        if (items.isEmpty()) {
            return null;
        } else {
            return new SplitSettlementData(1, currency.toString(), items);
        }
    }

    private PaymentTransactionInfoPlugin getPaymentTransactionInfoPluginForHPP(
            final TransactionType transactionType, final UUID kbAccountId, final UUID kbPaymentId,
            final UUID kbTransactionId, final BigDecimal amount, final Currency currency,
            final Iterable<PluginProperty> properties, final TenantContext context)
            throws PaymentPluginApiException {
        final AdyenPaymentServiceProviderHostedPaymentPagePort hostedPaymentPagePort = adyenHppConfigurationHandler
                .getConfigurable(context.getTenantId());
        final Map<String, String> requestParameterMap = Maps
                .transformValues(PluginProperties.toStringMap(properties), new Function<String, String>() {
                    @Override
                    public String apply(final String input) {
                        // Adyen will encode parameters like merchantSig
                        return decode(input);
                    }
                });
        final HppCompletedResult hppCompletedResult = hostedPaymentPagePort
                .parseAndVerifyRequestIntegrity(requestParameterMap);
        final PurchaseResult purchaseResult = new PurchaseResult(hppCompletedResult);

        final DateTime utcNow = clock.getUTCNow();
        try {
            final AdyenResponsesRecord adyenResponsesRecord = dao.addResponse(kbAccountId, kbPaymentId,
                    kbTransactionId, transactionType, amount, currency, purchaseResult, utcNow,
                    context.getTenantId());
            return buildPaymentTransactionInfoPlugin(adyenResponsesRecord);
        } catch (final SQLException e) {
            throw new PaymentPluginApiException("HPP payment came through, but we encountered a database error", e);
        }
    }

    // For API
    private PaymentData buildPaymentData(final String merchantAccount, final String countryCode,
            final AccountData account, final UUID kbPaymentId, final UUID kbTransactionId,
            final AdyenPaymentMethodsRecord paymentMethodsRecord, final BigDecimal amount, final Currency currency,
            final Iterable<PluginProperty> properties, final TenantContext context)
            throws PaymentPluginApiException {
        final Payment payment;
        try {
            payment = killbillAPI.getPaymentApi().getPayment(kbPaymentId, false, false, properties, context);
        } catch (final PaymentApiException e) {
            throw new PaymentPluginApiException(String.format("Unable to retrieve kbPaymentId='%s'", kbPaymentId),
                    e);
        }

        final PaymentTransaction paymentTransaction = Iterables.<PaymentTransaction>find(payment.getTransactions(),
                new Predicate<PaymentTransaction>() {
                    @Override
                    public boolean apply(final PaymentTransaction input) {
                        return kbTransactionId.equals(input.getId());
                    }
                });

        final PaymentInfo paymentInfo = buildPaymentInfo(merchantAccount, countryCode, account,
                paymentMethodsRecord, properties, context);

        return new PaymentData<PaymentInfo>(amount, currency, paymentTransaction.getExternalKey(), paymentInfo);
    }

    // For HPP
    private PaymentData buildPaymentData(final String merchantAccount, final String countryCode,
            final AccountData account, final BigDecimal amount, final Currency currency,
            final Iterable<PluginProperty> properties, final TenantContext context) {
        final PaymentInfo paymentInfo = buildPaymentInfo(merchantAccount, countryCode, account, null, properties,
                context);
        final String paymentTransactionExternalKey = PluginProperties.getValue(PROPERTY_PAYMENT_EXTERNAL_KEY,
                UUID.randomUUID().toString(), properties);
        return new PaymentData<PaymentInfo>(amount, currency, paymentTransactionExternalKey, paymentInfo);
    }

    private PaymentInfo buildPaymentInfo(final String merchantAccount, final String countryCode,
            final AccountData account, @Nullable final AdyenPaymentMethodsRecord paymentMethodsRecord,
            final Iterable<PluginProperty> properties, final TenantContext context) {
        // A bit of a hack - it would be nice to be able to isolate AdyenConfigProperties
        final AdyenConfigProperties adyenConfigProperties = getConfigProperties(context);
        return PaymentInfoMappingService.toPaymentInfo(merchantAccount, countryCode, adyenConfigProperties, clock,
                account, paymentMethodsRecord, properties);
    }

    /**
     * There is the option not to use the adyen payment method table to retrieve payment data but to always provide
     * it as plugin properties. In this case an empty record (null object) could help.
     */
    private AdyenPaymentMethodsRecord emptyRecord(@Nullable final UUID kbPaymentMethodId) {
        final AdyenPaymentMethodsRecord record = new AdyenPaymentMethodsRecord();
        if (kbPaymentMethodId != null) {
            record.setKbPaymentMethodId(kbPaymentMethodId.toString());
        }
        return record;
    }

    private AdyenPaymentMethodsRecord getAdyenPaymentMethodsRecord(@Nullable final UUID kbPaymentMethodId,
            final TenantContext context) {
        AdyenPaymentMethodsRecord paymentMethodsRecord = null;

        if (kbPaymentMethodId != null) {
            try {
                paymentMethodsRecord = dao.getPaymentMethod(kbPaymentMethodId, context.getTenantId());
            } catch (final SQLException e) {
                logService.log(LogService.LOG_WARNING, "Failed to retrieve payment method " + kbPaymentMethodId, e);
            }
        }

        return MoreObjects.firstNonNull(paymentMethodsRecord, emptyRecord(kbPaymentMethodId));
    }

    private Payment createPendingPayment(final boolean authMode, final Account account,
            @Nullable final UUID paymentMethodId, final PaymentData paymentData, final CallContext context)
            throws PaymentPluginApiException {
        final UUID kbPaymentId = null;
        final String paymentTransactionExternalKey = paymentData.getPaymentTransactionExternalKey();
        //noinspection UnnecessaryLocalVariable
        final String paymentExternalKey = paymentTransactionExternalKey;
        final ImmutableMap<String, Object> purchasePropertiesMap = ImmutableMap.<String, Object>of(
                AdyenPaymentPluginApi.PROPERTY_FROM_HPP, true,
                AdyenPaymentPluginApi.PROPERTY_FROM_HPP_TRANSACTION_STATUS, PaymentPluginStatus.PENDING.toString());
        final Iterable<PluginProperty> purchaseProperties = PluginProperties
                .buildPluginProperties(purchasePropertiesMap);

        try {
            final UUID kbPaymentMethodId = paymentMethodId != null ? paymentMethodId
                    : getAdyenKbPaymentMethodId(account.getId(), context);
            if (authMode) {
                return killbillAPI.getPaymentApi().createAuthorization(account, kbPaymentMethodId, kbPaymentId,
                        paymentData.getAmount(), paymentData.getCurrency(), paymentExternalKey,
                        paymentTransactionExternalKey, purchaseProperties, context);
            } else {
                return killbillAPI.getPaymentApi().createPurchase(account, kbPaymentMethodId, kbPaymentId,
                        paymentData.getAmount(), paymentData.getCurrency(), paymentExternalKey,
                        paymentTransactionExternalKey, purchaseProperties, context);
            }
        } catch (final PaymentApiException e) {
            throw new PaymentPluginApiException("Failed to record purchase", e);
        }
    }

    // Could be shared (see KillbillAdyenNotificationHandler)
    private UUID getAdyenKbPaymentMethodId(final UUID kbAccountId, final TenantContext context)
            throws PaymentApiException {
        //noinspection RedundantTypeArguments
        return Iterables.<PaymentMethod>find(killbillAPI.getPaymentApi().getAccountPaymentMethods(kbAccountId,
                false, ImmutableList.<PluginProperty>of(), context), new Predicate<PaymentMethod>() {
                    @Override
                    public boolean apply(final PaymentMethod paymentMethod) {
                        return AdyenActivator.PLUGIN_NAME.equals(paymentMethod.getPluginName());
                    }
                }).getId();
    }

    private AdyenResponsesRecord previousAdyenResponseRecord(final UUID kbPaymentId,
            final String kbPaymentTransactionId, final CallContext context) {
        try {
            final AdyenResponsesRecord previousAuthorizationResponse = dao
                    .getSuccessfulAuthorizationResponse(kbPaymentId, context.getTenantId());
            if (previousAuthorizationResponse != null
                    && previousAuthorizationResponse.getKbPaymentTransactionId().equals(kbPaymentTransactionId)) {
                return previousAuthorizationResponse;
            }

            return null;
        } catch (final SQLException e) {
            logService.log(LogService.LOG_ERROR, "Failed to get previous AdyenResponsesRecord", e);
            return null;
        }
    }

    private String getMerchantAccount(final String countryCode, final Iterable<PluginProperty> properties,
            final TenantContext context) {
        return getMerchantAccount(countryCode, null, properties, context);
    }

    private String getMerchantAccount(final String countryCode,
            @Nullable final AdyenResponsesRecord adyenResponsesRecord, final Iterable<PluginProperty> properties,
            final TenantContext context) {
        final String pluginPropertyMerchantAccount = PluginProperties
                .findPluginPropertyValue(PROPERTY_PAYMENT_PROCESSOR_ACCOUNT_ID, properties);
        if (pluginPropertyMerchantAccount != null) {
            return pluginPropertyMerchantAccount;
        }

        if (adyenResponsesRecord != null) {
            final String merchantAccountCode = getMerchantAccountFromRecord(adyenResponsesRecord);
            if (merchantAccountCode != null) {
                return merchantAccountCode;
            }
        }

        return getConfigProperties(context).getMerchantAccount(countryCode);
    }

    private String getMerchantAccountFromRecord(final AdyenResponsesRecord adyenResponsesRecord) {
        final Map additionalData = AdyenDao.fromAdditionalData(adyenResponsesRecord.getAdditionalData());
        final Object merchantAccountCode = additionalData.get(AdyenPaymentPluginApi.PROPERTY_MERCHANT_ACCOUNT_CODE);
        if (merchantAccountCode != null) {
            return merchantAccountCode.toString();
        }
        return null;
    }

    private AdyenConfigProperties getConfigProperties(final TenantContext context) {
        return adyenConfigPropertiesConfigurationHandler.getConfigurable(context.getTenantId());
    }

    public static String decode(final String value) {
        try {
            return URLDecoder.decode(value, "UTF-8");
        } catch (final UnsupportedEncodingException e) {
            return value;
        }
    }
}