From abfca0fbb32b8876a47783b7dd65054fe80c0b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kriszti=C3=A1n=20N=C3=A9meth?= Date: Wed, 30 Oct 2024 17:54:31 +0100 Subject: [PATCH] Code refactoring and drying --- .../Abstractions/IStripePaymentService.cs | 6 - .../Api/StripeCheckoutApiEndpoint.cs | 27 +--- .../SubscriptionCheckoutEndpointViewModel.cs | 4 + .../Endpoints/Api/StripeCustomerEndpoint.cs | 10 +- .../Api/StripeSubscriptionEndpoint.cs | 11 +- .../Services/IStripeCustomerService.cs | 16 ++- .../Services/IStripeSessionEventHandler.cs | 7 +- .../Services/IStripeSessionService.cs | 11 +- .../Services/IStripeSubscriptionService.cs | 6 +- .../Services/StripeCustomerService.cs | 116 +++++++++++++----- .../Services/StripePaymentService.cs | 66 ---------- .../Services/StripeSessionService.cs | 75 ++--------- .../Services/StripeSubscriptionService.cs | 53 ++++++++ .../SubscriptionStripeWebhookEventHandler.cs | 6 +- .../Migrations/SubscriptionMigrations.cs | 4 + 15 files changed, 191 insertions(+), 227 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs index ac9d706a..d6f742fd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs @@ -18,12 +18,6 @@ namespace OrchardCore.Commerce.Payment.Stripe.Abstractions; /// public interface IStripePaymentService { - Task CreateSubscriptionAsync(StripeCreateSubscriptionViewModel viewModel); - - Task GetCustomerAsync(string customerId); - - Task CreateCustomerAsync(CustomerCreateOptions customerCreateOptions); - Task GetConfirmationTokenAsync(string confirmationTokenId); long GetPaymentAmount(Amount total); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeCheckoutApiEndpoint.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeCheckoutApiEndpoint.cs index 35ceb672..4995f0df 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeCheckoutApiEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeCheckoutApiEndpoint.cs @@ -7,10 +7,7 @@ using OrchardCore.Commerce.Payment.Stripe.Endpoints.Models; using OrchardCore.Commerce.Payment.Stripe.Endpoints.Permissions; using OrchardCore.Commerce.Payment.Stripe.Services; -using OrchardCore.Environment.Shell; using Stripe.Checkout; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using static OrchardCore.Commerce.Payment.Stripe.Endpoints.Constants.Endpoints; @@ -30,7 +27,6 @@ private static async Task GetStripeCheckoutEndpointAsync( [FromServices] IAuthorizationService authorizationService, [FromServices] IStripeCustomerService stripeCustomerService, [FromServices] IStripeSessionService stripeSessionService, - [FromServices] ShellSettings shellSettings, HttpContext httpContext) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApiStripePayment)) @@ -38,8 +34,7 @@ private static async Task GetStripeCheckoutEndpointAsync( return httpContext.ChallengeOrForbidApi(); } - //TODO: We need update also - var customer = await stripeCustomerService.GetOrCreateCustomerAsync( + var customer = await stripeCustomerService.GetAndUpdateOrCreateCustomerAsync( viewModel.BillingAddress, viewModel.ShippingAddress, viewModel.Email, @@ -53,30 +48,10 @@ private static async Task GetStripeCheckoutEndpointAsync( SuccessUrl = viewModel.SuccessUrl, CancelUrl = viewModel.CancelUrl, Customer = customer.Id, - SubscriptionData = new SessionSubscriptionDataOptions - { - Metadata = new Dictionary - { - { "tenantName", shellSettings.Name }, - }, - }, }; - foreach (var lineItem in options.LineItems) - { - //TODO: Change this to use the actual tax rate - lineItem.TaxRates = ["txr_1F3586L1SJaDnrcsvfTTvknD"]; - } - var session = await stripeSessionService.CreateSessionAsync(options); - //Save session id to DB, with current User data and other necessary data - var result = await stripeSessionService.SaveSessionDataAsync(customer, session); - if (result.Errors?.Any() == true) - { - return TypedResults.BadRequest(result.Errors); - } - return TypedResults.Ok(session.Url); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/SubscriptionCheckoutEndpointViewModel.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/SubscriptionCheckoutEndpointViewModel.cs index 47e18564..45a393c6 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/SubscriptionCheckoutEndpointViewModel.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/SubscriptionCheckoutEndpointViewModel.cs @@ -8,7 +8,11 @@ public class SubscriptionCheckoutEndpointViewModel { public string SuccessUrl { get; set; } public string CancelUrl { get; set; } + + // This is an API model so we don't need to make it read-only. +#pragma warning disable CA2227 // CA2227: Change 'SessionLineItemOptions' to be read-only by removing the property setter public IList SessionLineItemOptions { get; set; } = new List(); +#pragma warning restore CA2227 public PaymentMode PaymentMode { get; set; } public string Phone { get; set; } public string Email { get; set; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeCustomerEndpoint.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeCustomerEndpoint.cs index 31301490..1cfaa36a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeCustomerEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeCustomerEndpoint.cs @@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; -using OrchardCore.Commerce.Payment.Stripe.Abstractions; using OrchardCore.Commerce.Payment.Stripe.Endpoints.Permissions; +using OrchardCore.Commerce.Payment.Stripe.Services; using Stripe; using System.Threading.Tasks; using static OrchardCore.Commerce.Payment.Stripe.Endpoints.Constants.Endpoints; @@ -22,7 +22,7 @@ public static IEndpointRouteBuilder AddStripeGetCustomerEndpoint(this IEndpointR private static async Task GetStripeCustomerAsync( [FromQuery] string? customerId, - [FromServices] IStripePaymentService stripePaymentService, + [FromServices] IStripeCustomerService stripeCustomerService, [FromServices] IAuthorizationService authorizationService, HttpContext httpContext) { @@ -31,7 +31,7 @@ private static async Task GetStripeCustomerAsync( return httpContext.ChallengeOrForbidApi(); } - var customer = await stripePaymentService.GetCustomerAsync(customerId); + var customer = await stripeCustomerService.GetCustomerByIdAsync(customerId); return TypedResults.Ok(customer); } @@ -43,7 +43,7 @@ public static IEndpointRouteBuilder AddStripeCreateCustomerEndpoint(this IEndpoi private static async Task GetStripeCreateCustomerAsync( [FromBody] CustomerCreateOptions customerCreateOptions, - [FromServices] IStripePaymentService stripePaymentService, + [FromServices] IStripeCustomerService stripeCustomerService, [FromServices] IAuthorizationService authorizationService, HttpContext httpContext) { @@ -52,7 +52,7 @@ private static async Task GetStripeCreateCustomerAsync( return httpContext.ChallengeOrForbidApi(); } - var customer = await stripePaymentService.CreateCustomerAsync(customerCreateOptions); + var customer = await stripeCustomerService.CreateCustomerFromOptionsAsync(customerCreateOptions); return TypedResults.Ok(customer); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeSubscriptionEndpoint.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeSubscriptionEndpoint.cs index 81cc5fd7..588b923a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeSubscriptionEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Endpoints/Api/StripeSubscriptionEndpoint.cs @@ -6,9 +6,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using OrchardCore.Commerce.Endpoints; -using OrchardCore.Commerce.Payment.Stripe.Abstractions; using OrchardCore.Commerce.Payment.Stripe.Endpoints.Permissions; using OrchardCore.Commerce.Payment.Stripe.Extensions; +using OrchardCore.Commerce.Payment.Stripe.Services; using OrchardCore.Commerce.Payment.Stripe.ViewModels; using Stripe; using System.Collections.Generic; @@ -27,7 +27,8 @@ public static IEndpointRouteBuilder AddStripeCreateSubscriptionEndpoint(this IEn private static async Task GetStripeCreateSubscriptionAsync( [FromBody] StripeCreateSubscriptionViewModel viewModel, - [FromServices] IStripePaymentService stripePaymentService, + [FromServices] IStripeCustomerService stripeCustomerService, + [FromServices] IStripeSubscriptionService stripeSubscriptionService, [FromServices] IShoppingCartService shoppingCartService, [FromServices] IAuthorizationService authorizationService, HttpContext httpContext) @@ -77,12 +78,12 @@ private static async Task GetStripeCreateSubscriptionAsync( State = billingAddress.Province, }, }; - var customer = await stripePaymentService.CreateCustomerAsync(options); + var customer = await stripeCustomerService.CreateCustomerAsync(options); viewModel.CustomerId = customer.Id; } // Create the subscription. - var subscription = await stripePaymentService.CreateSubscriptionAsync(viewModel); - return TypedResults.Ok(subscription); + var response = await stripeSubscriptionService.CreateSubscriptionAsync(viewModel); + return TypedResults.Ok(response); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeCustomerService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeCustomerService.cs index 638d0677..6431ade1 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeCustomerService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeCustomerService.cs @@ -6,9 +6,23 @@ namespace OrchardCore.Commerce.Payment.Stripe.Services; public interface IStripeCustomerService { + Task CreateCustomerAsync(CustomerCreateOptions customerCreateOptions); Task GetFirstCustomerByEmailAsync(string customerEmail); Task GetCustomerByIdAsync(string customerId); - Task GetOrCreateCustomerAsync( + Task GetAndUpdateOrCreateCustomerAsync( + Address billingAddress, + Address shippingAddress, + string email, + string phone); + + Task CreateCustomerAsync( + Address billingAddress, + Address shippingAddress, + string email, + string phone); + + Task UpdateCustomerAsync( + string customerId, Address billingAddress, Address shippingAddress, string email, diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionEventHandler.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionEventHandler.cs index a65347c5..a4181bf2 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionEventHandler.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionEventHandler.cs @@ -1,11 +1,10 @@ -using OrchardCore.Commerce.Payment.Stripe.Models; -using Stripe; -using Stripe.Checkout; +using Stripe.Checkout; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Stripe.Services; public interface IStripeSessionEventHandler { - Task StripeSessionDataCreatingAsync(StripeSessionData sessionData, Session session, Customer customer); + Task StripeSessionCreatingAsync(SessionCreateOptions options) => Task.CompletedTask; + Task StripeSessionCreatedAsync(Session session, SessionCreateOptions options) => Task.CompletedTask; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionService.cs index 7a8c3d06..cbd5024b 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSessionService.cs @@ -1,7 +1,4 @@ -using OrchardCore.Commerce.Payment.Stripe.Models; -using Stripe; -using Stripe.Checkout; -using System.Collections.Generic; +using Stripe.Checkout; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Stripe.Services; @@ -9,10 +6,4 @@ namespace OrchardCore.Commerce.Payment.Stripe.Services; public interface IStripeSessionService { Task CreateSessionAsync(SessionCreateOptions options); - - Task SaveSessionDataAsync(Customer customer, Session session); - - Task> GetAllSessionDataAsync(string userId, string invoiceId, string sessionId); - - Task GetFirstSessionDataByInvoiceIdAsync(string invoiceId); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSubscriptionService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSubscriptionService.cs index cf8ad857..00ecacb3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSubscriptionService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/IStripeSubscriptionService.cs @@ -1,4 +1,6 @@ -using Stripe; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Payment.Stripe.ViewModels; +using Stripe; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Stripe.Services; @@ -7,4 +9,6 @@ public interface IStripeSubscriptionService { Task UpdateSubscriptionAsync(string subscriptionId, SubscriptionUpdateOptions options); Task GetSubscriptionAsync(string subscriptionId, SubscriptionGetOptions options); + Task CreateSubscriptionAsync(SubscriptionCreateOptions options); + Task CreateSubscriptionAsync(StripeCreateSubscriptionViewModel viewModel); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeCustomerService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeCustomerService.cs index 29070adf..0fc89a0a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeCustomerService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeCustomerService.cs @@ -57,12 +57,11 @@ public async Task GetFirstCustomerByEmailAsync(string customerEmail) } catch (StripeException stripeException) { - return null; } } - public async Task GetOrCreateCustomerAsync( + public async Task GetAndUpdateOrCreateCustomerAsync( Address billingAddress, Address shippingAddress, string email, @@ -77,44 +76,20 @@ public async Task GetOrCreateCustomerAsync( if (customer?.Id != null) { + customer = await UpdateCustomerAsync(customer.Id, billingAddress, shippingAddress, email, phone); return customer; } return await CreateCustomerAsync(billingAddress, shippingAddress, email, phone); } - private async Task CreateCustomerAsync(Address billingAddress, Address shippingAddress, string email, string phone) + public async Task CreateCustomerAsync( + Address billingAddress, + Address shippingAddress, + string email, + string phone) { - var customerCreateOptions = new CustomerCreateOptions - { - Name = billingAddress.Name, - Email = email, - Phone = phone, - Address = new AddressOptions - { - City = billingAddress.City, - Country = billingAddress.Region, - Line1 = billingAddress.StreetAddress1, - Line2 = billingAddress.StreetAddress2, - PostalCode = billingAddress.PostalCode, - State = billingAddress.Province, - }, - Shipping = shippingAddress?.Name != null - ? new ShippingOptions - { - Name = shippingAddress.Name, - Address = new AddressOptions - { - City = shippingAddress.City, - Country = shippingAddress.Region, - Line1 = shippingAddress.StreetAddress1, - Line2 = shippingAddress.StreetAddress2, - PostalCode = shippingAddress.PostalCode, - State = shippingAddress.Province, - }, - } - : null, - }; + var customerCreateOptions = PopulateCustomerCreateOptions(billingAddress, shippingAddress, email, phone); var customer = await _customerService.CreateAsync( customerCreateOptions, @@ -123,4 +98,79 @@ await _requestOptionsService.SetIdempotencyKeyAsync(), return customer; } + + public async Task CreateCustomerAsync(CustomerCreateOptions customerCreateOptions) + { + var customer = await _customerService.CreateAsync( + customerCreateOptions, + await _requestOptionsService.SetIdempotencyKeyAsync(), + cancellationToken: _hca.HttpContext.RequestAborted); + + return customer; + } + + public async Task UpdateCustomerAsync( + string customerId, + Address billingAddress, + Address shippingAddress, + string email, + string phone) + { + var customerUpdateOptions = PopulateCustomerUpdateOptions(billingAddress, shippingAddress, email, phone); + + var customer = await _customerService.UpdateAsync( + customerId, + customerUpdateOptions, + await _requestOptionsService.SetIdempotencyKeyAsync(), + cancellationToken: _hca.HttpContext.RequestAborted); + + return customer; + } + + private static CustomerUpdateOptions PopulateCustomerUpdateOptions( + Address billingAddress, + Address shippingAddress, + string email, + string phone) => + new() + { + Name = billingAddress.Name, + Email = email, + Phone = phone, + Address = CreateAddressOptions(billingAddress), + Shipping = CreateShippingOptions(shippingAddress), + }; + + private static CustomerCreateOptions PopulateCustomerCreateOptions( + Address billingAddress, + Address shippingAddress, + string email, + string phone) => + new() + { + Name = billingAddress.Name, + Email = email, + Phone = phone, + Address = CreateAddressOptions(billingAddress), + Shipping = CreateShippingOptions(shippingAddress), + }; + + private static AddressOptions CreateAddressOptions(Address address) => new() + { + City = address.City, + Country = address.Region, + Line1 = address.StreetAddress1, + Line2 = address.StreetAddress2, + PostalCode = address.PostalCode, + State = address.Province, + }; + + private static ShippingOptions CreateShippingOptions(Address shippingAddress) => + shippingAddress?.Name != null + ? new ShippingOptions + { + Name = shippingAddress.Name, + Address = CreateAddressOptions(shippingAddress), + } + : null; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index 8a2cb1b4..a89f122f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -21,7 +21,6 @@ using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.Settings; using Stripe; -using Stripe.Checkout; using System; using System.Collections.Generic; using System.Linq; @@ -35,9 +34,6 @@ public class StripePaymentService : IStripePaymentService { private readonly PaymentIntentService _paymentIntentService = new(); private readonly ConfirmationTokenService _confirmationTokenService = new(); - private readonly CustomerService _customerService = new(); - private readonly SubscriptionService _subscriptionService = new(); - private readonly SessionService _sessionService = new(); private readonly IHttpContextAccessor _httpContextAccessor; private readonly IContentManager _contentManager; private readonly ISiteService _siteService; @@ -72,68 +68,6 @@ public StripePaymentService( _httpContextAccessor = httpContextAccessor; } - public async Task CreateSubscriptionAsync(StripeCreateSubscriptionViewModel viewModel) - { - // Automatically save the payment method to the subscription - // when the first payment is successful. - var paymentSettings = new SubscriptionPaymentSettingsOptions - { - SaveDefaultPaymentMethod = "on_subscription", - }; - - var subscriptionOptions = new SubscriptionCreateOptions - { - Customer = viewModel.CustomerId, - PaymentSettings = paymentSettings, - PaymentBehavior = "default_incomplete", - }; - - foreach (var priceId in viewModel.PriceIds) - { - subscriptionOptions.Items.Add(new SubscriptionItemOptions { Price = priceId }); - } - - subscriptionOptions.AddExpand("latest_invoice.payment_intent"); - subscriptionOptions.AddExpand("pending_setup_intent"); - - var subscription = await _subscriptionService.CreateAsync( - subscriptionOptions, - await _requestOptionsService.SetIdempotencyKeyAsync(), - _httpContextAccessor.HttpContext.RequestAborted); - if (subscription.PendingSetupIntent != null) - { - return new SubscriptionCreateResponse - { - Type = "setup", - ClientSecret = subscription.PendingSetupIntent.ClientSecret, - }; - } - - return new SubscriptionCreateResponse - { - Type = "payment", - ClientSecret = subscription.LatestInvoice.PaymentIntent.ClientSecret, - }; - } - - public async Task CreateSessionAsync(string customerId) => - await _customerService.GetAsync( - customerId, - requestOptions: await _requestOptionsService.SetIdempotencyKeyAsync(), - cancellationToken: _httpContextAccessor.HttpContext.RequestAborted); - - public async Task GetCustomerAsync(string customerId) => - await _customerService.GetAsync( - customerId, - requestOptions: await _requestOptionsService.SetIdempotencyKeyAsync(), - cancellationToken: _httpContextAccessor.HttpContext.RequestAborted); - - public async Task CreateCustomerAsync(CustomerCreateOptions customerCreateOptions) => - await _customerService.CreateAsync( - customerCreateOptions, - await _requestOptionsService.SetIdempotencyKeyAsync(), - _httpContextAccessor.HttpContext.RequestAborted); - public async Task GetPublicKeyAsync() { var stripeApiSettings = (await _siteService.GetSiteSettingsAsync()).As(); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSessionService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSessionService.cs index 116cf414..cd4518da 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSessionService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSessionService.cs @@ -1,16 +1,9 @@ #nullable enable -using Lombiq.HelpfulLibraries.OrchardCore.Users; using Microsoft.AspNetCore.Http; using OrchardCore.Commerce.Payment.Stripe.Abstractions; -using OrchardCore.Commerce.Payment.Stripe.Indexes; -using OrchardCore.Commerce.Payment.Stripe.Models; -using Stripe; using Stripe.Checkout; using System.Collections.Generic; -using System.Text.Json; using System.Threading.Tasks; -using YesSql; -using ISession = YesSql.ISession; using Session = Stripe.Checkout.Session; namespace OrchardCore.Commerce.Payment.Stripe.Services; @@ -20,82 +13,28 @@ public class StripeSessionService : IStripeSessionService private readonly SessionService _sessionService = new(); private readonly IRequestOptionsService _requestOptionsService; private readonly IHttpContextAccessor _hca; - private readonly ISession _session; - private readonly ICachingUserManager _cachingUserManager; private readonly IEnumerable _stripeSessionEventHandlers; public StripeSessionService( IRequestOptionsService requestOptionsService, IHttpContextAccessor httpContextAccessor, - ISession session, - ICachingUserManager cachingUserManager, IEnumerable stripeSessionEventHandlers) { _requestOptionsService = requestOptionsService; _hca = httpContextAccessor; - _session = session; - _cachingUserManager = cachingUserManager; _stripeSessionEventHandlers = stripeSessionEventHandlers; } - public async Task CreateSessionAsync(SessionCreateOptions options) => - await _sessionService.CreateAsync( + public async Task CreateSessionAsync(SessionCreateOptions options) + { + await _stripeSessionEventHandlers.AwaitEachAsync(handler => handler.StripeSessionCreatingAsync(options)); + + var session = await _sessionService.CreateAsync( options, await _requestOptionsService.SetIdempotencyKeyAsync(), cancellationToken: _hca.HttpContext.RequestAborted); - // Create session content item and save it to the database - public async Task SaveSessionDataAsync(Customer customer, Session session) - { - var user = await _cachingUserManager.GetUserByEmailAsync(customer.Email); - if (user == null) - { - return new StripeSessionDataSave { Errors = ["User not found with email: " + customer.Email] }; - } - - var sessionData = await GetSessionDataQuery(user.UserId, session.InvoiceId, session.Id).FirstOrDefaultAsync(); - sessionData ??= new StripeSessionData(); - sessionData.UserId = user.UserId; - sessionData.StripeSessionId = session.Id; - sessionData.StripeSessionUrl = session.Url; - sessionData.StripeInvoiceId = session.InvoiceId; - sessionData.StripeCustomerId = customer.Id; - sessionData.SerializedAdditionalData = JsonSerializer.Serialize(session.Metadata); - - // Here you can override to the session data that will be saved. And e.g. give additional data in the serializeddata. - await _stripeSessionEventHandlers.AwaitEachAsync(handler => - handler.StripeSessionDataCreatingAsync(sessionData, session, customer)); - - await _session.SaveAsync(sessionData); - - return new StripeSessionDataSave { StripeSessionData = sessionData }; - } - - public Task> GetAllSessionDataAsync(string userId, string invoiceId, string sessionId) => - GetSessionDataQuery(userId, invoiceId, sessionId).ListAsync(); - - // Get session data by invoice id - public Task GetFirstSessionDataByInvoiceIdAsync(string invoiceId) => - GetSessionDataQuery(userId: string.Empty, invoiceId, sessionId: string.Empty).FirstOrDefaultAsync(); - - public IQuery GetSessionDataQuery(string userId, string invoiceId, string sessionId) - { - var query = _session.Query(); - if (!string.IsNullOrEmpty(userId)) - { - query = query.Where(item => item.UserId == userId); - } - - if (!string.IsNullOrEmpty(invoiceId)) - { - query = query.Where(item => item.StripeInvoiceId == invoiceId); - } - - if (!string.IsNullOrEmpty(sessionId)) - { - query = query.Where(item => item.StripeSessionId == sessionId); - } - - return query; + await _stripeSessionEventHandlers.AwaitEachAsync(handler => handler.StripeSessionCreatedAsync(session, options)); + return session; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSubscriptionService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSubscriptionService.cs index ee5e46c9..dacf7c50 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSubscriptionService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeSubscriptionService.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Payment.Stripe.ViewModels; using Stripe; using System.Threading.Tasks; @@ -17,6 +19,57 @@ public StripeSubscriptionService(IRequestOptionsService requestOptionsService, I _hca = httpContextAccessor; } + public async Task CreateSubscriptionAsync(StripeCreateSubscriptionViewModel viewModel) + { + // Automatically save the payment method to the subscription + // when the first payment is successful. + var paymentSettings = new SubscriptionPaymentSettingsOptions + { + SaveDefaultPaymentMethod = "on_subscription", + }; + + var subscriptionOptions = new SubscriptionCreateOptions + { + Customer = viewModel.CustomerId, + PaymentSettings = paymentSettings, + PaymentBehavior = "default_incomplete", + }; + + foreach (var priceId in viewModel.PriceIds) + { + subscriptionOptions.Items.Add(new SubscriptionItemOptions { Price = priceId }); + } + + subscriptionOptions.AddExpand("latest_invoice.payment_intent"); + subscriptionOptions.AddExpand("pending_setup_intent"); + + var subscription = await _subscriptionService.CreateAsync( + subscriptionOptions, + requestOptions: await _requestOptionsService.SetIdempotencyKeyAsync(), + cancellationToken: _hca.HttpContext.RequestAborted); + + if (subscription.PendingSetupIntent != null) + { + return new SubscriptionCreateResponse + { + Type = "setup", + ClientSecret = subscription.PendingSetupIntent.ClientSecret, + }; + } + + return new SubscriptionCreateResponse + { + Type = "payment", + ClientSecret = subscription.LatestInvoice.PaymentIntent.ClientSecret, + }; + } + + public async Task CreateSubscriptionAsync(SubscriptionCreateOptions options) => + await _subscriptionService.CreateAsync( + options, + requestOptions: await _requestOptionsService.SetIdempotencyKeyAsync(), + cancellationToken: _hca.HttpContext.RequestAborted); + public async Task UpdateSubscriptionAsync(string subscriptionId, SubscriptionUpdateOptions options) => await _subscriptionService.UpdateAsync( subscriptionId, diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/SubscriptionStripeWebhookEventHandler.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/SubscriptionStripeWebhookEventHandler.cs index e3c40396..e8ec012c 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/SubscriptionStripeWebhookEventHandler.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/SubscriptionStripeWebhookEventHandler.cs @@ -49,13 +49,15 @@ public async Task ReceivedStripeEventAsync(Event stripeEvent) return; } - // Get session data for the invoice Id + // Get current subscription if exists, if exists do not override start date + // Handle case when subscription isn't active, or someone else payed for the tenant. + // Might be a good idea to do it in an officefreund own handler var subscriptionPart = new SubscriptionPart(); subscriptionPart.UserId.Text = user.UserId; subscriptionPart.Status.Text = SubscriptionStatuses.Active; subscriptionPart.StartDateUtc.Value = _clock.UtcNow; subscriptionPart.EndDateUtc.Value = invoice.PeriodEnd; - subscriptionPart.PaymentProviderName.Text = "Stripe"; + subscriptionPart.PaymentProviderName.Text = StripePaymentProvider.ProviderName; subscriptionPart.IdInPaymentProvider.Text = invoice.SubscriptionId; var stripeSubscription = await _stripeSubscriptionService.GetSubscriptionAsync(invoice.SubscriptionId, options: null); diff --git a/src/Modules/OrchardCore.Commerce/Migrations/SubscriptionMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/SubscriptionMigrations.cs index 3639c5d5..655177a2 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/SubscriptionMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/SubscriptionMigrations.cs @@ -49,6 +49,10 @@ await _contentDefinitionManager.AlterPartDefinitionAsync(build .WithDisplayName("User Id") .WithDescription("The user ID of the subscriber.")) .WithField(part => part.SerializedMetadata, field => field + .WithSettings(new TextFieldSettings + { + Hint = "Additional data about the subscription in Dictionary JSON serialized form.", + }) .WithDisplayName("Additional data") .WithDescription("Additional data about the subscription in JSON serialized form.")) .WithField(part => part.StartDateUtc, field => field