public SetExpressCheckoutRequestDetailsType CreateExpressCheckoutReqDetailsType(IPayment payment, PayPalConfiguration payPalConfiguration)
        {
            var setExpressChkOutReqDetails = new SetExpressCheckoutRequestDetailsType
            {
                BillingAddress = AddressHandling.ToAddressType(payment.BillingAddress),
                BuyerEmail     = payment.BillingAddress.Email
            };

            TransactionType transactionType;

            if (Enum.TryParse(payment.TransactionType, out transactionType))
            {
                if (transactionType == TransactionType.Authorization)
                {
                    setExpressChkOutReqDetails.PaymentAction = PaymentActionCodeType.AUTHORIZATION;
                }
                else if (transactionType == TransactionType.Sale)
                {
                    setExpressChkOutReqDetails.PaymentAction = PaymentActionCodeType.SALE;
                }
            }

            if (payPalConfiguration.AllowChangeAddress != "1")
            {
                setExpressChkOutReqDetails.AddressOverride = "1";
            }

            if (payPalConfiguration.AllowGuest == "1")
            {
                setExpressChkOutReqDetails.SolutionType = SolutionTypeType.SOLE;
                setExpressChkOutReqDetails.LandingPage  = LandingPageType.BILLING;
            }

            return(setExpressChkOutReqDetails);
        }
        private SetExpressCheckoutRequestDetailsType SetupExpressCheckoutReqDetailsType(ICart cart, IPayment payment, string orderNumber)
        {
            var setExpressChkOutReqDetails = _payPalAPIHelper.CreateExpressCheckoutReqDetailsType(payment, _paymentMethodConfiguration);

            // This key is sent to PayPal using https so it is not likely it will come from other because
            // only PayPal knows this key to send back to us
            var acceptSecurityKey = Utilities.GetAcceptUrlHashValue(orderNumber);
            var cancelSecurityKey = Utilities.GetCancelUrlHashValue(orderNumber);

            _notifyUrl = UriSupport.AbsoluteUrlBySettings(Utilities.GetUrlFromStartPageReferenceProperty("PayPalPaymentPage"));

            var acceptUrl = UriUtil.AddQueryString(_notifyUrl, "accept", "true");

            acceptUrl = UriUtil.AddQueryString(acceptUrl, "hash", acceptSecurityKey);

            var cancelUrl = UriUtil.AddQueryString(_notifyUrl, "accept", "false");

            cancelUrl = UriUtil.AddQueryString(cancelUrl, "hash", cancelSecurityKey);

            setExpressChkOutReqDetails.CancelURL      = cancelUrl;
            setExpressChkOutReqDetails.ReturnURL      = acceptUrl;
            setExpressChkOutReqDetails.PaymentDetails = new List <PaymentDetailsType>()
            {
                _payPalAPIHelper.GetPaymentDetailsType(payment, cart, orderNumber, _notifyUrl)
            };

            setExpressChkOutReqDetails.BillingAddress = AddressHandling.ToAddressType(payment.BillingAddress);

            return(setExpressChkOutReqDetails);
        }
        /// <summary>
        /// Processes the checkout details from PayPal, update changed addresses appropriately.
        /// </summary>
        /// <param name="payerInfo">The PayPal payer info.</param>
        /// <param name="payPalAddress">The PayPal address.</param>
        /// <param name="orderAddress">The order address to process.</param>
        /// <param name="customerAddressType">The order address to process.</param>
        /// <param name="emptyAddressMsgKey">The empty address message language key in lang.xml file.</param>
        public string ProcessOrderAddress(PayerInfoType payerInfo, AddressType payPalAddress, IOrderAddress orderAddress, CustomerAddressTypeEnum customerAddressType, string emptyAddressMsgKey)
        {
            if (payPalAddress == null)
            {
                return(_localizationService.GetString(emptyAddressMsgKey));
            }

            if (orderAddress == null)
            {
                return(_localizationService.GetString("/Commerce/Checkout/PayPal/CommitTranErrorCartReset"));
            }

            if (string.IsNullOrEmpty(payPalAddress.Phone) && !string.IsNullOrEmpty(payerInfo?.ContactPhone))
            {
                payPalAddress.Phone = payerInfo.ContactPhone;
            }

            AddressHandling.UpdateOrderAddress(orderAddress, customerAddressType, payPalAddress, payerInfo.Payer);

            return(string.Empty);
        }
        /// <summary>
        /// Construct the Gets the PayPal payment details from our payment and Cart to pass onto PayPal.
        /// </summary>
        /// <remarks>
        /// The PayPal payment detail can be a bit different from OrderForm because
        /// sometimes cart total calculated by Commerce is different with cart total calculated by PalPal,
        /// though this difference is very small (~0.01 currency).
        /// We adjust this into an additional item to ensure PayPal shows the same total number with Commerce.
        /// We also add the Order discount (if any) as and special item with negative price to PayPal payment detail.
        /// See detail about PayPal structure type in this link https://developer.paypal.com/docs/classic/express-checkout/ec_api_flow/
        /// </remarks>
        /// <param name="payment">The payment to take info (Total, LineItem, ...) from</param>
        /// <param name="orderGroup">The order group (to be InvoiceID to pass to PayPal)</param>
        /// <param name="orderNumber">The order number.</param>
        /// <param name="notifyUrl">The notify Url.</param>
        /// <returns>The PayPal payment detail to pass to API request</returns>
        public PaymentDetailsType GetPaymentDetailsType(IPayment payment, IOrderGroup orderGroup, string orderNumber, string notifyUrl)
        {
            var orderForm          = orderGroup.Forms.First(form => form.Payments.Contains(payment));
            var paymentDetailsType = new PaymentDetailsType();

            paymentDetailsType.ButtonSource = "Episerver_Cart_EC";                                        // (Optional) An identification code for use by third-party applications to identify transactions. Character length and limitations: 32 single-byte alphanumeric characters
            paymentDetailsType.InvoiceID    = orderNumber;                                                // Character length and limitations: 127 single-byte alphanumeric characters
            paymentDetailsType.Custom       = orderGroup.CustomerId + "|" + paymentDetailsType.InvoiceID; // A free-form field for your own use. Character length and limitations: 256 single-byte alphanumeric characters
                                                                                                          // NOTE: paymentDetailsType.OrderDescription = 127 single-byte alphanumeric characters string
                                                                                                          // NOTE: paymentDetailsType.TransactionId = string, provided if you have transactionId in your Commerce system // (Optional) Transaction identification number of the transaction that was created.;

            // (Optional) Your URL for receiving Instant Payment Notification (IPN) about this transaction. If you do not specify this value in the request, the notification URL from your Merchant Profile is used, if one exists.
            // IMPORTANT:The notify URL only applies to DoExpressCheckoutPayment. This value is ignored when set in SetExpressCheckout or GetExpressCheckoutDetails.
            // Character length and limitations: 2,048 single-byte alphanumeric characters
            paymentDetailsType.NotifyURL = notifyUrl;

            var currency      = orderGroup.Currency;
            var totalOrder    = currency.Round(payment.Amount);
            var totalShipping = currency.Round(orderGroup.GetShippingTotal(_orderGroupCalculator).Amount);
            var totalHandling = currency.Round(orderForm.HandlingTotal);
            var totalTax      = currency.Round(orderGroup.GetTaxTotal(_orderGroupCalculator).Amount);
            var lineItemTotal = 0m;

            var paymentDetailItems = new List <PaymentDetailsItemType>();

            foreach (var lineItem in orderForm.GetAllLineItems())
            {
                // recalculate final unit price after all kind of discounts are subtracted from item.ListPrice
                var finalUnitPrice = currency.Round(lineItem.GetExtendedPrice(currency).Amount / lineItem.Quantity);
                lineItemTotal += finalUnitPrice * lineItem.Quantity;

                paymentDetailItems.Add(new PaymentDetailsItemType
                {
                    Name     = lineItem.DisplayName,
                    Number   = lineItem.Code,
                    Quantity = Convert.ToInt32(lineItem.Quantity.ToString("0")),
                    Amount   = ToPayPalAmount(finalUnitPrice, currency)
                });
            }

            // this adjustment also include the gift-card (in sample)
            var orderAdjustment       = totalOrder - totalShipping - totalHandling - totalTax - lineItemTotal;
            var adjustmentForShipping = 0m;

            if (orderAdjustment != 0 || // adjustment for gift card/(order level) promotion case
                lineItemTotal == 0    // in this case, the promotion (or discount) make all lineItemTotal zero, but buyer still have to pay shipping (and/or handling, tax).
                                      // We still need to adjust lineItemTotal for Paypal accepting (need to be greater than zero)
                )
            {
                var paymentDetailItem = new PaymentDetailsItemType
                {
                    Name        = "Order adjustment",
                    Number      = "ORDERADJUSTMENT",
                    Description = "GiftCard, Discount at OrderLevel and/or PayPal-Commerce-calculating difference in cart total",  // Character length and limitations: 127 single-byte characters
                    Quantity    = 1
                };

                var predictLineitemTotal = lineItemTotal + orderAdjustment;
                if (predictLineitemTotal <= 0)
                {
                    // can't overpaid for item. E.g.: total item amount is 68, orderAdjustment is -70, PayPal will refuse ItemTotal = -2
                    // we need to push -2 to shippingTotal or shippingDiscount

                    // HACK: Paypal will not accept an item total of $0, even if there is a shipping fee. The Item total must be at least 1 cent/penny.
                    // We need to take 1 cent/penny from adjustmentForLineItemTotal and push to adjustmentForShipping
                    orderAdjustment       = (-lineItemTotal + 0.01m);     // -68 + 0.01 = -67.99
                    adjustmentForShipping = predictLineitemTotal - 0.01m; // -2 - 0.01 = -2.01
                }
                else
                {
                    // this case means: due to PayPal calculation, buyer need to pay more that what Commerce calculate. Because:
                    // sometimes cart total calculated by Commerce is different with
                    // cart total calculated by PalPal, though this difference is very small (~0.01 currency)
                    // We adjust the items total to make up for that, to ensure PayPal shows the same total number with Commerce
                }

                lineItemTotal += orderAdjustment; // re-adjust the lineItemTotal

                paymentDetailItem.Amount = ToPayPalAmount(orderAdjustment, currency);
                paymentDetailItems.Add(paymentDetailItem);
            }

            if (adjustmentForShipping > 0)
            {
                totalShipping += adjustmentForShipping;
            }
            else
            {
                // Shipping discount for this order. You specify this value as a negative number.
                // NOTE:Character length and limitations: Must not exceed $10,000 USD in any currency.
                // No currency symbol. Regardless of currency, decimal separator must be a period (.), and the optional thousands separator must be a comma (,).
                // Equivalent to nine characters maximum for USD.
                // NOTE:You must set the currencyID attribute to one of the three-character currency codes for any of the supported PayPal currencies.
                paymentDetailsType.ShippingDiscount = ToPayPalAmount(adjustmentForShipping, currency);
            }

            paymentDetailsType.OrderTotal    = ToPayPalAmount(totalOrder, currency);
            paymentDetailsType.ShippingTotal = ToPayPalAmount(totalShipping, currency);
            paymentDetailsType.HandlingTotal = ToPayPalAmount(totalHandling, currency);
            paymentDetailsType.TaxTotal      = ToPayPalAmount(totalTax, currency);
            paymentDetailsType.ItemTotal     = ToPayPalAmount(lineItemTotal, currency);

            paymentDetailsType.PaymentDetailsItem = paymentDetailItems;
            paymentDetailsType.ShipToAddress      = AddressHandling.ToAddressType(orderForm.Shipments.First().ShippingAddress);

            if (orderForm.Shipments.Count > 1)
            {
                // (Optional) The value 1 indicates that this payment is associated with multiple shipping addresses. Character length and limitations: Four single-byte numeric characters.
                paymentDetailsType.MultiShipping = "1";
            }

            return(paymentDetailsType);
        }
        /// <summary>
        /// Processes the successful transaction, was called when PayPal.com redirect back.
        /// </summary>
        /// <param name="orderGroup">The order group that was processed.</param>
        /// <param name="payment">The order payment.</param>
        /// <param name="acceptUrl">The redirect url when finished.</param>
        /// <param name="cancelUrl">The redirect url when error happens.</param>
        /// <returns>The url redirection after process.</returns>
        public string ProcessSuccessfulTransaction(IOrderGroup orderGroup, IPayment payment, string acceptUrl, string cancelUrl)
        {
            if (HttpContext.Current == null)
            {
                return(cancelUrl);
            }

            var cart = orderGroup as ICart;

            if (cart == null)
            {
                // return to the shopping cart page immediately and show error messages
                return(ProcessUnsuccessfulTransaction(cancelUrl, Utilities.Translate("CommitTranErrorCartNull")));
            }

            string redirectionUrl;
            var    getDetailRequest = new GetExpressCheckoutDetailsRequestType
            {
                Token = payment.Properties[PayPalExpTokenPropertyName] as string // Add request-specific fields to the request.
            };

            // Execute the API operation and obtain the response.
            var caller             = PayPalAPIHelper.GetPayPalAPICallerServices(_paymentMethodConfiguration);
            var getDetailsResponse = caller.GetExpressCheckoutDetails(new GetExpressCheckoutDetailsReq
            {
                GetExpressCheckoutDetailsRequest = getDetailRequest
            });

            var errorCheck = _payPalAPIHelper.CheckErrors(getDetailsResponse);

            if (!string.IsNullOrEmpty(errorCheck))
            {
                // unsuccessful get detail call
                _logger.Error(errorCheck);
                return(ProcessUnsuccessfulTransaction(cancelUrl, PaymentTransactionFailedMessage));
            }

            var expressCheckoutDetailsResponse = getDetailsResponse.GetExpressCheckoutDetailsResponseDetails;

            // get commerceOrderId from what we put to PayPal instead of getting from cookie
            payment.Properties[PayPalOrderNumberPropertyName] = expressCheckoutDetailsResponse.InvoiceID;

            //process details sent from paypal, changing addresses if required
            var emptyAddressMsg = string.Empty;

            //process billing address
            var payPalBillingAddress = expressCheckoutDetailsResponse.BillingAddress;

            if (payPalBillingAddress != null && AddressHandling.IsAddressChanged(payment.BillingAddress, payPalBillingAddress))
            {
                emptyAddressMsg = _payPalAPIHelper.ProcessOrderAddress(expressCheckoutDetailsResponse.PayerInfo, payPalBillingAddress, payment.BillingAddress, CustomerAddressTypeEnum.Billing, "CommitTranErrorPayPalBillingAddressEmpty");
                if (!string.IsNullOrEmpty(emptyAddressMsg))
                {
                    return(ProcessUnsuccessfulTransaction(cancelUrl, emptyAddressMsg));
                }
            }

            //process shipping address
            var payPalShippingAddress = expressCheckoutDetailsResponse.PaymentDetails[0].ShipToAddress;

            if (payPalShippingAddress != null && AddressHandling.IsAddressChanged(cart.GetFirstShipment().ShippingAddress, payPalShippingAddress))
            {
                //when address was changed on PayPal site, it might cause changing tax value changed and changing order value also.
                var taxValueBefore = cart.GetTaxTotal(_orderGroupCalculator);

                var shippingAddress = orderGroup.CreateOrderAddress("address");

                emptyAddressMsg = _payPalAPIHelper.ProcessOrderAddress(expressCheckoutDetailsResponse.PayerInfo, payPalShippingAddress, shippingAddress, CustomerAddressTypeEnum.Shipping, "CommitTranErrorPayPalShippingAddressEmpty");
                if (!string.IsNullOrEmpty(emptyAddressMsg))
                {
                    return(ProcessUnsuccessfulTransaction(cancelUrl, emptyAddressMsg));
                }

                cart.GetFirstShipment().ShippingAddress = shippingAddress;

                var taxValueAfter = cart.GetTaxTotal(_orderGroupCalculator);
                if (taxValueBefore != taxValueAfter)
                {
                    _orderRepository.Save(cart); // Saving cart to submit order address changed.
                    return(ProcessUnsuccessfulTransaction(cancelUrl, Utilities.Translate("ProcessPaymentTaxValueChangedWarning")));
                }
            }

            // Add request-specific fields to the request.
            // Create the request details object.
            var doExpressChkOutPaymentReqDetails = CreateExpressCheckoutPaymentRequest(getDetailsResponse, orderGroup, payment);

            // Execute the API operation and obtain the response.
            var doCheckOutResponse = caller.DoExpressCheckoutPayment(new DoExpressCheckoutPaymentReq
            {
                DoExpressCheckoutPaymentRequest = new DoExpressCheckoutPaymentRequestType(doExpressChkOutPaymentReqDetails)
            });

            errorCheck = _payPalAPIHelper.CheckErrors(doCheckOutResponse);
            if (!string.IsNullOrEmpty(errorCheck))
            {
                // unsuccessful doCheckout response
                _logger.Error(errorCheck);
                return(ProcessUnsuccessfulTransaction(cancelUrl, PaymentTransactionFailedMessage));
            }

            // everything is fine, this is a flag to tell ProcessPayment know about this case: redirect back from PayPal with accepted payment
            var errorMessages = new List <string>();
            var cartCompleted = DoCompletingCart(cart, errorMessages);

            if (!cartCompleted)
            {
                return(UriUtil.AddQueryString(cancelUrl, "message", string.Join(";", errorMessages.Distinct().ToArray())));
            }

            // Place order
            var purchaseOrder = MakePurchaseOrder(doCheckOutResponse, cart, payment);

            redirectionUrl = CreateRedirectionUrl(purchaseOrder, acceptUrl, payment.BillingAddress.Email);

            _logger.Information($"PayPal transaction succeeds, redirect end user to {redirectionUrl}");
            return(redirectionUrl);
        }