package com.braintree.transaction.service.impl;

import com.braintree.command.request.BrainTreeAuthorizationRequest;
import com.braintree.command.request.BrainTreeCreatePaymentMethodRequest;
import com.braintree.command.request.BrainTreeCustomerRequest;
import com.braintree.command.request.BrainTreeDeletePaymentMethodRequest;
import com.braintree.command.request.BrainTreeFindMerchantAccountRequest;
import com.braintree.command.request.BrainTreeFindTransactionRequest;
import com.braintree.command.request.beans.BrainTreeLineItemBean;
import com.braintree.command.result.BrainTreeAbstractTransactionResult;
import com.braintree.command.result.BrainTreeAuthorizationResult;
import com.braintree.command.result.BrainTreeCreatePaymentMethodResult;
import com.braintree.command.result.BrainTreeFindCustomerResult;
import com.braintree.command.result.BrainTreeFindTransactionResult;
import com.braintree.command.result.BrainTreePaymentDetailsResult;
import com.braintree.command.result.BrainTreeRefundTransactionResult;
import com.braintree.command.result.BrainTreeVoidResult;
import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.customer.service.BrainTreeCustomerAccountService;
import com.braintree.customfield.service.BraintreeCustomFieldsService;
import com.braintree.enums.BrainTreeCardType;
import com.braintree.enums.BrainTreePaymentMethod;
import com.braintree.enums.BraintreePaymentTransactionStatus;
import com.braintree.enums.BraintreeThreeDSecureFlow;
import com.braintree.exceptions.BraintreeCreatePaymentMethodException;
import com.braintree.exceptions.BraintreePaymentUnAvailableForReplenishmentException;
import com.braintree.helpers.SubmitForSettlementHelper;
import com.braintree.hybris.data.BrainTreeSubscriptionInfoData;
import com.braintree.hybris.data.BraintreeRefundData;
import com.braintree.hybris.data.BraintreeStatusEventData;
import com.braintree.hybris.data.BraintreeTransactionEntryData;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeExchangeLegacyIdRequest;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.model.BrainTreePaymentInfoModel;
import com.braintree.model.PaymentTransactionLogEntryModel;
import com.braintree.order.service.BraintreeOrderBackofficeUtilService;
import com.braintree.payment.info.service.BraintreePaymentInfoService;
import com.braintree.paypal.converters.impl.BraintreeBillingAddressConverter;
import com.braintree.service.BrainTreeFraudCheckToolService;
import com.braintree.transaction.dao.BraintreeTransactionDAO;
import com.braintree.transaction.service.BrainTreePaymentTransactionService;
import com.braintree.transaction.service.BrainTreeTransactionService;
import com.braintree.util.BrainTreeUtils;
import com.google.common.collect.Lists;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.hybris.platform.commerceservices.enums.CustomerType;
import de.hybris.platform.commerceservices.service.data.CommerceOrderParameter;
import de.hybris.platform.commerceservices.strategies.CheckoutCustomerStrategy;
import de.hybris.platform.core.enums.OrderStatus;
import de.hybris.platform.core.model.c2l.CurrencyModel;
import de.hybris.platform.core.model.order.AbstractOrderEntryModel;
import de.hybris.platform.core.model.order.AbstractOrderModel;
import de.hybris.platform.core.model.order.CartModel;
import de.hybris.platform.core.model.order.OrderModel;
import de.hybris.platform.core.model.order.payment.PaymentInfoModel;
import de.hybris.platform.core.model.user.AddressModel;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.core.servicelayer.data.SearchPageData;
import de.hybris.platform.jalo.order.price.PriceInformation;
import de.hybris.platform.order.CartService;
import de.hybris.platform.payment.AdapterException;
import de.hybris.platform.payment.PaymentService;
import de.hybris.platform.payment.dto.BillingInfo;
import de.hybris.platform.payment.dto.TransactionStatus;
import de.hybris.platform.payment.dto.TransactionStatusDetails;
import de.hybris.platform.payment.enums.PaymentTransactionType;
import de.hybris.platform.payment.model.PaymentTransactionEntryModel;
import de.hybris.platform.payment.model.PaymentTransactionModel;
import de.hybris.platform.product.PriceService;
import de.hybris.platform.servicelayer.i18n.CommonI18NService;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.user.UserService;
import de.hybris.platform.servicelayer.util.ServicesUtil;
import de.hybris.platform.util.DiscountValue;
import de.hybris.platform.util.PriceValue;
import de.hybris.platform.util.TaxValue;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Currency;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.braintree.constants.BraintreeConstants.BRAINTREE_NOT_SUCCESS;
import static com.braintree.constants.BraintreeConstants.BRAINTREE_PROVIDER_NAME;
import static com.braintree.constants.BraintreeConstants.BRAINTREE_SUCCESS;
import static com.braintree.constants.BraintreeConstants.BRAINTREE_SUCCESS_MESSAGE;
import static com.braintree.constants.BraintreeConstants.FAKE_REQUEST_ID;
import static com.braintree.constants.BraintreeConstants.TYPE_TRANSACTION_PARAMETER;
import static com.braintreegateway.Transaction.Status.SETTLEMENT_DECLINED;
import static com.braintreegateway.Transaction.Status.VOIDED;
import static de.hybris.platform.servicelayer.util.ServicesUtil.validateParameterNotNullStandardMessage;
import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;
import static org.apache.commons.lang.StringUtils.EMPTY;
import static org.apache.commons.lang.StringUtils.abbreviate;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.isNotEmpty;
import static org.apache.commons.lang.StringUtils.substringAfter;

public class DefaultBrainTreeTransactionService implements BrainTreeTransactionService {

    private static final Logger LOG = Logger.getLogger(DefaultBrainTreeTransactionService.class);

    private static final int DEFAULT_CURRENCY_DIGIT = 2;
    private static final String _CART_ = "_cart_";
    private static final String _STAMP_ = "_stamp_";
    private static final int MAX_PRODUCT_NAME_FOR_NON_PAYPAL_WITH_LEVEL2LEVEL3_DATA = 35;
    private static final String DATA = "data";
    private static final String REFUND_TRANSACTION = "refundTransaction";
    private static final String REFUND = "refund";
    private static final String LEGACY_ID = "legacyId";

    private CartService cartService;
    private ModelService modelService;
    private UserService userService;
    private BrainTreeCustomerAccountService brainTreeCustomerAccountService;
    private BrainTreePaymentService brainTreePaymentService;
    private BrainTreePaymentTransactionService brainTreePaymentTransactionService;
    private PaymentService paymentService;
    private BraintreeOrderBackofficeUtilService backofficeUtilService;
    private BraintreeBillingAddressConverter billingAddressConverter;
    private CheckoutCustomerStrategy checkoutCustomerStrategy;
    private CommonI18NService commonI18NService;
    private BrainTreeConfigService brainTreeConfigService;
    private BraintreeCustomFieldsService customFieldsService;
    private BraintreePaymentInfoService braintreePaymentInfoService;
    private PriceService priceService;
    private BrainTreeFraudCheckToolService btFraudCheckToolService;
    private BraintreeTransactionDAO braintreeTransactionDAO;
    private SubmitForSettlementHelper submitForSettlementHelper;

    private void checkAndDeleteCurrentPaymentMethod() {
        CartModel sessionCart = cartService.getSessionCart();
        final CustomerModel currentCustomer = (CustomerModel) userService.getCurrentUser();
        final BrainTreePaymentInfoModel paymentInfo =
                (BrainTreePaymentInfoModel) sessionCart.getPaymentInfo();

        if (!paymentInfo.isSaved() && (!(currentCustomer.getType() == CustomerType.REGISTERED)
                || !isCustomerHaveNotCapturedOrdersForPaymentDetails(currentCustomer, paymentInfo))) {

            if (currentCustomer.getType() == CustomerType.REGISTERED) {
                brainTreeCustomerAccountService.deleteBrainTreePaymentInfo(currentCustomer, paymentInfo);
            }

            if (paymentInfo.getPaymentMethodToken() != null) {
                final BrainTreeDeletePaymentMethodRequest request = new BrainTreeDeletePaymentMethodRequest(
                        currentCustomer.getBraintreeCustomerId(), paymentInfo.getPaymentMethodToken());
                brainTreePaymentService.deletePaymentMethod(request);
                paymentInfo.setIsDeletedOnBraintree(true);
                modelService.save(paymentInfo);
            }
            sessionCart.setPaymentInfo(null);
            modelService.save(sessionCart);
        }
    }

    public boolean isCustomerHaveNotCapturedOrdersForPaymentDetails(CustomerModel customerModel,
                                                                    BrainTreePaymentInfoModel paymentInfoModel) {
        return customerModel.getOrders().stream()
                .filter(order -> {
                    PaymentInfoModel paymentInfo = order.getPaymentInfo();
                    if (paymentInfo instanceof BrainTreePaymentInfoModel) {
                        String methodToken = ((BrainTreePaymentInfoModel) (paymentInfo)).getPaymentMethodToken();
                        return ofNullable(methodToken)
                                .map(s -> s.equalsIgnoreCase(paymentInfoModel.getPaymentMethodToken()))
                                .orElse(Boolean.FALSE);
                    }
                    return false;
                })
                .anyMatch(orderModel -> !OrderStatus.COMPLETED.equals(orderModel.getStatus())
                        && !backofficeUtilService.isOrderVoided(orderModel));
    }

    private void saveIntent(AbstractOrderModel cart) {
        BrainTreePaymentInfoModel brainTreePaymentInfo = (BrainTreePaymentInfoModel) cart.getPaymentInfo();
        brainTreePaymentInfo.setPayPalIntent(brainTreeConfigService.getIntent());
        modelService.save(brainTreePaymentInfo);
    }

    public BrainTreeAuthorizationResult brainTreeAuthorizeCapture(final CommerceOrderParameter parameter) {
        BrainTreeAuthorizationRequest authorizationRequest = prepareAuthorizationRequest(parameter);
        authorizationRequest.setSubmitForSettlement(Boolean.TRUE);

        return authorize(parameter.getOrder(), authorizationRequest);
    }

    public BrainTreeAuthorizationResult brainTreeAuthorize(final CommerceOrderParameter parameter) {
        BrainTreeAuthorizationRequest authorizationRequest = prepareAuthorizationRequest(parameter);
        boolean isSubmitForSettlement =
                submitForSettlementHelper.isAvailableSubmitForSettlement(authorizationRequest.getPaymentType());
        authorizationRequest.setSubmitForSettlement(isSubmitForSettlement);

        return authorize(parameter.getOrder(), authorizationRequest);
    }

    private BrainTreeAuthorizationResult authorize(AbstractOrderModel cart,
                                                   BrainTreeAuthorizationRequest authorizationRequest) {
        CustomerModel customer = (CustomerModel) cart.getUser();
        BrainTreeAuthorizationResult brainTreeAuthorizationResult = (BrainTreeAuthorizationResult)
                brainTreePaymentService.authorize(authorizationRequest, customer);

        if (isPaymentMethodShouldBeSavedAfterCreatingTransaction(brainTreeAuthorizationResult)) {
            savePaymentMethodFromBTByToken(brainTreeAuthorizationResult.getPaymentDetails(),
                    cart);
        } else if (brainTreeAuthorizationResult.isShouldGetMethodTokenFromBraintree()) {
            savePaymentMethodFromBTForCustomer(authorizationRequest.getCustomerId(), cart);
        }

        return brainTreeAuthorizationResult;
    }

    private boolean isPaymentMethodShouldBeSavedAfterCreatingTransaction(BrainTreeAuthorizationResult result) {
        return result.getPaymentDetails() != null &&
                isNotBlank(result.getPaymentDetails().getPaymentMethodToken())
                && (BrainTreeUtils.isPayPalPayment(result.getPaymentDetails().getPaymentProvider()));
    }

    private void savePaymentMethodFromBTByToken(final BrainTreePaymentDetailsResult paymentMethod,
                                                final AbstractOrderModel cart) {
        brainTreePaymentService.updatePaymentInfo(cart.getPaymentInfo(), paymentMethod);

        LOG.info(paymentMethod);
    }

    private void savePaymentMethodFromBTForCustomer(String braintreeCustomerId, final AbstractOrderModel cart) {
        final BrainTreeCustomerRequest findCustomerRequest = new BrainTreeCustomerRequest(
                braintreeCustomerId);
        findCustomerRequest.setCustomerId(braintreeCustomerId);
        BrainTreeFindCustomerResult brainTreeFindCustomerResult = brainTreePaymentService
                .findCustomer(findCustomerRequest);
        if (brainTreeFindCustomerResult.isCustomerExist()) {
            List<BrainTreePaymentDetailsResult> payPalAccounts = brainTreeFindCustomerResult.getPayPalMethods();
            if (payPalAccounts != null && payPalAccounts.size() >= 1) {
                final BrainTreePaymentDetailsResult paymentMethod = payPalAccounts.get(0);
                brainTreePaymentService.updatePaymentInfo(cart.getPaymentInfo(), paymentMethod);
            }
        }
    }

    private BrainTreeAuthorizationRequest prepareAuthorizationRequest(final CommerceOrderParameter parameter) {
        AbstractOrderModel cart = parameter.getOrder();
        BigDecimal authAmount = parameter.getAmount();

        final PaymentInfoModel paymentInfo = parameter.getOrder().getPaymentInfo();
        final CustomerModel customer = (CustomerModel) cart.getUser();
        final String braintreeCustomerId = customer.getBraintreeCustomerId();
        validateParameterNotNullStandardMessage("paymentInfo", paymentInfo);
        String methodNonce = null;
        String deviceData = null;
        String paymentType = null;
        Boolean liabilityShifted = null;
        Boolean usePaymentMethodToken = null;
        boolean is3dSecureFlow = false;
        String paymentMethodToken = null;
        String paymentMethodGraphQLToken = null;
        Boolean threeDSecureConfiguration = null;
        Boolean advancedFraudTools = null;
        String merchantAccountIdForCurrentSite = null;
        String brainTreeChannel = null;
        boolean storeInVault = false;
        String shipsFromPostalCode = null;
        String venmoProfileId = null;

        LOG.info("prepareAuthorizationRequest, order number: " + cart.getCode());
        LOG.info(
                "cart.totalPrice: " + cart.getTotalPrice() + ", authAmount: " + authAmount + ", total tax: " + cart
                        .getTotalTax());

        if (paymentInfo instanceof BrainTreePaymentInfoModel) {
            methodNonce = ((BrainTreePaymentInfoModel) paymentInfo).getNonce();
            deviceData = ((BrainTreePaymentInfoModel) paymentInfo).getDeviceData();
            paymentType = ((BrainTreePaymentInfoModel) paymentInfo).getPaymentProvider();
            usePaymentMethodToken = ((BrainTreePaymentInfoModel) paymentInfo).isUsePaymentMethodToken();
            paymentMethodToken = ((BrainTreePaymentInfoModel) paymentInfo).getPaymentMethodToken();
            paymentMethodGraphQLToken = ((BrainTreePaymentInfoModel) paymentInfo).getPaymentMethodGraphQLToken();
            is3dSecureFlow = ((BrainTreePaymentInfoModel) paymentInfo).isThreeDSecureFlow();
            ((BrainTreePaymentInfoModel) paymentInfo).setThreeDSecureFlow(false);
            modelService.save(paymentInfo);

            storeInVault = brainTreeConfigService.isStoreInVault();
            liabilityShifted = ((BrainTreePaymentInfoModel) paymentInfo).isLiabilityShifted();

            if (BrainTreeUtils.isVenmoPayment(paymentType) &&
                    isNotBlank(brainTreeConfigService.getVenmoProfileId())) {
                venmoProfileId = brainTreeConfigService.getVenmoProfileId();
            }
            threeDSecureConfiguration = ((BrainTreePaymentInfoModel) paymentInfo).isThreeDSecureConfiguration();
            advancedFraudTools = brainTreeConfigService.isAdvancedFraudTools();
            merchantAccountIdForCurrentSite = ((BrainTreePaymentInfoModel) paymentInfo)
                    .getMerchantAccountIdForCurrentSite();
            brainTreeChannel = ((BrainTreePaymentInfoModel) paymentInfo).getBrainTreeChannel();
            shipsFromPostalCode = ((BrainTreePaymentInfoModel) paymentInfo).getShipsFromPostalCode();
        }

        final AddressModel shippingAddress;
        final AddressModel billingAddress;
        shippingAddress = cart.getDeliveryAddress();

        final BillingInfo shippingInfo = billingAddressConverter.convert(shippingAddress);

        final BrainTreeAuthorizationRequest authorizationRequest = new BrainTreeAuthorizationRequest(
                paymentInfo.getCode(), null,
                getCurrencyInstanceByCart(cart), authAmount, shippingInfo);
        authorizationRequest.setMethodNonce(methodNonce);
        authorizationRequest.setDeviceData(deviceData);
        authorizationRequest.setPaymentType(paymentType);
        authorizationRequest.setUsePaymentMethodToken(usePaymentMethodToken);
        authorizationRequest.setPaymentMethodToken(paymentMethodToken);
        authorizationRequest.setPaymentMethodGraphQLToken(paymentMethodGraphQLToken);
        authorizationRequest.setLiabilityShifted(liabilityShifted);
        authorizationRequest.setCustomerId(braintreeCustomerId);
        authorizationRequest.setThreeDSecureConfiguration(threeDSecureConfiguration);
        authorizationRequest.setAdvancedFraudTools(advancedFraudTools);
        authorizationRequest.setVenmoProfileId(venmoProfileId);
        authorizationRequest.setThreeDSecureFlow(is3dSecureFlow);
        authorizationRequest.setBrainTreeOrderId(cart.getBrainTreeOrderId());

        if (shippingAddress != null) {
            authorizationRequest.setBrainTreeAddressId(shippingAddress.getBrainTreeAddressId());
        }

        if (cart.getPaymentInfo().getBillingAddress() != null) {
            billingAddress = cart.getPaymentInfo().getBillingAddress();
            authorizationRequest.setBrainTreeBilligAddressId(billingAddress.getBrainTreeAddressId());
            final BillingInfo billingInfo = billingAddressConverter.convert(billingAddress);
            authorizationRequest.setBillingInfo(billingInfo);
        }

        if (isNotEmpty(merchantAccountIdForCurrentSite)) {
            final BrainTreeFindMerchantAccountRequest brainTreeFindMerchantAccountRequest = new BrainTreeFindMerchantAccountRequest(
                    EMPTY);
            brainTreeFindMerchantAccountRequest.setMerchantAccount(merchantAccountIdForCurrentSite);
            if (brainTreePaymentService.findMerchantAccount(brainTreeFindMerchantAccountRequest)
                    .isMerchantAccountExist()) {
                authorizationRequest.setMerchantAccountIdForCurrentSite(merchantAccountIdForCurrentSite);
            }
        }
        authorizationRequest.setBrainTreeChannel(brainTreeChannel);
        authorizationRequest.setStoreInVault(storeInVault);
        if (parameter.getCustomFields().size() > 0) {
            authorizationRequest.setCustomFields(parameter.getCustomFields());
        }

        //		add Level 2 data
        authorizationRequest.setPurchaseOrderNumber(cart.getCode());

        //		calc taxAmount via percentage of AUTH-amount
        BigDecimal authPercentage = roundNumberToTwoDecimalPlaces(
                authAmount.doubleValue() * 100 / (cart.getTotalPrice().doubleValue()));
        LOG.info("authPercentage: " + authPercentage + ", as double: " + authPercentage.doubleValue());

        BigDecimal taxSupposed = roundNumberToTwoDecimalPlaces(
                cart.getTotalTax().doubleValue() * authPercentage.doubleValue() / 100);
        LOG.info("taxSupposed: " + taxSupposed);
        authorizationRequest.setTaxAmountAuthorize(taxSupposed.doubleValue());

        //		add Level 3 data
        LOG.info("cart.getDeliveryCost: " + cart.getDeliveryCost());
        authorizationRequest.setShippingAmount(cart.getDeliveryCost());

        LOG.info("cart.getDiscounts: " + cart.getDiscounts());
        double orderDiscountAmountSum = 0d;
        for (DiscountValue dv : cart.getGlobalDiscountValues()) {
            LOG.info(
                    "discountString: " + dv.getCode() + ", value: " + dv.getValue());
            orderDiscountAmountSum = Double.sum(orderDiscountAmountSum, Math.abs(dv.getValue()));
        }
        LOG.info("orderDiscountAmountSum: " + orderDiscountAmountSum);
        authorizationRequest.setDiscountAmount(orderDiscountAmountSum);

        LOG.info("shipsFromPostalCode: " + shipsFromPostalCode);
        authorizationRequest.setShipsFromPostalCode(shipsFromPostalCode);

        if (cart.getDeliveryAddress() != null) {
            LOG.info("Delivery PostalCode: " + cart.getDeliveryAddress().getPostalcode());
            authorizationRequest.setShippingPostalCode(cart.getDeliveryAddress().getPostalcode());

            if (cart.getDeliveryAddress().getCountry() != null) {
                LOG.info("Delivery CountryISO: " + cart.getDeliveryAddress().getCountry().getIsocode());
                String alpha3Country = new Locale("en", cart.getDeliveryAddress().getCountry().getIsocode())
                        .getISO3Country();
                authorizationRequest.setShippingCountryCodeAlpha3(alpha3Country);
            }
        }

        List<BrainTreeLineItemBean> lineItems = new ArrayList<>();
        boolean enableLevel2Level3Data = brainTreeConfigService.getBrainTreePluginConfiguration()
                .getBraintreeGeneralConfiguration().isLevel2Level3();
        for (AbstractOrderEntryModel entry : cart.getEntries()) {
            BrainTreeLineItemBean lineItem = new BrainTreeLineItemBean();

            String name = entry.getProduct().getName();
            LOG.info("LineItem name: " + name);

            if (paymentInfo instanceof BrainTreePaymentInfoModel) {
                String paymentProvider = ((BrainTreePaymentInfoModel) paymentInfo).getPaymentProvider();
                if (enableLevel2Level3Data && !BrainTreeUtils.isPayPalPayment(paymentProvider)) {
                    name = abbreviate(name, MAX_PRODUCT_NAME_FOR_NON_PAYPAL_WITH_LEVEL2LEVEL3_DATA);
                }
            }

            lineItem.setName(name);
            Long qty = entry.getQuantity();
            lineItem.setQuantity(new BigDecimal(qty));
            double unitAmount = entry.getBasePrice();
            double itemDiscountAmountSum = 0d;
            if (Double.compare(unitAmount, 0) == 0) {
                unitAmount = priceService.getPriceInformationsForProduct(entry.getProduct()).stream()
                        .map(PriceInformation::getPriceValue)
                        .filter(x -> x.getCurrencyIso().equalsIgnoreCase(cart.getCurrency().getIsocode()))
                        .map(PriceValue::getValue)
                        .findFirst().orElse(0d);
                itemDiscountAmountSum += unitAmount;
            }
            lineItem.setUnitAmount(roundNumberToTwoDecimalPlaces(unitAmount));

            String unitOfMeasure = entry.getUnit().getUnitType();
            lineItem.setUnitOfMeasure(unitOfMeasure);
            lineItem.setTotalAmount(roundNumberToTwoDecimalPlaces(unitAmount).multiply(new BigDecimal(qty.longValue())));

            double itemTaxAmountSum = 0d;
            for (TaxValue taxValue : entry.getTaxValues()) {
                LOG.info("---> taxValue isAbsolute: " + taxValue.isAbsolute() + ", value: " + taxValue.getValue()
                        + ", appliedValue: "
                        + taxValue.getAppliedValue() + ", toString: " + taxValue);
                itemTaxAmountSum = Double.sum(itemTaxAmountSum, Math.abs(taxValue.getAppliedValue()));
            }
            lineItem.setTaxAmount(roundNumberToTwoDecimalPlaces(itemTaxAmountSum));

            for (DiscountValue discountValue : entry.getDiscountValues()) {
                LOG.info(
                        "discountValue.isAbsolute: " + discountValue.isAbsolute() + ", isAsTargetPrice: " + discountValue
                                .isAsTargetPrice());
                LOG.info("discountValue.getValue: " + discountValue.getValue());
                LOG.info("discountValue.getAppliedValue: " + discountValue.getAppliedValue());

                itemDiscountAmountSum = Double.sum(itemDiscountAmountSum, Math.abs(discountValue.getAppliedValue()));
            }
            lineItem.setDiscountAmount(roundNumberToTwoDecimalPlaces(itemDiscountAmountSum));

            orderDiscountAmountSum = Double.sum(itemDiscountAmountSum, orderDiscountAmountSum);
            LOG.info("orderDiscountAmountSum: " + orderDiscountAmountSum);
            authorizationRequest.setDiscountAmount(orderDiscountAmountSum);

            String productCode = entry.getProduct().getCode();
            lineItem.setProductCode(productCode);

            String commodityCode = "N/A";
            lineItem.setCommodityCode(commodityCode);

            lineItems.add(lineItem);
        }
        authorizationRequest.setLineItems(lineItems);

        if (parameter.getPaymentInitiator() != null) {
            authorizationRequest.setPaymentInitiator(parameter.getPaymentInitiator().toString());
        }

        return authorizationRequest;
    }

    private static BigDecimal roundNumberToTwoDecimalPlaces(final double number) {
        return BigDecimal.valueOf(number).setScale(2, RoundingMode.HALF_UP);
    }

    public BigDecimal calculateTotalAmount(final AbstractOrderModel cart) {
        final Double totalPrice = cart.getTotalPrice();
        final Double totalTax = (cart.getNet().booleanValue() && cart.getStore() != null
                && cart.getStore().getExternalTaxEnabled().booleanValue()) ? cart.getTotalTax() : Double.valueOf(0d);
        final BigDecimal totalPriceWithoutTaxBD = BigDecimal.valueOf(totalPrice == null ? 0d : totalPrice.doubleValue())
                .setScale(2,
                        RoundingMode.HALF_EVEN);
        final BigDecimal totalPriceBD = BigDecimal.valueOf(totalTax == null ? 0d : totalTax.doubleValue())
                .setScale(2, RoundingMode.HALF_EVEN).add(totalPriceWithoutTaxBD);
        return totalPriceBD;
    }

    @Override
    public void createPaymentMethodTokenForOrderReplenishment() {
        final CustomerModel customer = checkoutCustomerStrategy.getCurrentUserForCheckout();
        final PaymentInfoModel paymentInfo = cartService.getSessionCart().getPaymentInfo();
        BrainTreePaymentInfoModel brainTreePaymentInfo = Optional.of(paymentInfo)
                .filter(BrainTreePaymentInfoModel.class::isInstance)
                .map(BrainTreePaymentInfoModel.class::cast)
                .orElseThrow(() -> new AdapterException("Error during creation payment method for replenishment."));
        if (!brainTreePaymentInfo.isUsePaymentMethodToken()) {
            final BrainTreeCreatePaymentMethodRequest request = new BrainTreeCreatePaymentMethodRequest(null,
                    brainTreePaymentInfo.getNonce(), customer.getBraintreeCustomerId());

            try {
                final BrainTreeCreatePaymentMethodResult result = brainTreePaymentService.createPaymentMethod(request);
                brainTreePaymentInfo.setPaymentMethodToken(result.getPaymentMethodToken());
                brainTreePaymentInfo.setPaymentMethodGraphQLToken(result.getPaymentMethodGraphQLToken());
                brainTreePaymentInfo.setUsePaymentMethodToken(Boolean.TRUE);
                modelService.save(paymentInfo);
            }catch (BraintreeCreatePaymentMethodException e){
                throw new  BraintreePaymentUnAvailableForReplenishmentException();
            }
        }

    }

    @Override
    public PaymentTransactionEntryModel createAuthorizationTransaction(final CommerceOrderParameter parameter) {
        AbstractOrderModel cart = parameter.getOrder();
        parameter.setAmount(getTotalAmount(cart, parameter.getAmount()));
        final BrainTreeAuthorizationResult result = brainTreeAuthorize(parameter);
        return handleAuthorizationResult(parameter, result);
    }

    private PaymentTransactionEntryModel handleAuthorizationResult(CommerceOrderParameter parameter, BrainTreeAuthorizationResult result) {
        AbstractOrderModel cart = parameter.getOrder();
        PaymentTransactionEntryModel paymentTransactionEntry = createTransactionEntry(PaymentTransactionType.AUTHORIZATION, parameter, result);
        PaymentTransactionLogEntryModel paymentTransactionLogEntry = createTransactionLogEntry(result, PaymentTransactionType.AUTHORIZATION.name());
        savePaymentTransaction(paymentTransactionEntry, paymentTransactionLogEntry, cart);
        saveBraintreeTransactionStatus(result.getStatus(), paymentTransactionEntry.getPaymentTransaction());
        LOG.info("[BT AUTHORIZE] Transaction with code : " + paymentTransactionEntry.getCode()
                + " was created with status " + paymentTransactionEntry.getTransactionStatus());

        if (result.isSuccess()) {
            saveIntent(cart);
            brainTreePaymentService.updatePaymentInfo(cart.getPaymentInfo(), result.getPaymentDetails());
            btFraudCheckToolService.updateBtFraudToolReviewStatus(paymentTransactionEntry.getPaymentTransaction(), result);
        } else {
            checkAndDeleteCurrentPaymentMethod();
        }

        return paymentTransactionEntry;
    }

    @Override
    public PaymentTransactionEntryModel createAuthorizationTransactionForCapture(final CommerceOrderParameter parameter, boolean isOrderToBeForceCompleted) {
        BigDecimal totalAmount = getTotalAmount(parameter.getOrder(), parameter.getAmount());
        parameter.setCustomFields(getCustomFields());
        parameter.setAmount(totalAmount);
        BrainTreeAuthorizationResult result = brainTreeAuthorizeCapture(parameter);

        PaymentTransactionEntryModel transactionEntry = createTransactionEntry(PaymentTransactionType.CAPTURE, parameter, result);
        PaymentTransactionLogEntryModel transactionLogEntry = createTransactionLogEntry(result, PaymentTransactionType.CAPTURE.name());

        if (!result.isSuccess()) {
            transactionEntry.setTransactionStatus(TransactionStatus.REJECTED.name());
            transactionEntry.setTransactionStatusDetails(TransactionStatusDetails.BANK_DECLINE.name() + " "
                    + result.getTransactionStatusDetails());
        }

        updatePaymentTransaction(parameter.getTransactionEntry().getPaymentTransaction(), transactionEntry, transactionLogEntry);

        final OrderModel order = (OrderModel) parameter.getOrder();

        if (brainTreePaymentTransactionService.isOrderFullyCaptured(parameter.getTransactionEntry()) || isOrderToBeForceCompleted) {
            brainTreePaymentTransactionService.setOrderStatus(order, OrderStatus.PAYMENT_CAPTURED);
            brainTreePaymentTransactionService.continueOrderProcess(order);
        } else {
            brainTreePaymentTransactionService.setOrderStatus(order, OrderStatus.PARTIAL_CAPTURE);
        }
        saveBraintreeTransactionStatus(result.getStatus(), transactionEntry.getPaymentTransaction());
        return transactionEntry;
    }

    private BigDecimal getTotalAmount(AbstractOrderModel order, BigDecimal totalAmount) {
        if (totalAmount != null) {
            return totalAmount;
        }
        return calculateTotalAmount(order);
    }

    @Override
    public PaymentTransactionEntryModel createCancelTransaction(final PaymentTransactionModel transaction,
                                                                final BrainTreeVoidResult voidResult) {
        final PaymentTransactionType transactionType = PaymentTransactionType.CANCEL;
        final String newEntryCode = paymentService.getNewPaymentTransactionEntryCode(transaction, transactionType);

        final PaymentTransactionEntryModel entry = modelService.create(PaymentTransactionEntryModel.class);
        entry.setType(transactionType);
        entry.setCode(newEntryCode);
        entry.setRequestId(voidResult.getRequestId());
        entry.setPaymentTransaction(transaction);
        entry.setCurrency(resolveCurrency(voidResult.getCurrencyIsoCode()));
        entry.setAmount(formatAmount(voidResult.getAmount()));
        entry.setTransactionStatus(voidResult.getTransactionStatus().toString());
        entry.setTransactionStatusDetails(voidResult.getTransactionStatusDetails().toString());
        entry.setTime(new Date());
        modelService.saveAll(entry, transaction);

        return entry;
    }

    @Override
    public PaymentTransactionEntryModel createRefundTransaction(final PaymentTransactionModel transaction,
                                                                final BrainTreeRefundTransactionResult result) {
        final PaymentTransactionType transactionType = PaymentTransactionType.REFUND_STANDALONE;
        return createPaymentTransaction(transaction, result, transaction.getPlannedAmount(), transactionType);
    }

    @Override
    public PaymentTransactionEntryModel createPartialRefundTransaction(final PaymentTransactionEntryModel captureTransactionEntry,
                                                                       final BrainTreeRefundTransactionResult result, final BigDecimal amount) {
        final PaymentTransactionType transactionType = PaymentTransactionType.REFUND_PARTIAL;
        PaymentTransactionEntryModel partialRefundTransaction =
                createPaymentTransaction(captureTransactionEntry.getPaymentTransaction(), result, amount, transactionType);
        partialRefundTransaction.setSubscriptionID(captureTransactionEntry.getRequestId());
        modelService.save(partialRefundTransaction);
        return partialRefundTransaction;
    }

    public PaymentTransactionEntryModel createPaymentTransaction(PaymentTransactionModel transaction,
                                                                 BrainTreeAbstractTransactionResult result, BigDecimal amount, PaymentTransactionType transactionType,
                                                                 final String authorizeTransactionID) {
        PaymentTransactionEntryModel paymentTransaction = createPaymentTransaction(transaction, result, amount,
                transactionType);
        paymentTransaction.setSubscriptionID(authorizeTransactionID);
        modelService.save(paymentTransaction);
        saveBraintreeTransactionStatus(result.getStatus(), paymentTransaction.getPaymentTransaction());
        return paymentTransaction;
    }

    public PaymentTransactionEntryModel createPaymentTransaction(PaymentTransactionModel transaction,
                                                                 BrainTreeAbstractTransactionResult result, BigDecimal amount, PaymentTransactionType transactionType) {
        final String newEntryCode = paymentService.getNewPaymentTransactionEntryCode(transaction, transactionType);
        final PaymentTransactionEntryModel entry = modelService.create(PaymentTransactionEntryModel.class);
        final PaymentTransactionLogEntryModel logEntry = createTransactionLogEntry(result, transactionType.name());
        logEntry.setPaymentTransaction(transaction);
        entry.setType(transactionType);
        entry.setCode(newEntryCode);
        entry.setPaymentTransaction(transaction);
        entry.setTransactionStatus(result.getTransactionStatus().toString());
        if (result.isSuccess()) {
            entry.setRequestId(result.getTransactionId());
            entry.setRequestIdGraphQL(result.getTransactionGraphQLId());
            fillPaymentTransaction(entry, resolveCurrency(result.getCurrencyIsoCode()),
                    result.getAmount(), result.getTransactionStatusDetails().name());
        } else {
            setRequestId(entry, result.getResponseBody());
            fillPaymentTransaction(entry, transaction.getOrder().getCurrency(),
                    amount, result.getErrorMessage());
        }
        entry.setTime(new Date());
        modelService.saveAll(entry, logEntry, transaction);
        return entry;
    }

    private void setRequestId(PaymentTransactionEntryModel entryModel, String responseBody) {
        JsonParser jsonParser = new JsonParser();
        JsonElement jsonElement = jsonParser.parse(responseBody);

        JsonObject jsonData = jsonElement
                .getAsJsonObject()
                .getAsJsonObject(DATA);

        if (jsonData.has(REFUND_TRANSACTION)) {
            entryModel.setRequestId(jsonData
                    .getAsJsonObject(REFUND_TRANSACTION)
                    .getAsJsonObject(REFUND)
                    .get(LEGACY_ID).getAsString());
        }
    }

    private PaymentTransactionEntryModel fillPaymentTransaction(final PaymentTransactionEntryModel entry,
                                                                final CurrencyModel currency, final BigDecimal requestedAmount, final String statusDetails) {
        entry.setCurrency(currency);
        entry.setAmount(requestedAmount);
        entry.setTransactionStatusDetails(statusDetails);
        return entry;
    }

    protected CurrencyModel resolveCurrency(final String currencyIsoCode) {
        return commonI18NService.getCurrency(currencyIsoCode);
    }

    protected Currency getCurrencyInstanceByCart(final AbstractOrderModel cart) {
        return Currency.getInstance(cart.getCurrency().getIsocode());
    }

    private BigDecimal formatAmount(final BigDecimal amount) {
        return amount.setScale(getCurrencyDigit(), RoundingMode.HALF_EVEN);
    }

    private void savePaymentTransaction(final PaymentTransactionEntryModel paymentTransactionEntry,
                                        final PaymentTransactionLogEntryModel paymentTransactionLogEntry, final AbstractOrderModel cart) {
        final List<PaymentTransactionModel> paymentTransactions;

        modelService.refresh(cart);
        if (!cart.getPaymentTransactions().isEmpty()) {
            paymentTransactions = Lists.newArrayList(cart.getPaymentTransactions());
        } else {
            paymentTransactions = Lists.newArrayList();
        }

        PaymentTransactionModel braintreePaymentTransaction = modelService.create(PaymentTransactionModel.class);

        braintreePaymentTransaction.setRequestId(paymentTransactionEntry.getRequestId());
        braintreePaymentTransaction.setRequestIdGraphQL(paymentTransactionEntry.getRequestIdGraphQL());
        braintreePaymentTransaction.setPaymentProvider(BRAINTREE_PROVIDER_NAME);
        braintreePaymentTransaction.setPlannedAmount(paymentTransactionEntry.getAmount());

        if (isNotNullPaymentInfoAndInstanceOfPaymentInfo(cart)) {
            BrainTreePaymentInfoModel paymentInfo = (BrainTreePaymentInfoModel) cart.getPaymentInfo();
            braintreePaymentTransaction.setRequestToken(paymentInfo.getNonce());
            braintreePaymentTransaction.setInfo(paymentInfo);
        }
        paymentTransactions.add(braintreePaymentTransaction);

        updatePaymentTransaction(braintreePaymentTransaction, paymentTransactionEntry, paymentTransactionLogEntry);
        cart.setPaymentTransactions(paymentTransactions);

        modelService.saveAll(cart);
    }

    private void updatePaymentTransaction(PaymentTransactionModel transaction, PaymentTransactionEntryModel transactionEntry, PaymentTransactionLogEntryModel transactionLogEntry) {
        List<PaymentTransactionEntryModel> entries;
        List<PaymentTransactionLogEntryModel> entriesLogs;

        if (transaction.getEntries() != null) {
            entries = Lists.newArrayList(transaction.getEntries());
        } else {
            entries = Lists.newArrayList();
        }

        if (transaction.getEntriesLogs() != null) {
            entriesLogs = Lists.newArrayList(transaction.getEntriesLogs());
        } else {
            entriesLogs = Lists.newArrayList();
        }

        entries.add(transactionEntry);
        entriesLogs.add(transactionLogEntry);

        transaction.setEntries(entries);
        transaction.setEntriesLogs(entriesLogs);

        modelService.saveAll(transaction, transactionEntry, transactionLogEntry);
    }

    private boolean isNotNullPaymentInfoAndInstanceOfPaymentInfo(AbstractOrderModel cart) {
        return cart.getPaymentInfo() != null && cart.getPaymentInfo() instanceof BrainTreePaymentInfoModel;
    }

    @Override
    public void addTransactionLog(PaymentTransactionModel transaction,
                                  BrainTreeAbstractTransactionResult result, String interactionType) {
        PaymentTransactionLogEntryModel transactionLogEntry = createTransactionLogEntry(result, interactionType);
        transactionLogEntry.setPaymentTransaction(transaction);
        modelService.saveAll(transactionLogEntry, transaction);
    }

    @Override
    public String getGraphQLIdFromTransaction(String legacyId) {
        PaymentTransactionEntryModel transactionEntryByRequestId = brainTreePaymentTransactionService
                .getTransactionEntryByRequestId(legacyId);
        if (transactionEntryByRequestId != null) {
            String requestIdGraphQL = transactionEntryByRequestId.getRequestIdGraphQL();
            if (requestIdGraphQL == null) {
                BrainTreeExchangeLegacyIdRequest request = new BrainTreeExchangeLegacyIdRequest();
                request.setLegacyId(legacyId);
                request.setType(TYPE_TRANSACTION_PARAMETER);
                requestIdGraphQL = brainTreePaymentService.exchangeLegacyIdToGraphQLId(request);
                transactionEntryByRequestId.setRequestIdGraphQL(requestIdGraphQL);
                modelService.save(transactionEntryByRequestId);
            }
            return requestIdGraphQL;
        } else {
            BrainTreeExchangeLegacyIdRequest request = new BrainTreeExchangeLegacyIdRequest();
            request.setLegacyId(legacyId);
            request.setType(TYPE_TRANSACTION_PARAMETER);
            return brainTreePaymentService.exchangeLegacyIdToGraphQLId(request);
        }
    }

    @Override
    public PaymentTransactionLogEntryModel createTransactionLogEntry(String interactionType, String status,
                                                                     String paymentInstrumentType, String achReturnCode,
                                                                     Date date) {
        PaymentTransactionLogEntryModel logEntry = modelService.create(PaymentTransactionLogEntryModel.class);
        logEntry.setInteractionType(interactionType);
        logEntry.setStatus(status);
        logEntry.setPaymentType(paymentInstrumentType);
        logEntry.setAchReturnCode(achReturnCode);
        logEntry.setGatewayResultCode(achReturnCode);
        logEntry.setGatewayDate(date);
        return logEntry;
    }

    private PaymentTransactionLogEntryModel createTransactionLogEntry(String responseBody,
                                                                      String parsedRequestBody, String interactionType, String paymentInstrumentType, Date createdAt,
                                                                      boolean isSuccess, String message, String resultCode, String captureId, String refundId) {

        PaymentTransactionLogEntryModel paymentTransactionLogEntry = modelService.create(PaymentTransactionLogEntryModel.class);

        if (responseBody != null && isSuccess) {
            paymentTransactionLogEntry.setStatus(BRAINTREE_SUCCESS);
            paymentTransactionLogEntry.setGatewayMessage(BRAINTREE_SUCCESS_MESSAGE);
            paymentTransactionLogEntry.setGatewayResultCode(resultCode);
        } else if (responseBody != null) {
            paymentTransactionLogEntry.setStatus(BRAINTREE_NOT_SUCCESS);
            paymentTransactionLogEntry.setGatewayMessage(message);
            paymentTransactionLogEntry.setGatewayResultCode(resultCode);
        }

        paymentTransactionLogEntry.setInteractionType(interactionType);
        paymentTransactionLogEntry.setPaymentType(paymentInstrumentType);
        paymentTransactionLogEntry.setRequestBody(parsedRequestBody);
        ofNullable(createdAt).ifPresent(paymentTransactionLogEntry::setGatewayDate);
        ofNullable(captureId).ifPresent(paymentTransactionLogEntry::setCaptureId);
        ofNullable(refundId).ifPresent(paymentTransactionLogEntry::setRefundId);

        if (responseBody != null) {
            paymentTransactionLogEntry.setResponseBody(responseBody);
        }

        return paymentTransactionLogEntry;
    }

    private PaymentTransactionLogEntryModel createTransactionLogEntry(BrainTreeAuthorizationResult result, String interactionType) {
        String paymentProvider = Optional.ofNullable(result.getPaymentDetails())
                .map(BrainTreePaymentDetailsResult::getPaymentInstrumentType)
                .orElse(StringUtils.EMPTY);
        return createTransactionLogEntry(result.getResponseBody(), result.getRequestBody(), interactionType,
                paymentProvider, result.getAuthorizationTime(),
                result.isSuccess(), result.getMessage(), result.getStatusCode(), result.getCaptureId(), EMPTY);
    }

    @Override
    public PaymentTransactionLogEntryModel createTransactionLogEntry(BrainTreeAbstractTransactionResult result, String interactionType) {
        return createTransactionLogEntry(result.getResponseBody(), result.getRequestBody(), interactionType, result.getPaymentInstrumentType(),
                result.getCreatedAt(), result.isSuccess(), result.getErrorMessage(), result.getErrorCode(), result.getCaptureId(), result.getRefundId());
    }

    private PaymentTransactionEntryModel createTransactionEntry(final PaymentTransactionType type, final CommerceOrderParameter parameter,
                                                                final BrainTreeAuthorizationResult result) {
        final AbstractOrderModel cart = parameter.getOrder();

        PaymentTransactionEntryModel paymentTransactionEntry = cart.getPaymentTransactions().stream()
                .map(PaymentTransactionModel::getEntries)
                .flatMap(List::stream)
                .filter(transactionEntry -> FAKE_REQUEST_ID.equals(transactionEntry.getRequestId()))
                .findFirst().orElseGet(() -> modelService.create(PaymentTransactionEntryModel.class));

        final PaymentInfoModel paymentInfo = cart.getPaymentInfo();
        if (paymentInfo instanceof BrainTreePaymentInfoModel) {
            paymentTransactionEntry.setRequestToken(((BrainTreePaymentInfoModel) paymentInfo).getNonce());
        }

        paymentTransactionEntry.setType(type);
        paymentTransactionEntry.setTransactionStatus(result.getTransactionStatus().toString());

        String statusDetails;
        if (null == result.getAuthorizationExpiresAt()) {
            statusDetails = result.getTransactionStatusDetails().toString();
        } else {
            statusDetails =
                    result.getTransactionStatusDetails().toString() + ", Expires at: " + result.getAuthorizationExpiresAt()
                            .getTime();
        }
        paymentTransactionEntry.setTransactionStatusDetails(statusDetails);

        paymentTransactionEntry.setRequestId(result.getRequestId());
        paymentTransactionEntry.setRequestIdGraphQL(result.getRequestGraphQLId());
        paymentTransactionEntry.setCurrency(cart.getCurrency());
        paymentTransactionEntry.setAmount(parameter.getAmount());
        paymentTransactionEntry.setOriginalAmount(parameter.getAmount());

        final String code =
                BRAINTREE_PROVIDER_NAME + _CART_ + cart.getCode() + _STAMP_ + System.currentTimeMillis();
        paymentTransactionEntry.setCode(code);

        paymentTransactionEntry.setTime(result.getAuthorizationTime());

        return paymentTransactionEntry;
    }

    private int getCurrencyDigit() {
        final CurrencyModel currency = commonI18NService.getCurrentCurrency();
        if (currency != null) {
            final Integer digits = currency.getDigits();
            return digits != null ? digits.intValue() : DEFAULT_CURRENCY_DIGIT;
        }
        return DEFAULT_CURRENCY_DIGIT;
    }

    private BrainTreePaymentInfoModel createPaymentMethodInfoModel(final AddressModel billingAddress,
                                                                   final CustomerModel customerModel, final BrainTreeSubscriptionInfoData subscriptionData) {

        final BrainTreePaymentInfoModel cardPaymentInfoModel = modelService.create(BrainTreePaymentInfoModel.class);

        if (Optional.ofNullable(billingAddress).isPresent()) {
            resolveBillingAddress(billingAddress, customerModel, subscriptionData, cardPaymentInfoModel);
            billingAddress.setOwner(cardPaymentInfoModel);
            cardPaymentInfoModel.setBillingAddress(billingAddress);
        }
        if (userService.isAnonymousUser(customerModel)) {
            cardPaymentInfoModel.setCode(customerModel.getUid());
        } else {
            cardPaymentInfoModel.setCode(customerModel.getUid() + "_" + UUID.randomUUID());
        }
        cardPaymentInfoModel.setUser(customerModel);
        cardPaymentInfoModel.setPaymentMethodToken(subscriptionData.getPaymentMethodToken());
        cardPaymentInfoModel.setPaymentMethodGraphQLToken(subscriptionData.getPaymentMethodGraphQLToken());
        cardPaymentInfoModel.setNonce(subscriptionData.getNonce());
        cardPaymentInfoModel.setDeviceData(subscriptionData.getDeviceData());
        cardPaymentInfoModel.setImageSource(subscriptionData.getImageSource());
        cardPaymentInfoModel
                .setExpirationMonth(ofNullable(subscriptionData.getExpirationMonth()).orElse(EMPTY));
        cardPaymentInfoModel
                .setExpirationYear(ofNullable(subscriptionData.getExpirationYear()).orElse(EMPTY));
        if (isNotEmpty(customerModel.getBraintreeCustomerId())) {
            cardPaymentInfoModel.setCustomerId(customerModel.getBraintreeCustomerId());
        }
        if (subscriptionData.getLiabilityShifted() != null) {
            cardPaymentInfoModel.setLiabilityShifted(subscriptionData.getLiabilityShifted());
        }
        cardPaymentInfoModel.setPaymentProvider(subscriptionData.getPaymentProvider());
        cardPaymentInfoModel.setPaypalFundingSource(subscriptionData.getPaypalFundingSource());
        cardPaymentInfoModel.setShouldBeSaved(subscriptionData.isShouldBeSaved());
        cardPaymentInfoModel.setSaved(subscriptionData.isSavePaymentInfo());
        cardPaymentInfoModel
                .setUsePaymentMethodToken(isNotBlank(cardPaymentInfoModel.getPaymentMethodToken()));
        if (BrainTreeUtils.isPayPalPayment(subscriptionData.getPaymentProvider())) {
            cardPaymentInfoModel.setDuplicate(subscriptionData.isDuplicatedPayment());
        }
        cardPaymentInfoModel
                .setCardNumber(ofNullable(subscriptionData.getCardNumber()).orElse(EMPTY));
        cardPaymentInfoModel.setCardholderName(subscriptionData.getCardholder());
        cardPaymentInfoModel.setPayer(subscriptionData.getEmail());

        if (isNotEmpty(subscriptionData.getCardType())) {
            final String brainTreeCardType = subscriptionData.getCardType().replace("_", " ");
            cardPaymentInfoModel.setCardType(getBrainTreeCardTypeByName(brainTreeCardType));
        }

        cardPaymentInfoModel.setThreeDSecureConfiguration(brainTreeConfigService.is3dSecureConfiguration());
        cardPaymentInfoModel.setAdvancedFraudTools(brainTreeConfigService.isAdvancedFraudTools());
        cardPaymentInfoModel.setBrainTreeChannel(brainTreeConfigService.getBrainTreeChannel());
        cardPaymentInfoModel.setMerchantAccountIdForCurrentSite(
                ofNullable(subscriptionData.getMerchantAccountIdForCurrentSite())
                        .orElseGet(() -> brainTreeConfigService.getMerchantAccountIdForCurrentSiteAndCurrency())
        );
        cardPaymentInfoModel.setVerified(subscriptionData.isVerified());
        if (isNotEmpty(subscriptionData.getLast4())) {
            cardPaymentInfoModel.setLast4(subscriptionData.getLast4());
        }
        if (isNotEmpty(subscriptionData.getRoutingNumber())) {
            cardPaymentInfoModel.setRoutingNumber(subscriptionData.getRoutingNumber());
        }
        return cardPaymentInfoModel;
    }

    @Override
    public BrainTreePaymentInfoModel createPaymentMethodInfo(final AddressModel billingAddress,
                                                             final CustomerModel customerModel, final BrainTreeSubscriptionInfoData braintreeInfo) {

        BrainTreePaymentInfoModel newPaymentMethodModel = createPaymentMethodInfoModel(billingAddress, customerModel,
                braintreeInfo);

        if (BrainTreeUtils.isCreditCardPayment(newPaymentMethodModel.getPaymentProvider()) && shouldUpdateExpiredStatus(braintreeInfo)) {
            braintreePaymentInfoService.updateExpiredStatus(newPaymentMethodModel);
        }
        if (nonNull(billingAddress)) {
            billingAddress.setOwner(newPaymentMethodModel);
            modelService.save(billingAddress);
            LOG.info("[BT PaymentInfo] Created billing address with id: " + billingAddress.getPk());
        }
        modelService.refresh(customerModel);
        modelService.save(newPaymentMethodModel);
        if (customerModel.getDefaultPaymentInfo() == null) {
            newPaymentMethodModel.setIsDefault(Boolean.TRUE);
            brainTreeCustomerAccountService.setDefaultPaymentInfo(customerModel, newPaymentMethodModel);
            modelService.save(newPaymentMethodModel);
        }
        LOG.info("[BT PaymentInfo] Created payment info with id: " + newPaymentMethodModel.getCode());

        return newPaymentMethodModel;
    }

    private boolean shouldUpdateExpiredStatus(BrainTreeSubscriptionInfoData braintreeInfo) {
        return brainTreeConfigService.isBrainTreeCreditExpireOnAddingValidation()
                && (braintreeInfo.isShouldBeSaved() || braintreeInfo.isSavePaymentInfo());
    }

    private void resolveBillingAddress(final AddressModel billingAddress, final CustomerModel customerModel,
                                       final BrainTreeSubscriptionInfoData braintreeInfo, BrainTreePaymentInfoModel cardPaymentInfoModel) {
        if (CustomerType.GUEST.equals(customerModel.getType())
                && (BrainTreeUtils.isPayPalPayment(braintreeInfo.getPaymentProvider())
                || BrainTreeUtils.isCreditCardPayment(braintreeInfo.getPaymentProvider()))) {
            cardPaymentInfoModel.setPaymentInfo(billingAddress.getEmail());
            final String email = substringAfter(customerModel.getUid(), "|");
            billingAddress.setEmail(isNotEmpty(email) ? email : customerModel.getContactEmail());
        }
    }

    private void generatePaymentTransactionModel(final AbstractOrderModel cart,
                                                 List<PaymentTransactionEntryModel> paymentTransactionEntrys, final String paymentProvider) {
        final List<PaymentTransactionModel> paymentTransactions;
        PaymentTransactionEntryModel paymentTransactionEntry = paymentTransactionEntrys.get(0);
        PaymentTransactionModel braintreePaymentTransaction = null;
        paymentTransactions = Lists.newArrayList();

        braintreePaymentTransaction = modelService.create(PaymentTransactionModel.class);
        braintreePaymentTransaction.setRequestId(paymentTransactionEntry.getRequestId());
        braintreePaymentTransaction.setRequestIdGraphQL(paymentTransactionEntry.getRequestIdGraphQL());
        braintreePaymentTransaction.setPaymentProvider(paymentProvider);
        braintreePaymentTransaction
                .setPlannedAmount(paymentTransactionEntry.getAmount().setScale(2, RoundingMode.HALF_UP));

        if (cart.getPaymentInfo() != null && cart.getPaymentInfo() instanceof BrainTreePaymentInfoModel) {
            BrainTreePaymentInfoModel paymentInfo = (BrainTreePaymentInfoModel) cart.getPaymentInfo();
            braintreePaymentTransaction.setInfo(paymentInfo);
        }
        modelService.save(braintreePaymentTransaction);
        paymentTransactions.add(braintreePaymentTransaction);

        braintreePaymentTransaction.setEntries(paymentTransactionEntrys);
        cart.setPaymentTransactions(paymentTransactions);

        modelService.saveAll(paymentTransactionEntry, braintreePaymentTransaction, cart);
    }

    @Override
    public PaymentTransactionEntryModel createTransactionEntryForLocalPaymentReversed(final CartModel cart,
                                                                                      final PaymentTransactionType paymentTransactionType) {

        final PaymentTransactionEntryModel paymentTransactionEntryModel = modelService
                .create(PaymentTransactionEntryModel.class);
        paymentTransactionEntryModel.setType(paymentTransactionType);
        paymentTransactionEntryModel.setTransactionStatus(TransactionStatus.ACCEPTED.name());
        paymentTransactionEntryModel
                .setTransactionStatusDetails(TransactionStatusDetails.AUTHORIZATION_ALREADY_REVERSED.name());
        paymentTransactionEntryModel.setRequestId(FAKE_REQUEST_ID);
        paymentTransactionEntryModel.setTime(new Date());
        paymentTransactionEntryModel.setAmount(calculateTotalAmount(cart));
        paymentTransactionEntryModel.setCurrency(cart.getCurrency());
        final String code =
                BRAINTREE_PROVIDER_NAME + _CART_ + cart.getCode() + _STAMP_ + System.currentTimeMillis();
        paymentTransactionEntryModel.setCode(code);

        modelService.save(paymentTransactionEntryModel);

        return paymentTransactionEntryModel;
    }

    @Override
    public void saveBraintreeTransactionStatus(String status,
                                               PaymentTransactionModel paymentTransactionModel) {
        if (status != null) {
            AbstractOrderModel orderModel = paymentTransactionModel.getOrder();
            orderModel.setBraintreePaymentTransactionStatus(BraintreePaymentTransactionStatus.valueOf(status));
            paymentTransactionModel.setBraintreePaymentTransactionStatus(BraintreePaymentTransactionStatus.valueOf(status));
            modelService.saveAll(orderModel, paymentTransactionModel);
        }
    }

    @Override
    public SearchPageData<PaymentTransactionModel> getPaymentTransactionsByStatuses(
            List<BraintreePaymentTransactionStatus> statuses, SearchPageData<PaymentTransactionModel> searchPage) {
        return braintreeTransactionDAO.getPaymentTransactionsByStatuses(statuses, searchPage);
    }

    @Override
    public void updateBraintreePaymentTransactionStatuses(List<PaymentTransactionModel> transactionModels) {
        List<String> transactionIds = transactionModels.stream()
                .map(PaymentTransactionModel::getRequestIdGraphQL)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        BrainTreeFindTransactionRequest request = new BrainTreeFindTransactionRequest(userService.getCurrentUser().getUid());
        request.setTransactionIds(transactionIds);
        BrainTreeFindTransactionResult transactions = brainTreePaymentService.findTransactions(request);

        List<BraintreeTransactionEntryData> transactionEntries = transactions.getTransactionData().getTransactionEntries();
        transactionModels.forEach(model -> {
            Optional<BraintreeTransactionEntryData> entryData = transactionEntries.stream()
                    .filter(trn -> model.getRequestId().equals(trn.getId()))
                    .findFirst();
            entryData.ifPresent(data -> {
                checkingAndCreatingAuthorizedAndCaptureEntries(model, data);
                saveBraintreeTransactionStatus(data.getBraintreeTransactionStatus(), model);
            });
        });

        List<BraintreeTransactionEntryData> refundedTransactions = transactions.getTransactionData().getTransactionEntries().stream()
                .filter(transaction -> transaction.getRefundIds() != null && !transaction.getRefundIds().isEmpty())
                .collect(Collectors.toList());
        if (!refundedTransactions.isEmpty()) {
            processRefundTransaction(transactionModels, refundedTransactions);
        }
    }

    private void checkingAndCreatingAuthorizedAndCaptureEntries(PaymentTransactionModel transactionModel,
                                                                BraintreeTransactionEntryData entryData) {
        boolean wasCaptureTransaction = wasCapturedTransaction(transactionModel);
        boolean wasSubmittedForSettlementEntryData = wasSubmittedForSettlementEntryData(entryData);

        if (entryData.getBraintreeTransactionStatus().equalsIgnoreCase(VOIDED.name())) {
            paymentTransactionStatusIsVoid(wasCaptureTransaction, wasSubmittedForSettlementEntryData, transactionModel, entryData);
        } else if (wasSubmittedForSettlementEntryData && !wasCaptureTransaction) {
            PaymentTransactionEntryModel transactionEntry = createTransactionEntry(entryData, transactionModel, PaymentTransactionType.CAPTURE);
            saveTransactionLogEntry(transactionEntry, entryData);
            modelService.save(transactionEntry);
            brainTreePaymentTransactionService.processPaymentCapturedOrder((OrderModel) transactionModel.getOrder());
        }
    }

    private void paymentTransactionStatusIsVoid(boolean wasCaptureTransaction, boolean wasSubmittedForSettlementEntryData,
                                                PaymentTransactionModel transactionModel, BraintreeTransactionEntryData entryData) {
        PaymentTransactionEntryModel transactionEntry = null;
        if (!wasSubmittedForSettlementEntryData) {
            transactionEntry = transactionModel.getEntries().get(transactionModel.getEntries().size() - 1);
        } else if (!wasCaptureTransaction) {
            transactionEntry = createTransactionEntry(entryData, transactionModel, PaymentTransactionType.CAPTURE);
            saveTransactionLogEntry(transactionEntry);
        } else {
            transactionEntry = transactionModel.getEntries().stream()
                    .filter(e -> e.getType() == PaymentTransactionType.CAPTURE)
                    .findFirst().orElse(null);
        }
        if (transactionEntry != null) {
            saveVoidTransactionEntry(transactionEntry, OrderStatus.CANCELLED);
            saveTransactionLogEntry(transactionEntry);
        }
    }

    private void processRefundTransaction(List<PaymentTransactionModel> transactionModels,
                                          List<BraintreeTransactionEntryData> refundedTransactions) {
        List<String> refundIds = getRefundedIds(refundedTransactions);

        BrainTreeFindTransactionRequest refundPartialRequest =
                new BrainTreeFindTransactionRequest(userService.getCurrentUser().getUid());
        refundPartialRequest.setTransactionIds(refundIds);
        List<BraintreeTransactionEntryData> refundEntries = brainTreePaymentService.
                findRefundedTransactions(refundPartialRequest).getTransactionData().getTransactionEntries();

        transactionModels.forEach(model -> {
            List<String> entryIds = model.getEntries().stream()
                    .map(PaymentTransactionEntryModel::getRequestId)
                    .collect(Collectors.toList());

            refundEntries.stream()
                    .filter(trn -> model.getRequestId().equals(trn.getRefundedTransaction().getId()))
                    .forEach(trn -> {
                        checkingAndCreatingRefundedEntries(model, trn, entryIds);
                        saveBraintreeTransactionStatus(trn.getBraintreeTransactionStatus(), model);
                    });
        });
    }

    private void checkingAndCreatingRefundedEntries(PaymentTransactionModel model,
                                                    BraintreeTransactionEntryData entryData, List<String> entries) {
        if (!entries.contains(entryData.getId())) {
            PaymentTransactionEntryModel entryModel =
                    createTransactionEntry(entryData, model, PaymentTransactionType.REFUND_PARTIAL);

            if (entryData.getBraintreeTransactionStatus().equalsIgnoreCase(VOIDED.name())) {
                saveVoidTransactionEntry(entryModel, OrderStatus.COMPLETED);
                saveTransactionLogEntry(entryModel, entryData);
            } else if (entryData.getBraintreeTransactionStatus().equalsIgnoreCase(SETTLEMENT_DECLINED.name())) {
                saveRejectedTransactionEntry(entryModel, OrderStatus.REJECTED);
                saveTransactionLogEntry(entryModel, entryData);
            } else {
                saveTransactionLogEntry(entryModel, entryData);
                modelService.save(entryModel);
            }

        } else if (entries.contains(entryData.getId()) && entryData.getBraintreeTransactionStatus().equalsIgnoreCase(VOIDED.name())) {
            PaymentTransactionEntryModel entryModel = model.getEntries().stream()
                    .filter(entry -> entry.getRequestId().equals(entryData.getId()))
                    .findFirst().orElse(null);

            if (entryModel != null && !entryModel.getTransactionStatus().equalsIgnoreCase(VOIDED.name())) {
                saveVoidTransactionEntry(entryModel, OrderStatus.COMPLETED);
                saveTransactionLogEntry(entryModel, entryData);
            }
        }
    }

    @Override
    public void saveVoidTransactionEntry(PaymentTransactionEntryModel entry, OrderStatus status) {
        OrderModel order = (OrderModel) entry.getPaymentTransaction().getOrder();
        entry.setTransactionStatus(VOIDED.name());
        order.setStatus(status);
        modelService.save(entry);
        modelService.save(order);
    }

    @Override
    public void saveRejectedTransactionEntry(PaymentTransactionEntryModel entry, OrderStatus status) {
        OrderModel order = (OrderModel) entry.getPaymentTransaction().getOrder();
        entry.setTransactionStatus(TransactionStatus.REJECTED.name());
        entry.setTransactionStatusDetails(TransactionStatusDetails.AMOUNTS_MUST_MATCH.name());
        order.setStatus(status);
        modelService.save(entry);
        modelService.save(order);
    }

    @Override
    public PaymentTransactionEntryModel createTransactionEntry(BraintreeTransactionEntryData entryData,
                                                               PaymentTransactionModel transactionModel,
                                                               PaymentTransactionType type) {
        PaymentTransactionEntryModel entryModel = modelService.create(PaymentTransactionEntryModel.class);
        entryModel.setTransactionStatus(entryData.getStatus());
        entryModel.setTransactionStatusDetails(TransactionStatusDetails.SUCCESFULL.name());
        entryModel.setType(type);
        entryModel.setCode(paymentService.getNewPaymentTransactionEntryCode(transactionModel, type));
        entryModel.setPaymentTransaction(transactionModel);
        entryModel.setCurrency(commonI18NService.getCurrency(entryData.getCurrencyIsoCode()));
        entryModel.setRequestId(entryData.getId());
        entryModel.setRequestIdGraphQL(entryData.getIdGraphQL());
        entryModel.setAmount(entryData.getTotal());

        if (entryData.getDate() != null) {
            if (SETTLEMENT_DECLINED.name().equalsIgnoreCase(entryData.getStatus()) && PaymentTransactionType.CAPTURE.equals(type)) {
                entryData.getStatusHistory().stream()
                        .findFirst()
                        .map(BraintreeStatusEventData::getTimestamp)
                        .ifPresentOrElse(entryModel::setTime, () -> entryModel.setTime(new Date(entryData.getDate())));
            } else {
                entryModel.setTime(new Date(entryData.getDate()));
            }
        } else {
            entryModel.setTime(DateTime.now().toDate());
        }
        return entryModel;
    }

    private List<String> getRefundedIds(List<BraintreeTransactionEntryData> refundedTransactions) {
        return refundedTransactions.stream()
                .map(BraintreeTransactionEntryData::getRefundIds)
                .flatMap(Collection::stream)
                .map(BraintreeRefundData::getRequestIdGraphQL)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private boolean wasCapturedTransaction(PaymentTransactionModel transactionModel) {
        return transactionModel.getEntries().stream()
                .filter(x -> TransactionStatusDetails.SUCCESFULL.name().equals(x.getTransactionStatusDetails()))
                .map(PaymentTransactionEntryModel::getType)
                .collect(Collectors.toList())
                .contains(PaymentTransactionType.CAPTURE);
    }

    private boolean wasSubmittedForSettlementEntryData(BraintreeTransactionEntryData entryData) {
        return entryData.getStatusHistory().stream()
                .map(BraintreeStatusEventData::getStatus)
                .collect(Collectors.toList())
                .contains(BraintreePaymentTransactionStatus.SUBMITTED_FOR_SETTLEMENT);
    }

    @Override
    public void saveTransactionLogEntry(PaymentTransactionEntryModel entryModel) {
        modelService.save(createTransactionLogEntry(entryModel));
    }

    @Override
    public void saveTransactionLogEntry(PaymentTransactionEntryModel entryModel, BraintreeTransactionEntryData entryData) {
        PaymentTransactionLogEntryModel logEntry = createTransactionLogEntry(entryModel);
        logEntry.setCaptureId(entryData.getCaptureId());
        logEntry.setRefundId(entryData.getRefundId());
        modelService.save(logEntry);
    }

    private PaymentTransactionLogEntryModel createTransactionLogEntry(PaymentTransactionEntryModel entryModel) {
        PaymentTransactionLogEntryModel logEntry = modelService.create(PaymentTransactionLogEntryModel.class);
        logEntry.setPaymentTransaction(entryModel.getPaymentTransaction());
        logEntry.setStatus(BRAINTREE_SUCCESS);
        logEntry.setGatewayDate(entryModel.getTime());
        logEntry.setGatewayMessage(BRAINTREE_SUCCESS_MESSAGE);
        String paymentProvider = ((BrainTreePaymentInfoModel) entryModel.getPaymentTransaction()
                .getOrder().getPaymentInfo()).getPaymentProvider();
        logEntry.setPaymentType(BrainTreeUtils.camelToSnake(paymentProvider));

        if (entryModel.getTransactionStatus().equalsIgnoreCase(VOIDED.name())) {
            logEntry.setInteractionType(VOIDED.name());
        } else if (entryModel.getTransactionStatus().equalsIgnoreCase(TransactionStatus.REJECTED.name())) {
            logEntry.setInteractionType(entryModel.getType().name());
            logEntry.setStatus(TransactionStatus.REJECTED.name());
        } else if (SETTLEMENT_DECLINED.name().equalsIgnoreCase(entryModel.getTransactionStatus()) && PaymentTransactionType.CAPTURE.equals(entryModel.getType())) {
            logEntry.setInteractionType(entryModel.getType().name());
            logEntry.setStatus(BRAINTREE_NOT_SUCCESS);
        } else {
            logEntry.setInteractionType(entryModel.getType().name());
        }
        return logEntry;
    }

    @Override
    public PaymentTransactionModel findTransactionByTransactionId(String transactionId) {
        ServicesUtil.validateParameterNotNull(transactionId, "transactionId cannot be null");

        return braintreeTransactionDAO.findTransactionByTransactionId(transactionId);
    }

    public void createPaymentTransactionForLocalPaymentReversed(final AbstractOrderModel cart,
                                                                List<PaymentTransactionEntryModel> paymentTransactionEntrys) {

        generatePaymentTransactionModel(cart, paymentTransactionEntrys, BrainTreePaymentMethod.LOCALPAYMENT.getCode());
    }

    protected BrainTreeCardType getBrainTreeCardTypeByName(final String brainTreeCardType) {
        return BrainTreeCardType.valueOf(brainTreeCardType);
    }

    private Map<String, String> getCustomFields() {
        return customFieldsService.getDefaultCustomFieldsMap();
    }

    public void setBackofficeUtilService(BraintreeOrderBackofficeUtilService backofficeUtilService) {
        this.backofficeUtilService = backofficeUtilService;
    }

    public void setCartService(final CartService cartService) {
        this.cartService = cartService;
    }

    public void setModelService(final ModelService modelService) {
        this.modelService = modelService;
    }

    public void setUserService(final UserService userService) {
        this.userService = userService;
    }

    public void setBrainTreePaymentService(final BrainTreePaymentService brainTreePaymentService) {
        this.brainTreePaymentService = brainTreePaymentService;
    }

    public void setBrainTreePaymentTransactionService(
            BrainTreePaymentTransactionService brainTreePaymentTransactionService) {
        this.brainTreePaymentTransactionService = brainTreePaymentTransactionService;
    }

    public void setPaymentService(final PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void setBillingAddressConverter(final BraintreeBillingAddressConverter billingAddressConverter) {
        this.billingAddressConverter = billingAddressConverter;
    }

    public void setCheckoutCustomerStrategy(final CheckoutCustomerStrategy checkoutCustomerStrategy) {
        this.checkoutCustomerStrategy = checkoutCustomerStrategy;
    }

    public void setCommonI18NService(final CommonI18NService commonI18NService) {
        this.commonI18NService = commonI18NService;
    }

    public void setBrainTreeCustomerAccountService(final BrainTreeCustomerAccountService brainTreeCustomerAccountService) {
        this.brainTreeCustomerAccountService = brainTreeCustomerAccountService;
    }

    public void setBrainTreeConfigService(final BrainTreeConfigService brainTreeConfigService) {
        this.brainTreeConfigService = brainTreeConfigService;
    }

    public void setCustomFieldsService(BraintreeCustomFieldsService customFieldsService) {
        this.customFieldsService = customFieldsService;
    }

    public void setBraintreePaymentInfoService(
            BraintreePaymentInfoService braintreePaymentInfoService) {
        this.braintreePaymentInfoService = braintreePaymentInfoService;
    }

    public void setPriceService(PriceService priceService) {
        this.priceService = priceService;
    }

    public void setBtFraudCheckToolService(BrainTreeFraudCheckToolService btFraudCheckToolService) {
        this.btFraudCheckToolService = btFraudCheckToolService;
    }

    public void setBraintreeTransactionDAO(final BraintreeTransactionDAO braintreeTransactionDAO) {
        this.braintreeTransactionDAO = braintreeTransactionDAO;
    }

    public void setSubmitForSettlementHelper(SubmitForSettlementHelper submitForSettlementHelper) {
        this.submitForSettlementHelper = submitForSettlementHelper;
    }

}
