/// <summary>
        /// Gets API credentials
        /// </summary>
        /// <returns></returns>
        public static CustomSecurityHeaderType GetPaypalApiCredentials(PayPalApiSettingsBase settings)
        {
            CustomSecurityHeaderType customSecurityHeaderType = new CustomSecurityHeaderType();

            customSecurityHeaderType.Credentials = new UserIdPasswordType();
            customSecurityHeaderType.Credentials.Username = settings.ApiAccountName;
            customSecurityHeaderType.Credentials.Password = settings.ApiAccountPassword;
            customSecurityHeaderType.Credentials.Signature = settings.Signature;
            customSecurityHeaderType.Credentials.Subject = "";

            return customSecurityHeaderType;
        }
        public PayPalResponse GetPayment(PayPalApiSettingsBase settings, PayPalSessionData session)
        {
            var result = CallApi("GET", "/v1/payments/payment/" + session.PaymentId, session.AccessToken, settings, null);

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            return result;
        }
        /// <remarks>return 503 (HttpStatusCode.ServiceUnavailable) to ask paypal to resend it at later time again</remarks>
        public HttpStatusCode ProcessWebhook(
			PayPalApiSettingsBase settings,
			NameValueCollection headers,
			string rawJson,
			string providerSystemName)
        {
            if (rawJson.IsEmpty())
                return HttpStatusCode.OK;

            dynamic json = JObject.Parse(rawJson);
            var eventType = (string)json.event_type;

            //foreach (var key in headers.AllKeys)"{0}: {1}".FormatInvariant(key, headers[key]).Dump();
            //string data = JsonConvert.SerializeObject(json, Formatting.Indented);data.Dump();

            // validating against PayPal SDK failing using sandbox, so better we do not use it:
            //var apiContext = new global::PayPal.Api.APIContext
            //{
            //	AccessToken = "I do not have one here",
            //	Config = new Dictionary<string, string>
            //		{
            //			{ "mode", settings.UseSandbox ? "sandbox" : "live" },
            //			{ "clientId", settings.ClientId },
            //			{ "clientSecret", settings.Secret },
            //			{ "webhook.id", setting.WebhookId },
            //		}
            //};
            //var result = global::PayPal.Api.WebhookEvent.ValidateReceivedEvent(apiContext, headers, rawJson, webhookId);
            //}

            var paymentId = (string)json.resource.parent_payment;
            if (paymentId.IsEmpty())
            {
                LogError(null, T("Plugins.SmartStore.PayPal.FoundOrderForPayment", 0, "".NaIfEmpty()), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true);
                return HttpStatusCode.OK;
            }

            var orders = _orderRepository.Value.Table
                .Where(x => x.PaymentMethodSystemName == providerSystemName && x.AuthorizationTransactionCode == paymentId)
                .ToList();

            if (orders.Count != 1)
            {
                LogError(null, T("Plugins.SmartStore.PayPal.FoundOrderForPayment", orders.Count, paymentId), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true);
                return HttpStatusCode.OK;
            }

            var order = orders.First();
            var store = _services.StoreService.GetStoreById(order.StoreId);

            var total = decimal.Zero;
            var currency = (string)json.resource.amount.currency;
            var primaryCurrency = store.PrimaryStoreCurrency.CurrencyCode;

            if (!primaryCurrency.IsCaseInsensitiveEqual(currency))
            {
                LogError(null, T("Plugins.SmartStore.PayPal.CurrencyNotEqual", currency.NaIfEmpty(), primaryCurrency), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true);
                return HttpStatusCode.OK;
            }

            eventType = eventType.Substring(eventType.LastIndexOf('.') + 1);

            var newPaymentStatus = GetPaymentStatus(eventType, "authorization", order.PaymentStatus);

            var isValidTotal = decimal.TryParse((string)json.resource.amount.total, NumberStyles.Currency, CultureInfo.InvariantCulture, out total);

            if (newPaymentStatus == PaymentStatus.Refunded && (Math.Abs(order.OrderTotal) - Math.Abs(total)) > decimal.Zero)
            {
                newPaymentStatus = PaymentStatus.PartiallyRefunded;
            }

            switch (newPaymentStatus)
            {
                case PaymentStatus.Pending:
                    break;
                case PaymentStatus.Authorized:
                    if (_orderProcessingService.CanMarkOrderAsAuthorized(order))
                        _orderProcessingService.MarkAsAuthorized(order);
                    break;
                case PaymentStatus.Paid:
                    if (_orderProcessingService.CanMarkOrderAsPaid(order))
                        _orderProcessingService.MarkOrderAsPaid(order);
                    break;
                case PaymentStatus.Refunded:
                    if (_orderProcessingService.CanRefundOffline(order))
                        _orderProcessingService.RefundOffline(order);
                    break;
                case PaymentStatus.PartiallyRefunded:
                    if (_orderProcessingService.CanPartiallyRefundOffline(order, Math.Abs(total)))
                        _orderProcessingService.PartiallyRefundOffline(order, Math.Abs(total));
                    break;
                case PaymentStatus.Voided:
                    if (_orderProcessingService.CanVoidOffline(order))
                        _orderProcessingService.VoidOffline(order);
                    break;
            }

            AddOrderNote(settings, order, (string)ToInfoString(json), true);

            return HttpStatusCode.OK;
        }
        public PayPalResponse EnsureAccessToken(PayPalSessionData session, PayPalApiSettingsBase settings)
        {
            if (session.AccessToken.IsEmpty() || DateTime.UtcNow >= session.TokenExpiration)
            {
                var result = CallApi("POST", "/v1/oauth2/token", null, settings, "grant_type=client_credentials");

                if (result.Success)
                {
                    session.AccessToken = (string)result.Json.access_token;

                    var expireSeconds = ((string)result.Json.expires_in).ToInt(5 * 60);

                    session.TokenExpiration = DateTime.UtcNow.AddSeconds(expireSeconds);
                }
                else
                {
                    return result;
                }
            }

            return new PayPalResponse
            {
                Success = session.AccessToken.HasValue()
            };
        }
        public PayPalResponse ExecutePayment(PayPalApiSettingsBase settings, PayPalSessionData session)
        {
            var data = new Dictionary<string, object>();
            data.Add("payer_id", session.PayerId);

            var result = CallApi("POST", "/v1/payments/payment/{0}/execute".FormatInvariant(session.PaymentId), session.AccessToken, settings, JsonConvert.SerializeObject(data));

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;

                //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + result.Json.ToString());
            }

            return result;
        }
        public PayPalResponse DeleteCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session)
        {
            var result = CallApi("DELETE", "/v1/payment-experience/web-profiles/" + settings.ExperienceProfileId, session.AccessToken, settings, null);

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            return result;
        }
        public PayPalResponse DeleteWebhook(PayPalApiSettingsBase settings, PayPalSessionData session)
        {
            var result = CallApi("DELETE", "/v1/notifications/webhooks/" + settings.WebhookId, session.AccessToken, settings, null);

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            return result;
        }
        public PayPalResponse CreatePayment(
			PayPalApiSettingsBase settings,
			PayPalSessionData session,
			List<OrganizedShoppingCartItem> cart,
			string providerSystemName,
			string returnUrl,
			string cancelUrl)
        {
            var store = _services.StoreContext.CurrentStore;
            var customer = _services.WorkContext.CurrentCustomer;
            var language = _services.WorkContext.WorkingLanguage;
            var currencyCode = store.PrimaryStoreCurrency.CurrencyCode;

            var dateOfBirth = customer.GetAttribute<DateTime?>(SystemCustomerAttributeNames.DateOfBirth);

            Discount orderAppliedDiscount;
            List<AppliedGiftCard> appliedGiftCards;
            int redeemedRewardPoints = 0;
            decimal redeemedRewardPointsAmount;
            decimal orderDiscountInclTax;
            decimal totalOrderItems = decimal.Zero;

            var includingTax = (_services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.IncludingTax);

            var shipping = (_orderTotalCalculationService.GetShoppingCartShippingTotal(cart) ?? decimal.Zero);

            var paymentFee = _paymentService.GetAdditionalHandlingFee(cart, providerSystemName);

            var total = (_orderTotalCalculationService.GetShoppingCartTotal(cart, out orderDiscountInclTax, out orderAppliedDiscount, out appliedGiftCards,
                out redeemedRewardPoints, out redeemedRewardPointsAmount) ?? decimal.Zero);

            var data = new Dictionary<string, object>();
            var redirectUrls = new Dictionary<string, object>();
            var payer = new Dictionary<string, object>();
            var payerInfo = new Dictionary<string, object>();
            var transaction = new Dictionary<string, object>();
            var amount = new Dictionary<string, object>();
            var amountDetails = new Dictionary<string, object>();
            var items = new List<Dictionary<string, object>>();
            var itemList = new Dictionary<string, object>();

            // "PayPal PLUS only supports transaction type “Sale” (instant settlement)"
            if (providerSystemName == PayPalPlusProvider.SystemName)
                data.Add("intent", "sale");
            else
                data.Add("intent", settings.TransactMode == TransactMode.AuthorizeAndCapture ? "sale" : "authorize");

            if (settings.ExperienceProfileId.HasValue())
                data.Add("experience_profile_id", settings.ExperienceProfileId);

            // redirect urls
            if (returnUrl.HasValue())
                redirectUrls.Add("return_url", returnUrl);

            if (cancelUrl.HasValue())
                redirectUrls.Add("cancel_url", cancelUrl);

            if (redirectUrls.Any())
                data.Add("redirect_urls", redirectUrls);

            // payer, payer_info
            if (dateOfBirth.HasValue)
            {
                payerInfo.Add("birth_date", dateOfBirth.Value.ToString("yyyy-MM-dd"));
            }
            if (customer.BillingAddress != null)
            {
                payerInfo.Add("billing_address", CreateAddress(customer.BillingAddress, false));
            }

            payer.Add("payment_method", "paypal");
            payer.Add("payer_info", payerInfo);
            data.Add("payer", payer);

            // line items
            foreach (var item in cart)
            {
                decimal unitPriceTaxRate = decimal.Zero;
                decimal unitPrice = _priceCalculationService.GetUnitPrice(item, true);
                decimal productPrice = _taxService.GetProductPrice(item.Item.Product, unitPrice, includingTax, customer, out unitPriceTaxRate);

                var line = new Dictionary<string, object>();
                line.Add("quantity", item.Item.Quantity);
                line.Add("name", item.Item.Product.GetLocalized(x => x.Name, language.Id, true, false).Truncate(127));
                line.Add("price", productPrice.FormatInvariant());
                line.Add("currency", currencyCode);
                line.Add("sku", item.Item.Product.Sku.Truncate(50));
                items.Add(line);

                totalOrderItems += (productPrice * item.Item.Quantity);
            }

            var itemsPlusMisc = (totalOrderItems + shipping + paymentFee);

            if (total != itemsPlusMisc)
            {
                var line = new Dictionary<string, object>();
                line.Add("quantity", "1");
                line.Add("name", T("Plugins.SmartStore.PayPal.Other").Text.Truncate(127));
                line.Add("price", (total - itemsPlusMisc).FormatInvariant());
                line.Add("currency", currencyCode);
                items.Add(line);

                totalOrderItems += (total - itemsPlusMisc);
            }

            itemList.Add("items", items);
            if (customer.ShippingAddress != null)
            {
                itemList.Add("shipping_address", CreateAddress(customer.ShippingAddress, true));
            }

            // transactions
            amountDetails.Add("shipping", shipping.FormatInvariant());
            amountDetails.Add("subtotal", totalOrderItems.FormatInvariant());
            if (!includingTax)
            {
                // "To avoid rounding errors we recommend not submitting tax amounts on line item basis.
                // Calculated tax amounts for the entire shopping basket may be submitted in the amount objects.
                // In this case the item amounts will be treated as amounts excluding tax.
                // In a B2C scenario, where taxes are included, no taxes should be submitted to PayPal."

                SortedDictionary<decimal, decimal> taxRates = null;
                var taxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out taxRates);

                amountDetails.Add("tax", taxTotal.FormatInvariant());
            }
            if (paymentFee != decimal.Zero)
            {
                amountDetails.Add("handling_fee", paymentFee.FormatInvariant());
            }

            amount.Add("total", total.FormatInvariant());
            amount.Add("currency", currencyCode);
            amount.Add("details", amountDetails);

            transaction.Add("amount", amount);
            transaction.Add("item_list", itemList);
            transaction.Add("invoice_number", session.OrderGuid.ToString());

            data.Add("transactions", new List<Dictionary<string, object>> { transaction });

            var result = CallApi("POST", "/v1/payments/payment", session.AccessToken, settings, JsonConvert.SerializeObject(data));

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : ""));

            return result;
        }
        public PayPalResponse CreateWebhook(PayPalApiSettingsBase settings, PayPalSessionData session, string url)
        {
            var data = new Dictionary<string, object>();
            var events = new List<Dictionary<string, object>>();

            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.AUTHORIZATION.VOIDED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.CAPTURE.COMPLETED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.CAPTURE.DENIED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.CAPTURE.PENDING" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.CAPTURE.REFUNDED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.CAPTURE.REVERSED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.SALE.COMPLETED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.SALE.DENIED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.SALE.PENDING" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.SALE.REFUNDED" } });
            events.Add(new Dictionary<string, object> { { "name", "PAYMENT.SALE.REVERSED" } });

            data.Add("url", url);
            data.Add("event_types", events);

            var result = CallApi("POST", "/v1/notifications/webhooks", session.AccessToken, settings, JsonConvert.SerializeObject(data));

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            return result;
        }
        public PayPalResponse Capture(PayPalApiSettingsBase settings, PayPalSessionData session, CapturePaymentRequest request)
        {
            var data = new Dictionary<string, object>();
            //var isAuthorize = request.Order.AuthorizationTransactionCode.IsCaseInsensitiveEqual("authorize");

            var path = "/v1/payments/authorization/{0}/capture".FormatInvariant(request.Order.AuthorizationTransactionId);

            var store = _services.StoreService.GetStoreById(request.Order.StoreId);

            var amount = new Dictionary<string, object>();
            amount.Add("total", request.Order.OrderTotal.FormatInvariant());
            amount.Add("currency", store.PrimaryStoreCurrency.CurrencyCode);

            data.Add("amount", amount);

            var result = CallApi("POST", path, session.AccessToken, settings, JsonConvert.SerializeObject(data));

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            return result;
        }
        public PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data)
        {
            var isJson = (data.HasValue() && data.StartsWith("{"));
            var encoding = (isJson ? Encoding.UTF8 : Encoding.ASCII);
            var result = new PayPalResponse();
            HttpWebResponse webResponse = null;

            var url = GetApiUrl(settings.UseSandbox) + path.EnsureStartsWith("/");

            if (method.IsCaseInsensitiveEqual("GET") && data.HasValue())
                url = url.EnsureEndsWith("?") + data;

            if (settings.SecurityProtocol.HasValue)
                ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value;

            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = method;
            request.Accept = "application/json";
            request.ContentType = (isJson ? "application/json" : "application/x-www-form-urlencoded");

            try
            {
                if (HttpContext.Current != null && HttpContext.Current.Request != null)
                    request.UserAgent = HttpContext.Current.Request.UserAgent;
                else
                    request.UserAgent = Plugin.SystemName;
            }
            catch { }

            if (path.EmptyNull().EndsWith("/token"))
            {
                // see https://github.com/paypal/sdk-core-dotnet/blob/master/Source/SDK/OAuthTokenCredential.cs
                byte[] credentials = Encoding.UTF8.GetBytes("{0}:{1}".FormatInvariant(settings.ClientId, settings.Secret));

                request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(credentials));
            }
            else
            {
                request.Headers["Authorization"] = "Bearer " + accessToken.EmptyNull();
            }

            if (data.HasValue() && (method.IsCaseInsensitiveEqual("POST") || method.IsCaseInsensitiveEqual("PUT") || method.IsCaseInsensitiveEqual("PATCH")))
            {
                byte[] bytes = encoding.GetBytes(data);

                request.ContentLength = bytes.Length;

                using (var stream = request.GetRequestStream())
                {
                    stream.Write(bytes, 0, bytes.Length);
                }
            }
            else
            {
                request.ContentLength = 0;
            }

            try
            {
                webResponse = request.GetResponse() as HttpWebResponse;
                result.Success = ((int)webResponse.StatusCode < 400);
            }
            catch (WebException wexc)
            {
                result.Success = false;
                result.ErrorMessage = wexc.ToString();
                webResponse = wexc.Response as HttpWebResponse;
            }
            catch (Exception exception)
            {
                result.Success = false;
                result.ErrorMessage = exception.ToString();
                LogError(exception);
            }

            try
            {
                if (webResponse != null)
                {
                    using (var reader = new StreamReader(webResponse.GetResponseStream(), Encoding.UTF8))
                    {
                        var rawResponse = reader.ReadToEnd();
                        if (rawResponse.HasValue())
                        {
                            if (webResponse.ContentType.IsCaseInsensitiveEqual("application/json"))
                            {
                                if (rawResponse.StartsWith("["))
                                    result.Json = JArray.Parse(rawResponse);
                                else
                                    result.Json = JObject.Parse(rawResponse);

                                if (result.Json != null)
                                {
                                    if (!result.Success)
                                    {
                                        var name = (string)result.Json.name;
                                        var message = (string)result.Json.message;

                                        if (name.IsEmpty())
                                            name = (string)result.Json.error;

                                        if (message.IsEmpty())
                                            message = (string)result.Json.error_description;

                                        result.ErrorMessage = "{0} ({1}).".FormatInvariant(message.NaIfEmpty(), name.NaIfEmpty());
                                    }
                                }
                            }
                            else if (!result.Success)
                            {
                                result.ErrorMessage = rawResponse;
                            }
                        }
                    }

                    if (!result.Success)
                    {
                        if (result.ErrorMessage.IsEmpty())
                            result.ErrorMessage = webResponse.StatusDescription;

                        LogError(null, result.ErrorMessage, result.Json == null ? null : result.Json.ToString(), false);
                    }
                }
            }
            catch (Exception exception)
            {
                LogError(exception);
            }
            finally
            {
                if (webResponse != null)
                {
                    webResponse.Close();
                    webResponse.Dispose();
                }
            }

            return result;
        }
        public PayPalResponse Void(PayPalApiSettingsBase settings, PayPalSessionData session, VoidPaymentRequest request)
        {
            var path = "/v1/payments/authorization/{0}/void".FormatInvariant(request.Order.AuthorizationTransactionId);

            var result = CallApi("POST", path, session.AccessToken, settings, null);

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            return result;
        }
        public PayPalResponse UpsertCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session, Store store)
        {
            PayPalResponse result;
            var name = store.Name;
            var logo = _pictureService.Value.GetPictureById(store.LogoPictureId);
            var path = "/v1/payment-experience/web-profiles";

            var data = new Dictionary<string, object>();
            var presentation = new Dictionary<string, object>();
            var inpuFields = new Dictionary<string, object>();

            // find existing profile id, only one profile per profile name possible
            if (settings.ExperienceProfileId.IsEmpty())
            {
                result = CallApi("GET", path, session.AccessToken, settings, null);
                if (result.Success && result.Json != null)
                {
                    foreach (var profile in result.Json)
                    {
                        var profileName = (string)profile.name;
                        if (profileName.IsCaseInsensitiveEqual(name))
                        {
                            settings.ExperienceProfileId = (string)profile.id;
                            break;
                        }
                    }
                }
            }

            presentation.Add("brand_name", name);
            presentation.Add("locale_code", _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToUpper());

            if (logo != null)
                presentation.Add("logo_image", _pictureService.Value.GetPictureUrl(logo, showDefaultPicture: false, storeLocation: store.Url));

            inpuFields.Add("allow_note", false);
            inpuFields.Add("no_shipping", 0);
            inpuFields.Add("address_override", 1);

            data.Add("name", name);
            data.Add("presentation", presentation);
            data.Add("input_fields", inpuFields);

            if (settings.ExperienceProfileId.HasValue())
                path = string.Concat(path, "/", HttpUtility.UrlPathEncode(settings.ExperienceProfileId));

            result = CallApi(settings.ExperienceProfileId.HasValue() ? "PUT" : "POST", path, session.AccessToken, settings, JsonConvert.SerializeObject(data));

            if (result.Success)
            {
                if (result.Json != null)
                    result.Id = (string)result.Json.id;
                else
                    result.Id = settings.ExperienceProfileId;
            }

            return result;
        }
        public PayPalResponse Refund(PayPalApiSettingsBase settings, PayPalSessionData session, RefundPaymentRequest request)
        {
            var data = new Dictionary<string, object>();
            var store = _services.StoreService.GetStoreById(request.Order.StoreId);
            var isSale = request.Order.AuthorizationTransactionResult.Contains("(sale)");

            var path = "/v1/payments/{0}/{1}/refund".FormatInvariant(isSale ? "sale" : "capture", request.Order.CaptureTransactionId);

            var amount = new Dictionary<string, object>();
            amount.Add("total", request.AmountToRefund.FormatInvariant());
            amount.Add("currency", store.PrimaryStoreCurrency.CurrencyCode);

            data.Add("amount", amount);

            var result = CallApi("POST", path, session.AccessToken, settings, data.Any() ? JsonConvert.SerializeObject(data) : null);

            if (result.Success && result.Json != null)
            {
                result.Id = (string)result.Json.id;
            }

            //Logger.InsertLog(LogLevel.Information, "PayPal Refund", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : ""));

            return result;
        }