Пример #1
0
        public async Task <ActionResult <ChangePlanResult> > ChangePlanAsync(string id, string planId, string stripeToken = null, string last4 = null, string couponId = null)
        {
            if (String.IsNullOrEmpty(id) || !CanAccessOrganization(id))
            {
                return(NotFound());
            }

            if (!_stripeOptions.Value.EnableBilling)
            {
                return(Ok(ChangePlanResult.FailWithMessage("Plans cannot be changed while billing is disabled.")));
            }

            var organization = await GetModelAsync(id, false);

            if (organization == null)
            {
                return(Ok(ChangePlanResult.FailWithMessage("Invalid OrganizationId.")));
            }

            var plan = _billingManager.GetBillingPlan(planId);

            if (plan == null)
            {
                return(Ok(ChangePlanResult.FailWithMessage("Invalid PlanId.")));
            }

            if (String.Equals(organization.PlanId, plan.Id) && String.Equals(_plans.FreePlan.Id, plan.Id))
            {
                return(Ok(ChangePlanResult.SuccessWithMessage("Your plan was not changed as you were already on the free plan.")));
            }

            // Only see if they can downgrade a plan if the plans are different.
            if (!String.Equals(organization.PlanId, plan.Id))
            {
                var result = await _billingManager.CanDownGradeAsync(organization, plan, CurrentUser);

                if (!result.Success)
                {
                    return(Ok(result));
                }
            }

            var customerService     = new CustomerService(_stripeOptions.Value.StripeApiKey);
            var subscriptionService = new SubscriptionService(_stripeOptions.Value.StripeApiKey);

            try {
                // If they are on a paid plan and then downgrade to a free plan then cancel their stripe subscription.
                if (!String.Equals(organization.PlanId, _plans.FreePlan.Id) && String.Equals(plan.Id, _plans.FreePlan.Id))
                {
                    if (!String.IsNullOrEmpty(organization.StripeCustomerId))
                    {
                        var subs = await subscriptionService.ListAsync(new SubscriptionListOptions { CustomerId = organization.StripeCustomerId });

                        foreach (var sub in subs.Where(s => !s.CanceledAt.HasValue))
                        {
                            await subscriptionService.CancelAsync(sub.Id, new SubscriptionCancelOptions());
                        }
                    }

                    organization.BillingStatus = BillingStatus.Trialing;
                    organization.RemoveSuspension();
                }
                else if (String.IsNullOrEmpty(organization.StripeCustomerId))
                {
                    if (String.IsNullOrEmpty(stripeToken))
                    {
                        return(Ok(ChangePlanResult.FailWithMessage("Billing information was not set.")));
                    }

                    organization.SubscribeDate = SystemClock.UtcNow;

                    var createCustomer = new CustomerCreateOptions {
                        SourceToken = stripeToken,
                        PlanId      = planId,
                        Description = organization.Name,
                        Email       = CurrentUser.EmailAddress
                    };

                    if (!String.IsNullOrWhiteSpace(couponId))
                    {
                        createCustomer.CouponId = couponId;
                    }

                    var customer = await customerService.CreateAsync(createCustomer);

                    organization.BillingStatus = BillingStatus.Active;
                    organization.RemoveSuspension();
                    organization.StripeCustomerId = customer.Id;
                    if (customer.Sources.Data.Count > 0)
                    {
                        organization.CardLast4 = (customer.Sources.Data.First() as Card)?.Last4;
                    }
                }
                else
                {
                    var update = new SubscriptionUpdateOptions {
                        Items = new List <SubscriptionItemUpdateOption>()
                    };
                    var create = new SubscriptionCreateOptions {
                        CustomerId = organization.StripeCustomerId, Items = new List <SubscriptionItemOption>()
                    };
                    bool cardUpdated = false;

                    var customerUpdateOptions = new CustomerUpdateOptions {
                        Description = organization.Name, Email = CurrentUser.EmailAddress
                    };
                    if (!String.IsNullOrEmpty(stripeToken))
                    {
                        customerUpdateOptions.SourceToken = stripeToken;
                        cardUpdated = true;
                    }

                    await customerService.UpdateAsync(organization.StripeCustomerId, customerUpdateOptions);

                    var subscriptionList = await subscriptionService.ListAsync(new SubscriptionListOptions { CustomerId = organization.StripeCustomerId });

                    var subscription = subscriptionList.FirstOrDefault(s => !s.CanceledAt.HasValue);
                    if (subscription != null)
                    {
                        update.Items.Add(new SubscriptionItemUpdateOption {
                            Id = subscription.Items.Data[0].Id, PlanId = planId
                        });
                        await subscriptionService.UpdateAsync(subscription.Id, update);
                    }
                    else
                    {
                        create.Items.Add(new SubscriptionItemOption {
                            PlanId = planId
                        });
                        await subscriptionService.CreateAsync(create);
                    }

                    if (cardUpdated)
                    {
                        organization.CardLast4 = last4;
                    }

                    organization.BillingStatus = BillingStatus.Active;
                    organization.RemoveSuspension();
                }

                _billingManager.ApplyBillingPlan(organization, plan, CurrentUser);
                await _repository.SaveAsync(organization, o => o.Cache());

                await _messagePublisher.PublishAsync(new PlanChanged { OrganizationId = organization.Id });
            } catch (Exception ex) {
                using (_logger.BeginScope(new ExceptionlessState().Tag("Change Plan").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext)))
                    _logger.LogCritical(ex, "An error occurred while trying to update your billing plan: {Message}", ex.Message);

                return(Ok(ChangePlanResult.FailWithMessage(ex.Message)));
            }

            return(Ok(new ChangePlanResult {
                Success = true
            }));
        }
Пример #2
0
        public ActionResult <Subscription> CreateSubscription([FromBody] CreateSubscriptionRequest req)
        {
            if (!ModelState.IsValid)
            {
                return(this.FailWithMessage("invalid params"));
            }
            var newPrice = Environment.GetEnvironmentVariable(req.Price.ToUpper());

            if (newPrice is null || newPrice == "")
            {
                return(this.FailWithMessage($"No price with the new price ID ({req.Price}) found in .env"));
            }

            // Attach payment method
            var options = new PaymentMethodAttachOptions
            {
                Customer = req.Customer,
            };
            var service = new PaymentMethodService();

            PaymentMethod paymentMethod;

            try
            {
                paymentMethod = service.Attach(req.PaymentMethod, options);
            }
            catch (Exception e)
            {
                return(this.FailWithMessage($"Failed to attach payment method {e}"));
            }

            // Update customer's default invoice payment method
            var customerOptions = new CustomerUpdateOptions
            {
                InvoiceSettings = new CustomerInvoiceSettingsOptions
                {
                    DefaultPaymentMethod = paymentMethod.Id,
                },
            };
            var customerService = new CustomerService();

            try
            {
                customerService.Update(req.Customer, customerOptions);
            }
            catch (StripeException e)
            {
                return(this.FailWithMessage($"Failed to attach payment method {e}"));
            }

            // Create subscription
            var subscriptionOptions = new SubscriptionCreateOptions
            {
                Customer = req.Customer,
                Items    = new List <SubscriptionItemOptions>
                {
                    new SubscriptionItemOptions
                    {
                        Price    = Environment.GetEnvironmentVariable(req.Price),
                        Quantity = req.Quantity,
                    },
                },
            };

            subscriptionOptions.AddExpand("latest_invoice.payment_intent");
            var subscriptionService = new SubscriptionService();

            try
            {
                return(subscriptionService.Create(subscriptionOptions));
            }
            catch (StripeException e)
            {
                return(this.FailWithMessage($"Failed to attach payment method: {e}"));
            }
        }
Пример #3
0
        static void Main(string[] args)
        {
            // Set with a valid test API key.
            StripeConfiguration.ApiKey = "sk_test_XXX";
            Console.WriteLine("Hello Metadata!");

            // Create a customer with 2 metadata key value pairs
            var options = new CustomerCreateOptions
            {
                Name     = "Jenny Rosen",
                Metadata = new Dictionary <string, string>
                {
                    { "my_app_id", "123" },
                    { "my_app_username", "jenny_rosen" },
                }
            };

            var service  = new CustomerService();
            var customer = service.Create(options);

            Console.WriteLine(customer);

            // Add additional metadata to an existing object by making an update call.
            // Replace this customer id with the customer you created above.
            var updateOptions = new CustomerUpdateOptions
            {
                Metadata = new Dictionary <string, string>
                {
                    { "favorite_animal", "cheetah" }
                }
            };

            customer = service.Update("cus_IZzl0c2oBOjftt", updateOptions);
            Console.WriteLine(customer);

            // Update metadata on an existing object by making an update call.
            // Only the metadata keys you specify in the call will be updated,
            // other keys will remain unchanged.
            // Replace this customer id with the customer you created above.
            updateOptions = new CustomerUpdateOptions
            {
                Metadata = new Dictionary <string, string>
                {
                    { "favorite_animal", "llama" }
                }
            };
            customer = service.Update("cus_IZzl0c2oBOjftt", updateOptions);
            Console.WriteLine(customer);

            // Remove metadata from an object by making an update call.
            // Set the value to the empty string for any metadata you wish to remove from the object.
            // Replace this customer id with the customer you created above.
            updateOptions = new CustomerUpdateOptions
            {
                Metadata = new Dictionary <string, string>
                {
                    { "favorite_animal", "" },
                    { "my_app_username", "" }
                }
            };
            customer = service.Update("cus_IZzl0c2oBOjftt", updateOptions);
            Console.WriteLine(customer);
        }
        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;
                        }
                    ")
            });
        }