public interface PayPalPaymentService {

    /**
     * Creates a PayPal payment
     *
     * @param paymentRequest
     * @param performCheckoutOnReturn Indicates if we should start checkout after the user has
     *        authorized the payment
     *
     * @return The new payment
     * @throws PaymentException
     */
    Payment createPayPalPayment(PaymentRequest paymentRequest, boolean performCheckoutOnReturn)
            throws PaymentException;

    /**
     * Updates the PayPal payment to be in sync with the order. This method should be used when
     * fulfillment or pricing information changes after Payment creation. For creating payment use
     * {@link #createPayPalPayment(PaymentRequest, boolean)}
     * 
     * @throws PaymentException
     * @param paymentRequest
     */
    void updatePayPalPaymentForFulfillment(@NonNull PaymentRequest paymentRequest)
            throws PaymentException;

    /**
     * Updates the PayPal payment to include the provided custom value.
     *
     * @param paymentId the primary identifier of the PayPal Payment
     * @param custom the value that is to be stored on the PayPal Payment object
     * @throws PaymentException thrown if the request to update the Payment fails
     */
    void updatePaymentCustom(String paymentId, String custom) throws PaymentException;

} public class MessageConstants {

    public static final String CHECKOUT_COMPLETE = "CheckoutComplete";
    public static final String TOTAL = "Total";
    public static final String ESTIMATEDTOTAL = "EstimatedTotal";
    public static final String AUTHORIZATONID = "AUTHORIZATIONID";
    public static final String CAPTUREID = "CAPTUREID";
    public static final String SALEID = "SALEID";
    public static final String REFUNDID = "REFUNDID";
    public static final String NOTE = "NOTE";
    public static final String PAYERID = "PAYERID";
    public static final String PAYMENTID = "PAYMENTID";
    public static final String PAYERSTATUS = "PAYERSTATUS";
    public static final String ADDRESSSTATUS = "PAYMENTREQUEST_n_ADDRESSSTATUS";
    public static final String DETAILSPAYMENTITEMTOTAL = "PAYMENTREQUEST_n_ITEMAMT";
    public static final String DETAILSPAYMENTSHIPPINGTOTAL = "PAYMENTREQUEST_n_SHIPPINGAMT";
    public static final String DETAILSPAYMENTSHIPPINGDISCOUNT = "PAYMENTREQUEST_n_SHIPDISCAMT";
    public static final String DETAILSPAYMENTTOTALTAX = "PAYMENTREQUEST_n_TAXAMT";
    public static final String DETAILSPAYMENTTRANSACTIONID = "PAYMENTREQUEST_n_TRANSACTIONID";
    public static final String DETAILSPAYMENTALLOWEDMETHOD =
            "PAYMENTREQUEST_n_ALLOWEDPAYMENTMETHOD";
    public static final String BILLINGAGREEMENTID = "BILLING_AGREEMENT_ID";
    public static final String BILLINGECTOKEN = "BILLING_EC_TOKEN";
    public static final String AGREEMENTTOKENID = "AGREEMENT_TOKEN_ID";
    public static final String PAYER_PAYMENTMETHOD_PAYPAL = "paypal";
    public static final String PAYER_INFO_EMAIL = "PAYPAL_PAYER_INFO_EMAIL";
    public static final String PAYER_INFO_FIRST_NAME = "PAYPAL_PAYER_INFO_FIRST_NAME";
    public static final String PAYER_INFO_LAST_NAME = "PAYPAL_PAYER_INFO_LAST_NAME";
    public static final String PLAN_TYPE_MERCHANTINITIATEDBILLING = "MERCHANT_INITIATED_BILLING";
    public static final String MERCHANTPREF_ACCEPTEDPAYMENTTYPE_INSTANT = "INSTANT";
    public static final String HTTP_HEADER_AUTH_ASSERTION = "PayPal-Auth-Assertion";
    public static final String HTTP_HEADER_CLIENT_METADATA_ID = "PayPal-Client-Metadata-Id";
    public static final String IDEMPOTENCY_KEY = "idempotency_key";
    public static final String HTTP_HEADER_REQUEST_ID = "PayPal-Request-Id";
    public static final String HTTP_HEADER_MOCK_RESPONSE = "PayPal-Mock-Response";
    public static final String BN = "PayPal-Partner-Attribution-Id";
    public static final String BNCODE = "BroadleafCommerce_Cart_EC";
    public static final String HTTP_PAYERID = "payerId";
    public static final String HTTP_PAYMENTID = "paymentId";
    public static final String HTTP_TOKEN = "token";
    public static final String HTTP_BILLINGTOKEN = "billingToken";
    public static final String HTTP_REQUEST = "HTTP_REQUEST";
    public static final String ERROR_NAME = "errorName";
    public static final String ERROR_MESSAGE = "errorMessage";
    public static final String ERROR_DEBUG_ID = "errorDebugId";
    public static final String CUSTOM_FIELD = "custom_field";
    public static final String TRANSACTION_STATUS = "TRANSACTION_STATUS";
    public static final String PAYMENT_SUBMITTED_TIME = "PAYMENT_SUBMITTED_TIME";

} public class PayPalAuthorizationRetrievalRequest extends PayPalRequest {

    protected String authId;

    public PayPalAuthorizationRetrievalRequest(String authId, APIContext apiContext) {
        super(apiContext);
        this.authId = authId;
    }

    @Override
    protected PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalAuthorizationRetrievalResponse(Authorization.get(apiContext, authId));
    }

    @Override
    protected boolean isRequestValid() {
        return StringUtils.isNoneBlank(authId);
    }

} public class PayPalAuthorizationRetrievalResponse implements PayPalResponse {

    protected Authorization authorization;

    public PayPalAuthorizationRetrievalResponse(Authorization authorization) {
        this.authorization = authorization;
    }

    public Authorization getAuthorization() {
        return authorization;
    }
} public class PayPalAuthorizeRequest extends PayPalRequest {

    protected Payment payment;
    protected PaymentExecution paymentExecution;

    public PayPalAuthorizeRequest(Payment payment, PaymentExecution paymentExecution,
            APIContext apiContext) {
        super(apiContext);
        this.payment = payment;
        this.paymentExecution = paymentExecution;
    }

    @Override
    public PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalAuthorizeResponse(payment.execute(apiContext, paymentExecution));
    }

    @Override
    protected boolean isRequestValid() {
        return payment != null && StringUtils.isNoneBlank(payment.getId())
                && paymentExecution != null
                && StringUtils.isNoneBlank(paymentExecution.getPayerId());
    }

} public class PayPalAuthorizeResponse implements PayPalResponse {

    protected Payment authorizedPayment;
    protected Authorization authorization;

    public PayPalAuthorizeResponse(Payment authorizedPayment) {
        this.authorizedPayment = authorizedPayment;
    }

    public Payment getAuthorizedPayment() {
        return authorizedPayment;
    }

    public Authorization getAuthorization() {
        if (authorization == null) {
            this.authorization = authorizedPayment.getTransactions().get(0).getRelatedResources()
                    .get(0).getAuthorization();
        }
        return authorization;
    }
} public class PayPalCaptureRequest extends PayPalRequest {

    protected Authorization authorization;
    protected Capture capture;

    public PayPalCaptureRequest(Authorization authorization, Capture capture,
            APIContext apiContext) {
        super(apiContext);
        this.authorization = authorization;
        this.capture = capture;
    }

    @Override
    public PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalCaptureResponse(authorization.capture(apiContext, capture));
    }

    @Override
    protected boolean isRequestValid() {
        return authorization != null && StringUtils.isNoneBlank(authorization.getId())
                && capture != null && capture.getAmount() != null
                && capture.getAmount().getDetails() == null
                && StringUtils.isNoneBlank(capture.getAmount().getCurrency())
                && StringUtils.isNoneBlank(capture.getAmount().getTotal())
                && NumberUtils.isCreatable(capture.getAmount().getTotal());
    }


} public class PayPalCaptureResponse implements PayPalResponse {

    protected Capture capture;

    public PayPalCaptureResponse(Capture capture) {
        this.capture = capture;
    }

    public Capture getCapture() {
        return capture;
    }
} public class PayPalCaptureRetrievalRequest extends PayPalRequest {

    protected String captureId;

    public PayPalCaptureRetrievalRequest(String captureId, APIContext apiContext) {
        super(apiContext);
        this.captureId = captureId;
    }

    @Override
    protected PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalCaptureRetrievalResponse(Capture.get(apiContext, captureId));
    }

    @Override
    protected boolean isRequestValid() {
        return StringUtils.isNoneBlank(captureId);
    }

} public class PayPalCaptureRetrievalResponse implements PayPalResponse {

    protected Capture capture;

    public PayPalCaptureRetrievalResponse(Capture capture) {
        this.capture = capture;
    }

    public Capture getCapture() {
        return capture;
    }
} public class PayPalCreatePaymentRequest extends PayPalRequest {

    protected Payment payment;

    public PayPalCreatePaymentRequest(Payment payment, APIContext apiContext) {
        super(apiContext);
        this.payment = payment;
    }

    @Override
    protected PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalCreatePaymentResponse(payment.create(apiContext));
    }

    @Override
    protected boolean isRequestValid() {
        boolean baseCaseValid = payment != null && payment.getPayer() != null
                && CollectionUtils.isNotEmpty(payment.getTransactions());
        if (baseCaseValid) {
            Transaction transaction = payment.getTransactions().get(0);
            if (transaction != null && transaction.getAmount() != null) {
                Amount amount = transaction.getAmount();
                Details details = amount.getDetails();
                boolean amountConditions = (amount.getCurrency() == null
                        || StringUtils.isNotBlank(amount.getCurrency()))
                        && StringUtils.isNotBlank(amount.getTotal());
                boolean detailsConditions = details != null
                        && StringUtils.isNotBlank(details.getSubtotal())
                        && (details.getShipping() == null
                                || StringUtils.isNotBlank(details.getShipping()))
                        && (details.getTax() == null || StringUtils.isNotBlank(details.getTax()));
                return amountConditions && detailsConditions;
            }
        }
        return false;
    }

} public class PayPalCreatePaymentResponse implements PayPalResponse {

    protected Payment payment;

    public PayPalCreatePaymentResponse(Payment payment) {
        this.payment = payment;
    }

    public Payment getPayment() {
        return payment;
    }
} public class PayPalRefundRequest extends PayPalRequest {

    protected RefundRequest refundRequest;
    protected Capture capture;
    protected Sale sale;

    public PayPalRefundRequest(RefundRequest refundRequest, Capture capture,
            APIContext apiContext) {
        super(apiContext);
        this.refundRequest = refundRequest;
        this.capture = capture;
    }

    public PayPalRefundRequest(RefundRequest refundRequest, Sale sale, APIContext apiContext) {
        super(apiContext);
        this.refundRequest = refundRequest;
        this.sale = sale;
    }

    @Override
    protected PayPalResponse executeInternal() throws PayPalRESTException {
        if (capture != null) {
            return new PayPalRefundResponse(capture.refund(apiContext, refundRequest));
        } else {
            return new PayPalRefundResponse(sale.refund(apiContext, refundRequest));
        }
    }

    @Override
    protected boolean isRequestValid() {
        return refundRequest != null && refundRequest.getAmount() != null
                && ((capture != null && StringUtils.isNoneBlank(capture.getId()))
                        || (sale != null && StringUtils.isNoneBlank(sale.getId())));
    }

} public class PayPalRefundResponse implements PayPalResponse {

    protected DetailedRefund detailedRefund;

    public PayPalRefundResponse(DetailedRefund detailedRefund) {
        this.detailedRefund = detailedRefund;
    }

    public DetailedRefund getDetailedRefund() {
        return detailedRefund;
    }
} public class PayPalSaleRequest extends PayPalRequest {

    protected Payment payment;
    protected PaymentExecution paymentExecution;

    public PayPalSaleRequest(Payment payment, PaymentExecution paymentExecution,
            APIContext apiContext) {
        super(apiContext);
        this.payment = payment;
        this.paymentExecution = paymentExecution;
    }

    @Override
    public PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalSaleResponse(payment.execute(apiContext, paymentExecution));
    }

    @Override
    protected boolean isRequestValid() {
        return payment != null && StringUtils.isNoneBlank(payment.getId())
                && paymentExecution != null
                && StringUtils.isNoneBlank(paymentExecution.getPayerId());
    }
} public class PayPalSaleResponse implements PayPalResponse {

    protected Payment salePayment;
    protected Sale sale;

    public PayPalSaleResponse(Payment salePayment) {
        this.salePayment = salePayment;
    }

    public Payment getSalePayment() {
        return salePayment;
    }

    public Sale getSale() {
        if (sale == null) {
            this.sale = salePayment.getTransactions().get(0).getRelatedResources().get(0).getSale();
        }
        return sale;
    }

} public class PayPalSaleRetrievalRequest extends PayPalRequest {

    protected String saleId;

    public PayPalSaleRetrievalRequest(String saleId, APIContext apiContext) {
        super(apiContext);
        this.saleId = saleId;
    }

    @Override
    protected PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalSaleRetrievalResponse(Sale.get(apiContext, saleId));
    }

    @Override
    protected boolean isRequestValid() {
        return StringUtils.isNoneBlank(saleId);
    }

} public class PayPalSaleRetrievalResponse implements PayPalResponse {

    protected Sale sale;

    public PayPalSaleRetrievalResponse(Sale sale) {
        this.sale = sale;
    }

    public Sale getSale() {
        return sale;
    }
} public class PayPalVoidRequest extends PayPalRequest {

    protected Authorization authorization;

    public PayPalVoidRequest(Authorization authorization, APIContext apiContext) {
        super(apiContext);
        this.authorization = authorization;
    }

    @Override
    protected PayPalResponse executeInternal() throws PayPalRESTException {
        return new PayPalVoidResponse(authorization.doVoid(apiContext));
    }

    @Override
    protected boolean isRequestValid() {
        return authorization != null && StringUtils.isNoneBlank(authorization.getId());
    }

} public class PayPalVoidResponse implements PayPalResponse {

    protected Authorization voidedAuthorization;

    public PayPalVoidResponse(Authorization voidedAuthorization) {
        this.voidedAuthorization = voidedAuthorization;
    }

    public Authorization getVoidedAuthorization() {
        return voidedAuthorization;
    }
}
import org.apache.commons.lang3.StringUtils;
import org.broadleafcommerce.vendor.paypal.service.PayPalPaymentService;
import org.broadleafcommerce.vendor.paypal.service.payment.MessageConstants;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalAuthorizationRetrievalRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalAuthorizationRetrievalResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalAuthorizeRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalAuthorizeResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalCaptureRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalCaptureResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalCaptureRetrievalRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalCaptureRetrievalResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalCreatePaymentRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalCreatePaymentResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalRefundRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalRefundResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalSaleRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalSaleResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalSaleRetrievalRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalSaleRetrievalResponse;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalVoidRequest;
import org.broadleafcommerce.vendor.paypal.service.payment.PayPalVoidResponse;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import com.broadleafcommerce.money.util.MonetaryUtils;
import com.broadleafcommerce.paymentgateway.domain.PaymentRequest;
import com.broadleafcommerce.paymentgateway.domain.PaymentResponse;
import com.broadleafcommerce.paymentgateway.domain.enums.DefaultPaymentTypes;
import com.broadleafcommerce.paymentgateway.domain.enums.DefaultTransactionFailureTypes;
import com.broadleafcommerce.paymentgateway.domain.enums.DefaultTransactionTypes;
import com.broadleafcommerce.paymentgateway.service.exception.PaymentException;
import com.paypal.api.payments.Amount;
import com.paypal.api.payments.Authorization;
import com.paypal.api.payments.Billing;
import com.paypal.api.payments.Capture;
import com.paypal.api.payments.DetailedRefund;
import com.paypal.api.payments.Error;
import com.paypal.api.payments.FundingInstrument;
import com.paypal.api.payments.Payer;
import com.paypal.api.payments.Payment;
import com.paypal.api.payments.PaymentExecution;
import com.paypal.api.payments.RefundRequest;
import com.paypal.api.payments.RelatedResources;
import com.paypal.api.payments.Sale;
import com.paypal.api.payments.Transaction;
import com.paypal.api.payments.Transactions;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.PayPalRESTException;
import com.paypal.base.rest.PayPalResource;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.apachecommons.CommonsLog;
                .findFirst()
                .orElseThrow(NoSuchElementException::new);
    }

    /**
     * Executes an {@link DefaultTransactionTypes#AUTHORIZE_AND_CAPTURE} transaction based on the
     * provided {@link PaymentRequest}
     *
     * @param paymentRequest The request payload that should be used to form the transaction
     * @return a {@link PayPalResource} representing the final state of the transaction
     */
    protected PayPalResource salePayment(PaymentRequest paymentRequest) {
        Payment payment = new Payment();
        payment.setId(getPaymentId(paymentRequest));
        PaymentExecution paymentExecution = new PaymentExecution();
        paymentExecution.setPayerId(getPayerId(paymentRequest));
        paymentExecution.setTransactions(generateSaleTransactions(paymentRequest));

        APIContext apiContext = paypalCheckoutService.constructAPIContext(paymentRequest);

        if (isBillingAgreementRequest(paymentRequest)) {
            payment.setIntent("sale");
            payment.setPayer(generateSalePayer(paymentRequest));

            PayPalCreatePaymentResponse response = retryTemplate.execute(context -> {
                PayPalCreatePaymentRequest createPaymentRequest =
                        new PayPalCreatePaymentRequest(payment, apiContext);
                return (PayPalCreatePaymentResponse) paypalCheckoutService
                        .call(createPaymentRequest);
            });
            return response.getPayment();
        }

        PayPalSaleResponse response = retryTemplate.execute(context -> {
            PayPalSaleRequest saleRequest =
                    new PayPalSaleRequest(payment, paymentExecution, apiContext);
            return (PayPalSaleResponse) paypalCheckoutService.call(saleRequest);
        });
        return response.getSale();
    }

    /**
     * Generates a {@link Payer} for an {@link DefaultTransactionTypes#AUTHORIZE_AND_CAPTURE}
     * transaction based on the provided {@link PaymentRequest}
     *
     * @param paymentRequest The request payload that should be used to form the payer
     * @return a payer based on the payment request
     */
    @Nullable
    protected Payer generateSalePayer(PaymentRequest paymentRequest) {
        return generateAuthorizePayer(paymentRequest);
    }

    /**
     * Generates a list of {@link Transactions} for an
     * {@link DefaultTransactionTypes#AUTHORIZE_AND_CAPTURE} transaction based on the provided
     * {@link PaymentRequest}
     *
     * @param paymentRequest The request payload that should be used to form the transactions
     * @return a list of transactions based on the payment request
     */
    protected List<Transactions> generateSaleTransactions(PaymentRequest paymentRequest) {
        return generateTransactions(paymentRequest);
    }

    /**
     * Generates a {@link Payer} based on the provided {@link PaymentRequest}
     *
     * @param paymentRequest The request payload that should be used to form the payer
     * @return a payer based on the payment request
     */
    protected Payer generateBillingAgreementPayer(PaymentRequest paymentRequest) {
        Payer payer = new Payer();
        payer.setPaymentMethod(MessageConstants.PAYER_PAYMENTMETHOD_PAYPAL);
        List<FundingInstrument> fundingInstruments = new ArrayList<>();
        FundingInstrument billingAgreement = new FundingInstrument();
        Billing billing = new Billing();
        billing.setBillingAgreementId((String) paymentRequest.getAdditionalFields()
                .get(MessageConstants.BILLINGAGREEMENTID));
        billingAgreement.setBilling(billing);
        fundingInstruments.add(billingAgreement);
        payer.setFundingInstruments(fundingInstruments);
        return payer;
    }

    /**
     * Executes a {@link DefaultTransactionTypes#REVERSE_AUTH} for the provided
     * {@link Authorization} object
     *
     * @param auth The authorization that should be reversed
     * @param paymentRequest The request payload that should be used to form the transaction
     * @return an updated {@link Authorization} representing the final state of the transaction
     */
    protected Authorization voidAuthorization(Authorization auth, PaymentRequest paymentRequest) {
        APIContext apiContext = paypalCheckoutService.constructAPIContext(paymentRequest);

        PayPalVoidResponse response = retryTemplate.execute(context -> {
            PayPalVoidRequest voidRequest = new PayPalVoidRequest(auth, apiContext);
            return (PayPalVoidResponse) paypalCheckoutService.call(voidRequest);
        });
        return response.getVoidedAuthorization();
    }

    /**
     * Executes a {@link DefaultTransactionTypes#REFUND} for the provided {@link Capture} object
     *
     * @param capture The capture that should be refunded
     * @param paymentRequest The request payload that should be used to form the transaction
     * @return a {@link DetailedRefund} representing the final state of the transaction
     */
    protected DetailedRefund refundPayment(Capture capture, PaymentRequest paymentRequest) {
        APIContext apiContext = paypalCheckoutService.constructAPIContext(paymentRequest);

        RefundRequest refund = new RefundRequest();
        Amount amount = new Amount();
        amount.setCurrency(paymentRequest.getOrderSubtotal().getCurrency().getCurrencyCode());
        amount.setTotal(Objects.toString(paymentRequest.getTransactionTotal(), null));
        refund.setAmount(amount);

        PayPalRefundResponse response = retryTemplate.execute(context -> {
            PayPalRefundRequest refundRequest =