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 })); }
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}")); } }
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; } ") }); }