public override async Task <ApiResult> RefundPaymentAsync(PaymentProviderContext <BamboraCheckoutSettings> ctx)
        {
            try
            {
                var clientConfig = GetBamboraClientConfig(ctx.Settings);
                var client       = new BamboraClient(clientConfig);

                var transactionResp = await client.CreditTransactionAsync(ctx.Order.TransactionInfo.TransactionId, new BamboraAmountRequest
                {
                    Amount = (int)AmountToMinorUnits(ctx.Order.TransactionInfo.AmountAuthorized.Value)
                });

                if (transactionResp.Meta.Result)
                {
                    return(new ApiResult
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = ctx.Order.TransactionInfo.TransactionId,
                            PaymentStatus = PaymentStatus.Refunded
                        }
                    });
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Bambora - RefundPayment");
            }

            return(ApiResult.Empty);
        }
        public override async Task <ApiResult> CancelPaymentAsync(PaymentProviderContext <BamboraCheckoutSettings> ctx)
        {
            try
            {
                var clientConfig = GetBamboraClientConfig(ctx.Settings);
                var client       = new BamboraClient(clientConfig);

                var transactionResp = await client.DeleteTransactionAsync(ctx.Order.TransactionInfo.TransactionId);

                if (transactionResp.Meta.Result)
                {
                    return(new ApiResult
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = ctx.Order.TransactionInfo.TransactionId,
                            PaymentStatus = PaymentStatus.Cancelled
                        }
                    });
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Bambora - CancelPayment");
            }

            return(ApiResult.Empty);
        }
Example #3
0
        public override string GetContinueUrl(PaymentProviderContext <DibsSettings> ctx)
        {
            ctx.Settings.MustNotBeNull("ctx.Settings");
            ctx.Settings.ContinueUrl.MustNotBeNull("ctx.Settings.ContinueUrl");

            return(ctx.Settings.ContinueUrl);
        }
        public override string GetErrorUrl(PaymentProviderContext <TSettings> ctx)
        {
            ctx.Settings.MustNotBeNull("settings");
            ctx.Settings.ErrorUrl.MustNotBeNull("settings.ErrorUrl");

            return(ctx.Settings.ErrorUrl);
        }
Example #5
0
        public override async Task <ApiResult> RefundPaymentAsync(PaymentProviderContext <DibsSettings> ctx)
        {
            try
            {
                var currency     = Vendr.Services.CurrencyService.GetCurrency(ctx.Order.CurrencyId);
                var currencyCode = currency.Code.ToUpperInvariant();

                // Ensure currency has valid ISO 4217 code
                if (!Iso4217.CurrencyCodes.ContainsKey(currencyCode))
                {
                    throw new Exception("Currency must be a valid ISO 4217 currency code: " + currency.Name);
                }

                var strCurrency = Iso4217.CurrencyCodes[currencyCode].ToString(CultureInfo.InvariantCulture);
                var strAmount   = AmountToMinorUnits(ctx.Order.TransactionInfo.AmountAuthorized.Value).ToString("0", CultureInfo.InvariantCulture);

                // MD5(key2 + MD5(key1 + "merchant=<merchant>&ctx.Orderid=<ctx.Orderid>&transact=<transact>&amount=<amount>"))
                var md5Check = $"merchant={ctx.Settings.MerchantId}&ctx.Orderid={ctx.Order.OrderNumber}&transact={ctx.Order.TransactionInfo.TransactionId}&amount={strAmount}";

                var response = await $"https://payment.architrade.com/cgi-adm/refund.cgi"
                               .WithBasicAuth(ctx.Settings.ApiUsername, ctx.Settings.ApiPassword)
                               .PostUrlEncodedAsync(new
                {
                    merchant  = ctx.Settings.MerchantId,
                    orderid   = ctx.Order.OrderNumber,
                    transact  = ctx.Order.TransactionInfo.TransactionId,
                    amount    = strAmount,
                    currency  = strCurrency,
                    textreply = "1",
                    md5key    = MD5Hash(ctx.Settings.MD5Key2 + MD5Hash(ctx.Settings.MD5Key1 + md5Check))
                })
                               .ReceiveString();

                var responseParams = HttpUtility.ParseQueryString(response);
                var result         = responseParams["result"];

                if (result == "0") // 0 == Accepted
                {
                    return(new ApiResult()
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = ctx.Order.TransactionInfo.TransactionId,
                            PaymentStatus = PaymentStatus.Refunded
                        }
                    });
                }
                else
                {
                    _logger.Warn($"Dibs [{ctx.Order.OrderNumber}] - Error making API request - error message: {result}");
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Dibs - CapturePayment");
            }

            return(ApiResult.Empty);
        }
        public override async Task <ApiResult> RefundPaymentAsync(PaymentProviderContext <StripeCheckoutSettings> ctx)
        {
            try
            {
                // We can only refund a captured charge, so make sure we have one
                // otherwise there is nothing we can do
                var chargeId = ctx.Order.Properties["stripeChargeId"];
                if (string.IsNullOrWhiteSpace(chargeId))
                {
                    return(null);
                }

                var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;

                ConfigureStripe(secretKey);

                var refundService       = new RefundService();
                var refundCreateOptions = new RefundCreateOptions()
                {
                    Charge = chargeId
                };

                var refund = refundService.Create(refundCreateOptions);
                var charge = refund.Charge ?? await new ChargeService().GetAsync(refund.ChargeId);

                // If we have a subscription then we'll cancel it as refunding an ctx.Order
                // should effecitvely undo any purchase
                if (!string.IsNullOrWhiteSpace(ctx.Order.Properties["stripeSubscriptionId"]))
                {
                    var subscriptionService = new SubscriptionService();
                    var subscription        = await subscriptionService.GetAsync(ctx.Order.Properties["stripeSubscriptionId"]);

                    if (subscription != null)
                    {
                        subscriptionService.Cancel(ctx.Order.Properties["stripeSubscriptionId"], new SubscriptionCancelOptions
                        {
                            InvoiceNow = false,
                            Prorate    = false
                        });
                    }
                }

                return(new ApiResult()
                {
                    TransactionInfo = new TransactionInfoUpdate()
                    {
                        TransactionId = GetTransactionId(charge),
                        PaymentStatus = GetPaymentStatus(charge)
                    }
                });
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Stripe - RefundPayment");
            }

            return(ApiResult.Empty);
        }
        public override async Task <ApiResult> FetchPaymentStatusAsync(PaymentProviderContext <StripeCheckoutSettings> ctx)
        {
            try
            {
                var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;

                ConfigureStripe(secretKey);

                // See if we have a payment intent to work from
                var paymentIntentId = ctx.Order.Properties["stripePaymentIntentId"];
                if (!string.IsNullOrWhiteSpace(paymentIntentId))
                {
                    var paymentIntentService = new PaymentIntentService();
                    var paymentIntent        = await paymentIntentService.GetAsync(paymentIntentId, new PaymentIntentGetOptions
                    {
                        Expand = new List <string>(new[] {
                            "review"
                        })
                    });

                    return(new ApiResult()
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = GetTransactionId(paymentIntent),
                            PaymentStatus = GetPaymentStatus(paymentIntent)
                        }
                    });
                }

                // No payment intent, so look for a charge
                var chargeId = ctx.Order.Properties["stripeChargeId"];
                if (!string.IsNullOrWhiteSpace(chargeId))
                {
                    var chargeService = new ChargeService();
                    var charge        = await chargeService.GetAsync(chargeId);

                    return(new ApiResult()
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = GetTransactionId(charge),
                            PaymentStatus = GetPaymentStatus(charge)
                        }
                    });
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Stripe - FetchPaymentStatus");
            }

            return(ApiResult.Empty);
        }
Example #8
0
        protected StripeTaxRate GetOrCreateStripeTaxRate(PaymentProviderContext <TSettings> ctx, string taxName, decimal percentage, bool inclusive)
        {
            var taxRateService = new TaxRateService();
            var stripeTaxRates = new List <StripeTaxRate>();

            if (ctx.AdditionalData.ContainsKey("Vendr_StripeTaxRates"))
            {
                stripeTaxRates = (List <StripeTaxRate>)ctx.AdditionalData["Vendr_StripeTaxRates"];
            }

            if (stripeTaxRates.Count > 0)
            {
                var taxRate = GetStripeTaxRate(stripeTaxRates, taxName, percentage, inclusive);
                if (taxRate != null)
                {
                    return(taxRate);
                }
            }

            stripeTaxRates = taxRateService.List(new TaxRateListOptions {
                Active = true
            }).ToList();

            if (ctx.AdditionalData.ContainsKey("Vendr_StripeTaxRates"))
            {
                ctx.AdditionalData["Vendr_StripeTaxRates"] = stripeTaxRates;
            }
            else
            {
                ctx.AdditionalData.Add("Vendr_StripeTaxRates", stripeTaxRates);
            }

            if (stripeTaxRates.Count > 0)
            {
                var taxRate = GetStripeTaxRate(stripeTaxRates, taxName, percentage, inclusive);
                if (taxRate != null)
                {
                    return(taxRate);
                }
            }

            var newTaxRate = taxRateService.Create(new TaxRateCreateOptions
            {
                DisplayName = taxName,
                Percentage  = percentage,
                Inclusive   = inclusive,
            });

            stripeTaxRates.Add(newTaxRate);

            ctx.AdditionalData["Vendr_StripeTaxRates"] = stripeTaxRates;

            return(newTaxRate);
        }
Example #9
0
        public override async Task <ApiResult> FetchPaymentStatusAsync(PaymentProviderContext <DibsSettings> ctx)
        {
            try
            {
                var response = await $"https://@payment.architrade.com/cgi-adm/payinfo.cgi"
                               .WithBasicAuth(ctx.Settings.ApiUsername, ctx.Settings.ApiPassword)
                               .PostUrlEncodedAsync(new
                {
                    transact = ctx.Order.TransactionInfo.TransactionId
                })
                               .ReceiveString();

                var responseParams = HttpUtility.ParseQueryString(response);
                var status         = responseParams["status"];

                var paymentStatus = PaymentStatus.Initialized;

                switch (status)
                {
                case "2":
                    paymentStatus = PaymentStatus.Authorized;
                    break;

                case "5":
                    paymentStatus = PaymentStatus.Captured;
                    break;

                case "6":
                    paymentStatus = PaymentStatus.Cancelled;
                    break;

                case "11":
                    paymentStatus = PaymentStatus.Refunded;
                    break;
                }

                return(new ApiResult()
                {
                    TransactionInfo = new TransactionInfoUpdate()
                    {
                        TransactionId = ctx.Order.TransactionInfo.TransactionId,
                        PaymentStatus = paymentStatus
                    }
                });
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Dibs - FetchPaymentStatus");
            }

            return(ApiResult.Empty);
        }
Example #10
0
        public override async Task <ApiResult> CapturePaymentAsync(PaymentProviderContext <DibsSettings> ctx)
        {
            try
            {
                var strAmount = AmountToMinorUnits(ctx.Order.TransactionInfo.AmountAuthorized.Value).ToString("0", CultureInfo.InvariantCulture);

                // MD5(key2 + MD5(key1 + "merchant=<merchant>&ctx.Orderid=<ctx.Orderid>&transact=<transact>&amount=<amount>"))
                var md5Check = $"merchant={ctx.Settings.MerchantId}&ctx.Orderid={ctx.Order.OrderNumber}&transact={ctx.Order.TransactionInfo.TransactionId}&amount={strAmount}";

                var response = await $"https://payment.architrade.com/cgi-bin/capture.cgi"
                               .PostUrlEncodedAsync(new
                {
                    merchant  = ctx.Settings.MerchantId,
                    orderid   = ctx.Order.OrderNumber,
                    transact  = ctx.Order.TransactionInfo.TransactionId,
                    amount    = strAmount,
                    textreply = "1",
                    md5key    = MD5Hash(ctx.Settings.MD5Key2 + MD5Hash(ctx.Settings.MD5Key1 + md5Check))
                })
                               .ReceiveString();

                var responseParams = HttpUtility.ParseQueryString(response);
                var result         = responseParams["result"];

                if (result == "0") // 0 == Accepted
                {
                    return(new ApiResult()
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = ctx.Order.TransactionInfo.TransactionId,
                            PaymentStatus = PaymentStatus.Captured
                        }
                    });
                }
                else
                {
                    _logger.Warn($"Dibs [{ctx.Order.OrderNumber}] - Error making API request - error message: {result}");
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Dibs - CapturePayment");
            }

            return(ApiResult.Empty);
        }
        public override async Task <ApiResult> CapturePaymentAsync(PaymentProviderContext <StripeCheckoutSettings> ctx)
        {
            // NOTE: Subscriptions aren't currently abled to be "authorized" so the capture
            // routine shouldn't be relevant for subscription payments at this point

            try
            {
                // We can only capture a payment intent, so make sure we have one
                // otherwise there is nothing we can do
                var paymentIntentId = ctx.Order.Properties["stripePaymentIntentId"];
                if (string.IsNullOrWhiteSpace(paymentIntentId))
                {
                    return(null);
                }

                var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;

                ConfigureStripe(secretKey);

                var paymentIntentService = new PaymentIntentService();
                var paymentIntentOptions = new PaymentIntentCaptureOptions
                {
                    AmountToCapture = AmountToMinorUnits(ctx.Order.TransactionInfo.AmountAuthorized.Value)
                };
                var paymentIntent = await paymentIntentService.CaptureAsync(paymentIntentId, paymentIntentOptions);

                return(new ApiResult()
                {
                    TransactionInfo = new TransactionInfoUpdate()
                    {
                        TransactionId = GetTransactionId(paymentIntent),
                        PaymentStatus = GetPaymentStatus(paymentIntent)
                    },
                    MetaData = new Dictionary <string, string>
                    {
                        { "stripeChargeId", GetTransactionId(paymentIntent) },
                        { "stripeCardCountry", paymentIntent.Charges?.Data?.FirstOrDefault()?.PaymentMethodDetails?.Card?.Country }
                    }
                });
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Stripe - CapturePayment");
            }

            return(ApiResult.Empty);
        }
Example #12
0
        public override Task <PaymentFormResult> GenerateFormAsync(PaymentProviderContext <DibsSettings> ctx)
        {
            var currency     = Vendr.Services.CurrencyService.GetCurrency(ctx.Order.CurrencyId);
            var currencyCode = currency.Code.ToUpperInvariant();

            // Ensure currency has valid ISO 4217 code
            if (!Iso4217.CurrencyCodes.ContainsKey(currencyCode))
            {
                throw new Exception("Currency must be a valid ISO 4217 currency code: " + currency.Name);
            }

            var strCurrency = Iso4217.CurrencyCodes[currencyCode].ToString(CultureInfo.InvariantCulture);
            var orderAmount = AmountToMinorUnits(ctx.Order.TransactionAmount.Value).ToString("0", CultureInfo.InvariantCulture);

            var payTypes = ctx.Settings.PayTypes?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                           .Where(x => !string.IsNullOrWhiteSpace(x))
                           .Select(s => s.Trim())
                           .ToArray();

            // MD5(key2 + MD5(key1 + "merchant=<merchant>&ctx.Orderid=<ctx.Orderid> &currency=<cur>&amount=<amount>"))
            var md5Check = $"merchant={ctx.Settings.MerchantId}&ctx.Orderid={ctx.Order.OrderNumber}&currency={strCurrency}&amount={orderAmount}";
            var md5Hash  = MD5Hash(ctx.Settings.MD5Key2 + MD5Hash(ctx.Settings.MD5Key1 + md5Check));

            // Parse language - default language is Danish.
            Enum.TryParse(ctx.Settings.Lang, true, out DibsLang lang);

            return(Task.FromResult(new PaymentFormResult()
            {
                Form = new PaymentForm("https://payment.architrade.com/paymentweb/start.action", PaymentFormMethod.Post)
                       .WithInput("ctx.Orderid", ctx.Order.OrderNumber)
                       .WithInput("merchant", ctx.Settings.MerchantId)
                       .WithInput("amount", orderAmount)
                       .WithInput("currency", strCurrency)
                       .WithInput("accepturl", ctx.Urls.ContinueUrl)
                       .WithInput("cancelurl", ctx.Urls.CancelUrl)
                       .WithInput("callbackurl", ctx.Urls.CallbackUrl)
                       .WithInput("lang", lang.ToString())
                       .WithInputIf("paytype", payTypes != null && payTypes.Length > 0, string.Join(",", payTypes))
                       .WithInputIf("capturenow", ctx.Settings.Capture, "1")
                       .WithInputIf("calcfee", ctx.Settings.CalcFee, "1")
                       .WithInputIf("decorator", !string.IsNullOrWhiteSpace(ctx.Settings.Decorator), ctx.Settings.Decorator)
                       .WithInputIf("test", ctx.Settings.TestMode, "1")
                       .WithInput("md5key", md5Hash)
            }));
        }
Example #13
0
        public override async Task <ApiResult> CancelPaymentAsync(PaymentProviderContext <DibsSettings> ctx)
        {
            try
            {
                // MD5(key2 + MD5(key1 + "merchant=<merchant>&ctx.Orderid=<ctx.Orderid>&transact=<transact>"))
                var md5Check = $"merchant={ctx.Settings.MerchantId}&ctx.Orderid={ctx.Order.OrderNumber}&transact={ctx.Order.TransactionInfo.TransactionId}";

                var response = await $"https://payment.architrade.com/cgi-adm/cancel.cgi"
                               .WithBasicAuth(ctx.Settings.ApiUsername, ctx.Settings.ApiPassword)
                               .PostUrlEncodedAsync(new
                {
                    merchant  = ctx.Settings.MerchantId,
                    orderid   = ctx.Order.OrderNumber,
                    transact  = ctx.Order.TransactionInfo.TransactionId,
                    textreply = "1",
                    md5key    = MD5Hash(ctx.Settings.MD5Key2 + MD5Hash(ctx.Settings.MD5Key1 + md5Check))
                })
                               .ReceiveString();

                var responseParams = HttpUtility.ParseQueryString(response);
                var result         = responseParams["result"];

                if (result == "0") // 0 == Accepted
                {
                    return(new ApiResult()
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = ctx.Order.TransactionInfo.TransactionId,
                            PaymentStatus = PaymentStatus.Cancelled
                        }
                    });
                }
                else
                {
                    _logger.Warn($"Dibs [{ctx.Order.OrderNumber}] - Error making API request - error message: {result}");
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Dibs - CancelPayment");
            }

            return(ApiResult.Empty);
        }
        public override async Task <CallbackResult> ProcessCallbackAsync(PaymentProviderContext <BamboraCheckoutSettings> ctx)
        {
            try
            {
                var clientConfig = GetBamboraClientConfig(ctx.Settings);
                var client       = new BamboraClient(clientConfig);

                if (client.ValidateRequest(ctx.Request, out var qs))
                {
                    var txnId   = qs["txnid"];
                    var orderId = qs["orderid"];
                    var amount  = int.Parse("0" + qs["amount"]);
                    var txnFee  = int.Parse("0" + qs["txnfee"]);

                    // Validate params
                    if (!string.IsNullOrWhiteSpace(txnId) &&
                        !string.IsNullOrWhiteSpace(orderId) &&
                        orderId == BamboraSafeOrderId(ctx.Order.OrderNumber) &&
                        amount > 0)
                    {
                        // Fetch the transaction details so that we can work out
                        // the status of the transaction as the querystring params
                        // are not enough on their own
                        var transactionResp = await client.GetTransactionAsync(txnId);

                        if (transactionResp.Meta.Result)
                        {
                            return(CallbackResult.Ok(new TransactionInfo
                            {
                                TransactionId = transactionResp.Transaction.Id,
                                AmountAuthorized = AmountFromMinorUnits(amount + txnFee),
                                TransactionFee = AmountFromMinorUnits(txnFee),
                                PaymentStatus = GetPaymentStatus(transactionResp.Transaction)
                            }));
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Bambora - ProcessCallback");
            }

            return(CallbackResult.BadRequest());
        }
        public override async Task <ApiResult> CancelPaymentAsync(PaymentProviderContext <StripeCheckoutSettings> ctx)
        {
            // NOTE: Subscriptions aren't currently abled to be "authorized" so the cancel
            // routine shouldn't be relevant for subscription payments at this point

            try
            {
                // See if there is a payment intent to cancel
                var stripePaymentIntentId = ctx.Order.Properties["stripePaymentIntentId"];
                if (!string.IsNullOrWhiteSpace(stripePaymentIntentId))
                {
                    var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;

                    ConfigureStripe(secretKey);

                    var paymentIntentService = new PaymentIntentService();
                    var intent = await paymentIntentService.CancelAsync(stripePaymentIntentId);

                    return(new ApiResult()
                    {
                        TransactionInfo = new TransactionInfoUpdate()
                        {
                            TransactionId = GetTransactionId(intent),
                            PaymentStatus = GetPaymentStatus(intent)
                        }
                    });
                }

                // If there is a charge, then it's too late to cancel
                // so we attempt to refund it instead
                var chargeId = ctx.Order.Properties["stripeChargeId"];
                if (chargeId != null)
                {
                    return(await RefundPaymentAsync(ctx));
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Stripe - CancelPayment");
            }

            return(ApiResult.Empty);
        }
Example #16
0
        public override async Task <CallbackResult> ProcessCallbackAsync(PaymentProviderContext <DibsSettings> ctx)
        {
            try
            {
                var formData = await ctx.Request.Content.ReadAsFormDataAsync();

                var authkey      = formData["authkey"];
                var transaction  = formData["transact"];
                var currencyCode = formData["currency"];
                var strAmount    = formData["amount"];
                var strFee       = formData["fee"] ?? "0"; // Not always in the return data
                var captured     = formData["capturenow"] == "1";

                var totalAmount = decimal.Parse(strAmount, CultureInfo.InvariantCulture) + decimal.Parse(strFee, CultureInfo.InvariantCulture);

                var md5Check = $"transact={transaction}&amount={totalAmount.ToString("0", CultureInfo.InvariantCulture)}&currency={currencyCode}";

                // authkey = MD5(key2 + MD5(key1 + "transact=<transact>&amount=<amount>&currency=<currency>"))
                if (MD5Hash(ctx.Settings.MD5Key2 + MD5Hash(ctx.Settings.MD5Key1 + md5Check)) == authkey)
                {
                    return(new CallbackResult
                    {
                        TransactionInfo = new TransactionInfo
                        {
                            AmountAuthorized = AmountFromMinorUnits((long)totalAmount),
                            TransactionId = transaction,
                            PaymentStatus = !captured ? PaymentStatus.Authorized : PaymentStatus.Captured
                        }
                    });
                }
                else
                {
                    _logger.Warn($"Dibs [{ctx.Order.OrderNumber}] - MD5Sum security check failed");
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Dibs - ProcessCallback");
            }

            return(CallbackResult.Empty);
        }
Example #17
0
        public override async Task <OrderReference> GetOrderReferenceAsync(PaymentProviderContext <TSettings> ctx)
        {
            try
            {
                var secretKey            = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;
                var webhookSigningSecret = ctx.Settings.TestMode ? ctx.Settings.TestWebhookSigningSecret : ctx.Settings.LiveWebhookSigningSecret;

                ConfigureStripe(secretKey);

                var stripeEvent = await GetWebhookStripeEventAsync(ctx, webhookSigningSecret);

                if (stripeEvent != null && stripeEvent.Type == Events.CheckoutSessionCompleted)
                {
                    if (stripeEvent.Data?.Object?.Instance is Session stripeSession && !string.IsNullOrWhiteSpace(stripeSession.ClientReferenceId))
                    {
                        return(OrderReference.Parse(stripeSession.ClientReferenceId));
                    }
                }
                else if (stripeEvent != null && stripeEvent.Type == Events.ReviewClosed)
                {
                    if (stripeEvent.Data?.Object?.Instance is Review stripeReview && !string.IsNullOrWhiteSpace(stripeReview.PaymentIntentId))
                    {
                        var paymentIntentService = new PaymentIntentService();
                        var paymentIntent        = paymentIntentService.Get(stripeReview.PaymentIntentId);

                        if (paymentIntent != null && paymentIntent.Metadata.ContainsKey("orderReference"))
                        {
                            return(OrderReference.Parse(paymentIntent.Metadata["orderReference"]));
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Stripe - GetOrderReference");
            }

            return(await base.GetOrderReferenceAsync(ctx));
        }
        public override async Task <CallbackResult> ProcessCallbackAsync(PaymentProviderContext <StripeCheckoutSettings> ctx)
        {
            // The ProcessCallback method is only intendid to be called via a Stripe Webhook and so
            // it's job is to process the webhook event and finalize / update the ctx.Order accordingly

            try
            {
                var secretKey            = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;
                var webhookSigningSecret = ctx.Settings.TestMode ? ctx.Settings.TestWebhookSigningSecret : ctx.Settings.LiveWebhookSigningSecret;

                ConfigureStripe(secretKey);

                var stripeEvent = await GetWebhookStripeEventAsync(ctx, webhookSigningSecret);

                if (stripeEvent != null && stripeEvent.Type == Events.CheckoutSessionCompleted)
                {
                    if (stripeEvent.Data?.Object?.Instance is Session stripeSession)
                    {
                        if (stripeSession.Mode == "payment")
                        {
                            var paymentIntentService = new PaymentIntentService();
                            var paymentIntent        = await paymentIntentService.GetAsync(stripeSession.PaymentIntentId, new PaymentIntentGetOptions
                            {
                                Expand = new List <string>(new[] {
                                    "review"
                                })
                            });

                            return(CallbackResult.Ok(new TransactionInfo
                            {
                                TransactionId = GetTransactionId(paymentIntent),
                                AmountAuthorized = AmountFromMinorUnits(paymentIntent.Amount),
                                PaymentStatus = GetPaymentStatus(paymentIntent)
                            },
                                                     new Dictionary <string, string>
                            {
                                { "stripeSessionId", stripeSession.Id },
                                { "stripeCustomerId", stripeSession.CustomerId },
                                { "stripePaymentIntentId", stripeSession.PaymentIntentId },
                                { "stripeSubscriptionId", stripeSession.SubscriptionId },
                                { "stripeChargeId", GetTransactionId(paymentIntent) },
                                { "stripeCardCountry", paymentIntent.Charges?.Data?.FirstOrDefault()?.PaymentMethodDetails?.Card?.Country }
                            }));
                        }
                        else if (stripeSession.Mode == "subscription")
                        {
                            var subscriptionService = new SubscriptionService();
                            var subscription        = await subscriptionService.GetAsync(stripeSession.SubscriptionId, new SubscriptionGetOptions {
                                Expand = new List <string>(new[] {
                                    "latest_invoice",
                                    "latest_invoice.charge",
                                    "latest_invoice.charge.review",
                                    "latest_invoice.payment_intent",
                                    "latest_invoice.payment_intent.review"
                                })
                            });

                            var invoice = subscription.LatestInvoice;

                            return(CallbackResult.Ok(new TransactionInfo
                            {
                                TransactionId = GetTransactionId(invoice),
                                AmountAuthorized = AmountFromMinorUnits(invoice.PaymentIntent.Amount),
                                PaymentStatus = GetPaymentStatus(invoice)
                            },
                                                     new Dictionary <string, string>
                            {
                                { "stripeSessionId", stripeSession.Id },
                                { "stripeCustomerId", stripeSession.CustomerId },
                                { "stripePaymentIntentId", invoice.PaymentIntentId },
                                { "stripeSubscriptionId", stripeSession.SubscriptionId },
                                { "stripeChargeId", invoice.ChargeId },
                                { "stripeCardCountry", invoice.Charge?.PaymentMethodDetails?.Card?.Country }
                            }));
                        }
                    }
                    else if (stripeEvent != null && stripeEvent.Type == Events.ReviewClosed)
                    {
                        if (stripeEvent.Data?.Object?.Instance is Review stripeReview && !string.IsNullOrWhiteSpace(stripeReview.PaymentIntentId))
                        {
                            var paymentIntentService = new PaymentIntentService();
                            var paymentIntent        = paymentIntentService.Get(stripeReview.PaymentIntentId, new PaymentIntentGetOptions
                            {
                                Expand = new List <string>(new[] {
                                    "review"
                                })
                            });

                            return(CallbackResult.Ok(new TransactionInfo
                            {
                                TransactionId = GetTransactionId(paymentIntent),
                                AmountAuthorized = AmountFromMinorUnits(paymentIntent.Amount),
                                PaymentStatus = GetPaymentStatus(paymentIntent)
                            }));
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Stripe - ProcessCallback");
            }

            return(CallbackResult.BadRequest());
        }
Example #19
0
        protected async Task <StripeWebhookEvent> GetWebhookStripeEventAsync(PaymentProviderContext <TSettings> ctx, string webhookSigningSecret)
        {
            StripeWebhookEvent stripeEvent = null;

            if (ctx.AdditionalData.ContainsKey("Vendr_StripeEvent"))
            {
                stripeEvent = (StripeWebhookEvent)ctx.AdditionalData["Vendr_StripeEvent"];
            }
            else
            {
                try
                {
                    var json = await ctx.Request.Content.ReadAsStringAsync();

                    var stripeSignature = ctx.Request.Headers.GetValues("Stripe-Signature").FirstOrDefault();

                    // Just validate the webhook signature
                    EventUtility.ValidateSignature(json, stripeSignature, webhookSigningSecret);

                    // Parse the event ourselves to our custom webhook event model
                    // as it only captures minimal object information.
                    stripeEvent = JsonConvert.DeserializeObject <StripeWebhookEvent>(json);

                    // We manually fetch the event object type ourself as it means it will be fetched
                    // using the same API version as the payment providers is coded against.
                    // NB: Only supports a number of object types we are likely to be interested in.
                    if (stripeEvent?.Data?.Object != null)
                    {
                        switch (stripeEvent.Data.Object.Type)
                        {
                        case "checkout.session":
                            var sessionService = new SessionService();
                            stripeEvent.Data.Object.Instance = sessionService.Get(stripeEvent.Data.Object.Id);
                            break;

                        case "charge":
                            var chargeService = new ChargeService();
                            stripeEvent.Data.Object.Instance = chargeService.Get(stripeEvent.Data.Object.Id);
                            break;

                        case "payment_intent":
                            var paymentIntentService = new PaymentIntentService();
                            stripeEvent.Data.Object.Instance = paymentIntentService.Get(stripeEvent.Data.Object.Id);
                            break;

                        case "subscription":
                            var subscriptionService = new SubscriptionService();
                            stripeEvent.Data.Object.Instance = subscriptionService.Get(stripeEvent.Data.Object.Id);
                            break;

                        case "invoice":
                            var invoiceService = new InvoiceService();
                            stripeEvent.Data.Object.Instance = invoiceService.Get(stripeEvent.Data.Object.Id);
                            break;

                        case "review":
                            var reviewService = new ReviewService();
                            stripeEvent.Data.Object.Instance = reviewService.Get(stripeEvent.Data.Object.Id);
                            break;
                        }
                    }

                    ctx.AdditionalData.Add("Vendr_StripeEvent", stripeEvent);
                }
                catch (Exception ex)
                {
                    _logger.Error(ex, "Stripe - GetWebhookStripeEvent");
                }
            }

            return(stripeEvent);
        }
Example #20
0
 public override string GetCancelUrl(PaymentProviderContext <TSettings> ctx)
 {
     return(ctx.Settings.CancelUrl);
 }
Example #21
0
 public override string GetContinueUrl(PaymentProviderContext <TSettings> ctx)
 {
     return(ctx.Settings.ContinueUrl); // + (settings.ContinueUrl.Contains("?") ? "&" : "?") + "session_id={CHECKOUT_SESSION_ID}";
 }
        public override async Task <PaymentFormResult> GenerateFormAsync(PaymentProviderContext <BamboraCheckoutSettings> ctx)
        {
            var currency     = Vendr.Services.CurrencyService.GetCurrency(ctx.Order.CurrencyId);
            var currencyCode = currency.Code.ToUpperInvariant();

            // Ensure currency has valid ISO 4217 code
            if (!Iso4217.CurrencyCodes.ContainsKey(currencyCode))
            {
                throw new Exception("Currency must be a valid ISO 4217 currency code: " + currency.Name);
            }

            var orderAmount = (int)AmountToMinorUnits(ctx.Order.TransactionAmount.Value);

            var clientConfig = GetBamboraClientConfig(ctx.Settings);
            var client       = new BamboraClient(clientConfig);

            // Configure checkout session
            var checkoutSessionRequest = new BamboraCreateCheckoutSessionRequest
            {
                InstantCaptureAmount = ctx.Settings.Capture ? orderAmount : 0,
                Customer             = new BamboraCustomer
                {
                    Email = ctx.Order.CustomerInfo.Email
                },
                Order = new BamboraOrder
                {
                    Id       = BamboraSafeOrderId(ctx.Order.OrderNumber),
                    Amount   = orderAmount,
                    Currency = currencyCode
                },
                Urls = new BamboraUrls
                {
                    Accept    = ctx.Urls.ContinueUrl,
                    Cancel    = ctx.Urls.CancelUrl,
                    Callbacks = new[] {
                        new BamboraUrl {
                            Url = ctx.Urls.CallbackUrl
                        }
                    }
                },
                PaymentWindow = new BamboraPaymentWindow
                {
                    Id       = 1,
                    Language = ctx.Settings.Language
                }
            };

            // Exclude payment methods
            if (!string.IsNullOrWhiteSpace(ctx.Settings.ExcludedPaymentMethods))
            {
                checkoutSessionRequest.PaymentWindow.PaymentMethods = ctx.Settings.ExcludedPaymentMethods
                                                                      .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                                                                      .Where(x => !string.IsNullOrWhiteSpace(x))
                                                                      .Select(x => new BamboraPaymentFilter {
                    Id = x.Trim(), Action = BamboraPaymentFilter.Actions.Exclude
                })
                                                                      .ToArray();
            }

            // Exclude payment groups
            if (!string.IsNullOrWhiteSpace(ctx.Settings.ExcludedPaymentGroups))
            {
                checkoutSessionRequest.PaymentWindow.PaymentGroups = ctx.Settings.ExcludedPaymentGroups
                                                                     .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                                                                     .Where(x => !string.IsNullOrWhiteSpace(x))
                                                                     .Select(x => new BamboraPaymentFilter {
                    Id = x.Trim(), Action = BamboraPaymentFilter.Actions.Exclude
                })
                                                                     .ToArray();
            }

            // Exclude payment types
            if (!string.IsNullOrWhiteSpace(ctx.Settings.ExcludedPaymentTypes))
            {
                checkoutSessionRequest.PaymentWindow.PaymentTypes = ctx.Settings.ExcludedPaymentTypes
                                                                    .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                                                                    .Where(x => !string.IsNullOrWhiteSpace(x))
                                                                    .Select(x => new BamboraPaymentFilter {
                    Id = x.Trim(), Action = BamboraPaymentFilter.Actions.Exclude
                })
                                                                    .ToArray();
            }

            var checkoutSession = await client.CreateCheckoutSessionAsync(checkoutSessionRequest);

            if (checkoutSession.Meta.Result)
            {
                return(new PaymentFormResult()
                {
                    Form = new PaymentForm(checkoutSession.Url, PaymentFormMethod.Get)
                });
            }

            throw new ApplicationException(checkoutSession.Meta.Message.EndUser);
        }
Example #23
0
 public override string GetErrorUrl(PaymentProviderContext <TSettings> ctx)
 {
     return(ctx.Settings.ErrorUrl);
 }
        public override async Task <PaymentFormResult> GenerateFormAsync(PaymentProviderContext <StripeCheckoutSettings> ctx)
        {
            var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey;
            var publicKey = ctx.Settings.TestMode ? ctx.Settings.TestPublicKey : ctx.Settings.LivePublicKey;

            ConfigureStripe(secretKey);

            var currency       = Vendr.Services.CurrencyService.GetCurrency(ctx.Order.CurrencyId);
            var billingCountry = ctx.Order.PaymentInfo.CountryId.HasValue
                ? Vendr.Services.CountryService.GetCountry(ctx.Order.PaymentInfo.CountryId.Value)
                : null;

            Customer customer;
            var      customerService = new CustomerService();

            // If we've created a customer already, keep using it but update it incase
            // any of the billing details have changed
            if (!string.IsNullOrWhiteSpace(ctx.Order.Properties["stripeCustomerId"]))
            {
                var customerOptions = new CustomerUpdateOptions
                {
                    Name        = $"{ctx.Order.CustomerInfo.FirstName} {ctx.Order.CustomerInfo.LastName}",
                    Email       = ctx.Order.CustomerInfo.Email,
                    Description = ctx.Order.OrderNumber,
                    Address     = new AddressOptions
                    {
                        Line1 = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressLine1PropertyAlias)
                            ? ctx.Order.Properties[ctx.Settings.BillingAddressLine1PropertyAlias] : "",
                        Line2 = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressLine2PropertyAlias)
                            ? ctx.Order.Properties[ctx.Settings.BillingAddressLine2PropertyAlias] : "",
                        City = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressCityPropertyAlias)
                            ? ctx.Order.Properties[ctx.Settings.BillingAddressCityPropertyAlias] : "",
                        State = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressStatePropertyAlias)
                            ? ctx.Order.Properties[ctx.Settings.BillingAddressStatePropertyAlias] : "",
                        PostalCode = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressZipCodePropertyAlias)
                            ? ctx.Order.Properties[ctx.Settings.BillingAddressZipCodePropertyAlias] : "",
                        Country = billingCountry?.Code
                    }
                };

                // Pass billing country / zipcode as meta data as currently
                // this is the only way it can be validated via Radar
                // Block if ::customer:billingCountry:: != :card_country:
                customerOptions.Metadata = new Dictionary <string, string>
                {
                    { "billingCountry", customerOptions.Address.Country },
                    { "billingZipCode", customerOptions.Address.PostalCode }
                };

                customer = customerService.Update(ctx.Order.Properties["stripeCustomerId"].Value, customerOptions);
            }
            else
            {
                var customerOptions = new CustomerCreateOptions
                {
                    Name        = $"{ctx.Order.CustomerInfo.FirstName} {ctx.Order.CustomerInfo.LastName}",
                    Email       = ctx.Order.CustomerInfo.Email,
                    Description = ctx.Order.OrderNumber,
                    Address     = new AddressOptions
                    {
                        Line1 = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressLine1PropertyAlias)
                        ? ctx.Order.Properties[ctx.Settings.BillingAddressLine1PropertyAlias] : "",
                        Line2 = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressLine2PropertyAlias)
                        ? ctx.Order.Properties[ctx.Settings.BillingAddressLine2PropertyAlias] : "",
                        City = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressCityPropertyAlias)
                        ? ctx.Order.Properties[ctx.Settings.BillingAddressCityPropertyAlias] : "",
                        State = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressStatePropertyAlias)
                        ? ctx.Order.Properties[ctx.Settings.BillingAddressStatePropertyAlias] : "",
                        PostalCode = !string.IsNullOrWhiteSpace(ctx.Settings.BillingAddressZipCodePropertyAlias)
                        ? ctx.Order.Properties[ctx.Settings.BillingAddressZipCodePropertyAlias] : "",
                        Country = billingCountry?.Code
                    }
                };

                // Pass billing country / zipcode as meta data as currently
                // this is the only way it can be validated via Radar
                // Block if ::customer:billingCountry:: != :card_country:
                customerOptions.Metadata = new Dictionary <string, string>
                {
                    { "billingCountry", customerOptions.Address.Country },
                    { "billingZipCode", customerOptions.Address.PostalCode }
                };

                customer = customerService.Create(customerOptions);
            }

            var metaData = new Dictionary <string, string>
            {
                { "ctx.OrderReference", ctx.Order.GenerateOrderReference() },
                { "ctx.OrderId", ctx.Order.Id.ToString("D") },
                { "ctx.OrderNumber", ctx.Order.OrderNumber }
            };

            if (!string.IsNullOrWhiteSpace(ctx.Settings.OrderProperties))
            {
                foreach (var alias in ctx.Settings.OrderProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                         .Select(x => x.Trim())
                         .Where(x => !string.IsNullOrWhiteSpace(x)))
                {
                    if (!string.IsNullOrWhiteSpace(ctx.Order.Properties[alias]))
                    {
                        metaData.Add(alias, ctx.Order.Properties[alias]);
                    }
                }
            }

            var  hasRecurringItems   = false;
            long recurringTotalPrice = 0;
            long orderTotalPrice     = AmountToMinorUnits(ctx.Order.TransactionAmount.Value);

            var lineItems = new List <SessionLineItemOptions>();

            foreach (var orderLine in ctx.Order.OrderLines.Where(IsRecurringOrderLine))
            {
                var orderLineTaxRate = orderLine.TaxRate * 100;

                var lineItemOpts = new SessionLineItemOptions();

                if (orderLine.Properties.ContainsKey("stripePriceId") && !string.IsNullOrWhiteSpace(orderLine.Properties["stripePriceId"]))
                {
                    // NB: When using stripe prices there is an inherit risk that values may not
                    // actually be in sync and so the price displayed on the site might not match
                    // that in stripe and so this may cause inconsistant payments
                    lineItemOpts.Price = orderLine.Properties["stripePriceId"].Value;

                    // If we are using a stripe price, then assume the quantity of the line item means
                    // the quantity of the stripe price you want to buy.
                    lineItemOpts.Quantity = (long)orderLine.Quantity;

                    // Because we are in charge of what taxes apply, we need to setup a tax rate
                    // to ensure the price defined in stripe has the relevant taxes applied
                    var stripePricesIncludeTax = PropertyIsTrue(orderLine.Properties, "stripePriceIncludesTax");
                    var stripeTaxRate          = GetOrCreateStripeTaxRate(ctx, "Subscription Tax", orderLineTaxRate, stripePricesIncludeTax);
                    if (stripeTaxRate != null)
                    {
                        lineItemOpts.TaxRates = new List <string>(new[] { stripeTaxRate.Id });
                    }
                }
                else
                {
                    // We don't have a stripe price defined on the ctx.Order line
                    // so we'll create one on the fly using the ctx.Order lines total
                    // value
                    var priceData = new SessionLineItemPriceDataOptions
                    {
                        Currency   = currency.Code,
                        UnitAmount = AmountToMinorUnits(orderLine.TotalPrice.Value.WithoutTax / orderLine.Quantity), // Without tax as Stripe will apply the tax
                        Recurring  = new SessionLineItemPriceDataRecurringOptions
                        {
                            Interval      = orderLine.Properties["stripeRecurringInterval"].Value.ToLower(),
                            IntervalCount = long.TryParse(orderLine.Properties["stripeRecurringIntervalCount"], out var intervalCount) ? intervalCount : 1
                        }
                    };

                    if (orderLine.Properties.ContainsKey("stripeProductId") && !string.IsNullOrWhiteSpace(orderLine.Properties["stripeProductId"]))
                    {
                        priceData.Product = orderLine.Properties["stripeProductId"].Value;
                    }
                    else
                    {
                        priceData.ProductData = new SessionLineItemPriceDataProductDataOptions
                        {
                            Name     = orderLine.Name,
                            Metadata = new Dictionary <string, string>
                            {
                                { "ProductReference", orderLine.ProductReference }
                            }
                        };
                    }

                    lineItemOpts.PriceData = priceData;

                    // For dynamic subscriptions, regardless of line item quantity, treat the line
                    // as a single subscription item with one price being the line items total price
                    lineItemOpts.Quantity = (long)orderLine.Quantity;

                    // If we define the price, then create tax rates that are set to be inclusive
                    // as this means that we can pass prices inclusive of tax and Stripe works out
                    // the pre-tax price which would be less suseptable to rounding inconsistancies
                    var stripeTaxRate = GetOrCreateStripeTaxRate(ctx, "Subscription Tax", orderLineTaxRate, false);
                    if (stripeTaxRate != null)
                    {
                        lineItemOpts.TaxRates = new List <string>(new[] { stripeTaxRate.Id });
                    }
                }

                lineItems.Add(lineItemOpts);

                recurringTotalPrice += AmountToMinorUnits(orderLine.TotalPrice.Value.WithTax);
                hasRecurringItems    = true;
            }

            if (recurringTotalPrice < orderTotalPrice)
            {
                // If the total value of the ctx.Order is not covered by the subscription items
                // then we add another line item for the remainder of the ctx.Order value

                var lineItemOpts = new SessionLineItemOptions
                {
                    PriceData = new SessionLineItemPriceDataOptions
                    {
                        Currency    = currency.Code,
                        UnitAmount  = orderTotalPrice - recurringTotalPrice,
                        ProductData = new SessionLineItemPriceDataProductDataOptions
                        {
                            Name = hasRecurringItems
                                ? !string.IsNullOrWhiteSpace(ctx.Settings.OneTimeItemsHeading) ? ctx.Settings.OneTimeItemsHeading : "One time items (inc Tax)"
                                : !string.IsNullOrWhiteSpace(ctx.Settings.OrderHeading) ? ctx.Settings.OrderHeading : "#" + ctx.Order.OrderNumber,
                            Description = hasRecurringItems || !string.IsNullOrWhiteSpace(ctx.Settings.OrderHeading) ? "#" + ctx.Order.OrderNumber : null,
                        }
                    },
                    Quantity = 1
                };

                lineItems.Add(lineItemOpts);
            }

            // Add image to the first item (only if it's not a product link)
            if (!string.IsNullOrWhiteSpace(ctx.Settings.OrderImage) && lineItems.Count > 0 && lineItems[0].PriceData?.ProductData != null)
            {
                lineItems[0].PriceData.ProductData.Images = new[] { ctx.Settings.OrderImage }.ToList();
            }

            var sessionOptions = new SessionCreateOptions
            {
                Customer           = customer.Id,
                PaymentMethodTypes = !string.IsNullOrWhiteSpace(ctx.Settings.PaymentMethodTypes)
                    ? ctx.Settings.PaymentMethodTypes.Split(',')
                                     .Select(tag => tag.Trim())
                                     .Where(tag => !string.IsNullOrEmpty(tag))
                                     .ToList()
                    : new List <string> {
                    "card",
                },
                LineItems = lineItems,
                Mode      = hasRecurringItems
                    ? "subscription"
                    : "payment",
                ClientReferenceId = ctx.Order.GenerateOrderReference(),
                SuccessUrl        = ctx.Urls.ContinueUrl,
                CancelUrl         = ctx.Urls.CancelUrl
            };

            if (hasRecurringItems)
            {
                sessionOptions.SubscriptionData = new SessionSubscriptionDataOptions
                {
                    Metadata = metaData
                };
            }
            else
            {
                sessionOptions.PaymentIntentData = new SessionPaymentIntentDataOptions
                {
                    CaptureMethod = ctx.Settings.Capture ? "automatic" : "manual",
                    Metadata      = metaData
                };
            }

            if (ctx.Settings.SendStripeReceipt)
            {
                sessionOptions.PaymentIntentData.ReceiptEmail = ctx.Order.CustomerInfo.Email;
            }

            var sessionService = new SessionService();
            var session        = await sessionService.CreateAsync(sessionOptions);

            return(new PaymentFormResult()
            {
                MetaData = new Dictionary <string, string>
                {
                    { "stripeSessionId", session.Id },
                    { "stripeCustomerId", session.CustomerId }
                },
                Form = new PaymentForm(ctx.Urls.ContinueUrl, PaymentFormMethod.Post)
                       .WithAttribute("onsubmit", "return handleStripeCheckout(event)")
                       .WithJsFile("https://js.stripe.com/v3/")
                       .WithJs(@"
                        var stripe = Stripe('" + publicKey + @"');
                        window.handleStripeCheckout = function (e) {
                            e.preventDefault();
                            stripe.redirectToCheckout({
                                sessionId: '" + session.Id + @"'
                            }).then(function (result) {
                              // If `redirectToCheckout` fails due to a browser or network
                              // error, display the localized error message to your customer
                              // using `result.error.message`.
                            });
                            return false;
                        }
                    ")
            });
        }