From 913d7b3836abeefeba056659fddc53942d93b59a Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 28 Aug 2024 14:10:55 -0400 Subject: [PATCH 01/15] Make StripePaymentPart stronger (#487) --- .../Services/StripePaymentService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index d1d8c977c..9fa543411 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -271,7 +271,11 @@ await _contentManager.GetAsync(orderId) is not { } order) }; } - order.Alter(part => part.RetryCounter++); + order.Alter(part => + { + part.PaymentIntentId.Text = fetchedPaymentIntent.Id; + part.RetryCounter++; + }); await _contentManager.UpdateAsync(order); if (order.As().RetryCounter <= 10) From 3eb9a9f31f8deaa734745f221f2a91a613276c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kriszti=C3=A1n=20N=C3=A9meth?= Date: Thu, 29 Aug 2024 11:12:20 +0200 Subject: [PATCH 02/15] Inferred parameters are not always recognized correctly (#485) --- .../EndPoints/Api/StripeEndpoint.cs | 4 +-- .../Endpoints/Api/PaymentEndpoint.cs | 18 +++++------ .../Endpoints/Api/ShoppingCartLineEndpoint.cs | 32 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs index 5e02f7a27..a1a6112a7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs @@ -19,8 +19,8 @@ public static IEndpointRouteBuilder AddStripeMiddlewareEndpoint(this IEndpointRo } private static async Task AddStripeMiddlewareAsync( - string? shoppingCartId, - IStripePaymentService stripePaymentService, + [FromRoute] string? shoppingCartId, + [FromServices] IStripePaymentService stripePaymentService, [FromQuery(Name = "payment_intent")] string? paymentIntent = null ) { diff --git a/src/Modules/OrchardCore.Commerce/Endpoints/Api/PaymentEndpoint.cs b/src/Modules/OrchardCore.Commerce/Endpoints/Api/PaymentEndpoint.cs index ae434abf9..5687859b0 100644 --- a/src/Modules/OrchardCore.Commerce/Endpoints/Api/PaymentEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce/Endpoints/Api/PaymentEndpoint.cs @@ -23,10 +23,10 @@ public static IEndpointRouteBuilder AddFreeEndpoint(this IEndpointRouteBuilder b [Authorize(AuthenticationSchemes = "Api")] private static async Task AddFreeAsync( - string? shoppingCartId, - IAuthorizationService authorizationService, - HttpContext httpContext, - IPaymentService paymentService + [FromRoute] string? shoppingCartId, + [FromServices] IAuthorizationService authorizationService, + [FromServices] HttpContext httpContext, + [FromServices] IPaymentService paymentService ) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApi)) @@ -49,12 +49,12 @@ public static IEndpointRouteBuilder AddCallbackEndpoint(this IEndpointRouteBuild [Authorize(AuthenticationSchemes = "Api")] private static async Task AddCallbackAsync( - string paymentProviderName, - string? orderId, + [FromRoute] string paymentProviderName, + [FromRoute] string? orderId, [FromQuery] string? shoppingCartId, - IAuthorizationService authorizationService, - HttpContext httpContext, - IPaymentService paymentService + [FromServices] IAuthorizationService authorizationService, + [FromServices] HttpContext httpContext, + [FromServices] IPaymentService paymentService ) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApi)) diff --git a/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs b/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs index 02b428d45..6b3db2f8f 100644 --- a/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs @@ -27,10 +27,10 @@ public static IEndpointRouteBuilder AddAddItemEndpoint(this IEndpointRouteBuilde [Authorize(AuthenticationSchemes = "Api")] private static async Task AddItemAsync( [FromBody] AddItemViewModel addItemVM, - IAuthorizationService authorizationService, - HttpContext httpContext, - IShoppingCartService shoppingCartService, - IHtmlLocalizer htmlLocalizer + [FromServices] IAuthorizationService authorizationService, + [FromServices] HttpContext httpContext, + [FromServices] IShoppingCartService shoppingCartService, + [FromServices] IHtmlLocalizer htmlLocalizer ) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApi)) @@ -67,10 +67,10 @@ public static IEndpointRouteBuilder AddUpdateEndpoint(this IEndpointRouteBuilder [Authorize(AuthenticationSchemes = "Api")] private static async Task UpdateAsync( [FromBody] UpdateViewModel updateVM, - IAuthorizationService authorizationService, - HttpContext httpContext, - IShoppingCartService shoppingCartService, - IHtmlLocalizer htmlLocalizer + [FromServices] IAuthorizationService authorizationService, + [FromServices] HttpContext httpContext, + [FromServices] IShoppingCartService shoppingCartService, + [FromServices] IHtmlLocalizer htmlLocalizer ) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApi)) @@ -105,10 +105,10 @@ public static IEndpointRouteBuilder AddRemoveLineEndpoint(this IEndpointRouteBui [Authorize(AuthenticationSchemes = "Api")] private static async Task RemoveLineAsync( [FromBody] RemoveLineViewModel removeLineVM, - IAuthorizationService authorizationService, - HttpContext httpContext, - IShoppingCartService shoppingCartService, - IHtmlLocalizer htmlLocalizer + [FromServices] IAuthorizationService authorizationService, + [FromServices] HttpContext httpContext, + [FromServices] IShoppingCartService shoppingCartService, + [FromServices] IHtmlLocalizer htmlLocalizer ) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApi)) @@ -143,10 +143,10 @@ public static IEndpointRouteBuilder AddRetrieveAsyncEndpoint(this IEndpointRoute [Authorize(AuthenticationSchemes = "Api")] private static async Task RetrieveAsync( - string? shoppingCartId, - IAuthorizationService authorizationService, - HttpContext httpContext, - IShoppingCartPersistence shoppingCartPersistence + [FromRoute] string? shoppingCartId, + [FromServices] IAuthorizationService authorizationService, + [FromServices] HttpContext httpContext, + [FromServices] IShoppingCartPersistence shoppingCartPersistence ) { if (!await authorizationService.AuthorizeAsync(httpContext.User, ApiPermissions.CommerceApi)) From 4b50375d6bda8e041d2f1dcb72842b44ef7d0189 Mon Sep 17 00:00:00 2001 From: andy Date: Fri, 30 Aug 2024 05:10:03 -0400 Subject: [PATCH 03/15] Make window.stripePaymentForm more abstract (#473) * Make window.stripePaymentForm more abstract * use Default parameters --- .../Assets/Scripts/stripe-payment-form.js | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js index 3256bb369..7b496611d 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js @@ -6,21 +6,30 @@ window.stripePaymentForm = function stripePaymentForm( urlPrefix, errorText, missingText, - updatePaymentIntentUrl) { - const allErrorContainers = [document.querySelector('.message-error')]; - const form = document.querySelector('.payment-form'); - const submitButton = form.querySelector('.pay-button-stripe'); - const payText = submitButton.querySelector('.pay-text'); - const paymentProcessingContainer = submitButton.querySelector('.payment-processing-container'); - const stripeElements = stripe.elements({ - clientSecret, - }); - const payment = stripeElements.create('payment', { - fields: { - billingDetails: 'never', - }, - }); - const placeOfPayment = document.querySelector('#payment-form_payment'); + updatePaymentIntentUrl, + validateUrl = 'checkout/validate/Stripe', + paramsUrl = 'checkout/params/Stripe', + priceUrl = 'checkout/price', + errorContainerSelector = '.message-error', + stripeFieldErrorSelector = '.stripe-field-error', + paymentFormSelector = '.payment-form', + payButtonSelector = '.pay-button-stripe', + payTextSelector = '.pay-text', + paymentProcessingContainerSelector = '.payment-processing-container', + placeOfPaymentSelector = '#payment-form_payment', + payButtonValueSelector = '.pay-button-value', + addressesSelector = '*[id^="OrderPart_ShippingAddress_"], *[id^="OrderPart_BillingAddress_"]', + addressSelector = '.address', + addressTitleSelector = '.address__title' +) { + const allErrorContainers = [document.querySelector(errorContainerSelector)]; + const form = document.querySelector(paymentFormSelector); + const submitButton = form.querySelector(payButtonSelector); + const payText = submitButton.querySelector(payTextSelector); + const paymentProcessingContainer = submitButton.querySelector(paymentProcessingContainerSelector); + const stripeElements = stripe.elements({ clientSecret }); + const payment = stripeElements.create('payment', { fields: { billingDetails: 'never' } }); + const placeOfPayment = document.querySelector(placeOfPaymentSelector); let formElements = Array.from(form.elements); @@ -30,9 +39,7 @@ window.stripePaymentForm = function stripePaymentForm( }); payment.update({ disabled: !enable }); - submitButton.disabled = !enable; - paymentProcessingContainer.hidden = enable; payText.hidden = !enable; } @@ -45,14 +52,13 @@ window.stripePaymentForm = function stripePaymentForm( container.innerHTML = '
    '; const ul = container.querySelector('ul'); - for (let i = 0; i < err.length; i++) { + err.forEach((error) => { const li = document.createElement('li'); - li.textContent = Object.prototype.hasOwnProperty.call(err[i], 'message') ? err[i].message : err[i]; + li.textContent = error.message || error; ul.appendChild(li); - } + }); toggleInputs(true); - container.hidden = false; container.scrollIntoView({ block: 'center' }); } @@ -68,7 +74,7 @@ window.stripePaymentForm = function stripePaymentForm( function registerElements() { // Displaying payment input error. - const stripeFieldError = document.querySelector('.stripe-field-error'); + const stripeFieldError = document.querySelector(stripeFieldErrorSelector); allErrorContainers.push(stripeFieldError); payment.on('change', (event) => { displayError(event?.error, stripeFieldError); @@ -80,7 +86,6 @@ window.stripePaymentForm = function stripePaymentForm( toggleInputs(false); const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret); - await fetch(updatePaymentIntentUrl.replace('PAYMENT_INTENT', paymentIntent.id)); let result; @@ -94,29 +99,28 @@ window.stripePaymentForm = function stripePaymentForm( throw emptyRequiredFields .map((element) => document.querySelector(`label[for="${element.id}"]`)) .filter(getText) - .filter((label) => !label.closest('.address')?.hidden) + .filter((label) => !label.closest(addressSelector)?.hidden) .map((label) => { - const title = getText(label.closest('.address')?.querySelector('.address__title')); + const title = getText(label.closest(addressSelector)?.querySelector(addressTitleSelector)); const name = title ? `${title} ${getText(label)}` : getText(label); - return missingText.replace('%LABEL%', name); }); } - const validationJson = await fetchPost('checkout/validate/Stripe'); + const validationJson = await fetchPost(validateUrl); if (validationJson?.errors?.length) { toggleInputs(true); throw validationJson.errors; } + result = await stripe.confirmPayment({ elements: stripeElements, - confirmParams: await fetchPost('checkout/params/Stripe'), + confirmParams: await fetchPost(paramsUrl), }); displayError(result.error); - } - catch (error) { + } catch (error) { result = { error }; displayError(result.error); } @@ -125,18 +129,18 @@ window.stripePaymentForm = function stripePaymentForm( function registerPriceUpdater() { let debounce = false; - Array.from(document.querySelectorAll('*[id^="OrderPart_ShippingAddress_"], *[id^="OrderPart_BillingAddress_"]')) + Array.from(document.querySelectorAll(addressesSelector)) .forEach((element) => element.addEventListener('change', () => { if (debounce) return; - const payButtonValue = document.querySelector('.pay-button-value'); + const payButtonValue = document.querySelector(payButtonValueSelector); if (!payButtonValue) return; debounce = true; submitButton.disabled = true; setTimeout(async () => { - const priceJson = await fetchPost('checkout/price'); + const priceJson = await fetchPost(priceUrl); debounce = false; submitButton.disabled = false; @@ -157,7 +161,6 @@ window.stripePaymentForm = function stripePaymentForm( // Refreshing form elements with the payment input. formElements = Array.from(form.elements); registerElements(); - registerPriceUpdater(); } }; From facd4acfca39a04b3639fcfa2a8470e62e4bed08 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 15 Sep 2024 13:52:44 -0400 Subject: [PATCH 04/15] Pass PaymentIntentId to Client for convenience of API calling (#491) * Pass PaymentIntentId to Client for convenience of API calling * Add bool needToJudgeIntentStorage = true * Add more accurate comment for PaymentConfirmationAsync method. * use Payment Intent * fixes spelling --- .../Abstractions/IStripePaymentService.cs | 10 +++++++++- .../Models/StripePaymentProviderData.cs | 3 ++- .../Services/StripePaymentProvider.cs | 1 + .../Services/StripePaymentService.cs | 13 +++++++------ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs index 99dd74adc..107882c1e 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs @@ -3,6 +3,7 @@ using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Payment.Stripe.Services; using OrchardCore.Commerce.Payment.ViewModels; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.ModelBinding; @@ -72,5 +73,12 @@ string shoppingCartId /// /// Confirm the result of Stripe payment. /// - Task PaymentConfirmationAsync(string paymentIntent, string shoppingCartId); + /// The Payment Intent Id from Stripe. + /// The Shopping Cart Id of this application. + /// To judge if this method should use the storage of . + /// The status of payment operation. + Task PaymentConfirmationAsync( + string paymentIntentId, + string shoppingCartId, + bool needToJudgeIntentStorage = true); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentProviderData.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentProviderData.cs index 405962960..97c20d593 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentProviderData.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentProviderData.cs @@ -1,7 +1,8 @@ -namespace OrchardCore.Commerce.Payment.Stripe.Models; +namespace OrchardCore.Commerce.Payment.Stripe.Models; public class StripePaymentProviderData { public string PublishableKey { get; set; } public string ClientSecret { get; set; } + public string PaymentIntentId { get; set; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs index 48d1cc1af..2bb0dffaa 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs @@ -62,6 +62,7 @@ await _session.SaveAsync(new OrderPayment { PublishableKey = (await _siteService.GetSiteSettingsAsync()).As().PublishableKey, ClientSecret = paymentIntent.ClientSecret, + PaymentIntentId = paymentIntent.Id, }; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index 9fa543411..052a018c9 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -211,12 +211,13 @@ await _session.SaveAsync(new OrderPayment } public async Task PaymentConfirmationAsync( - string paymentIntent, - string shoppingCartId + string paymentIntentId, + string shoppingCartId, + bool needToJudgeIntentStorage = true ) { // If it is null it means the session was not loaded yet and a redirect is needed. - if (string.IsNullOrEmpty(_paymentIntentPersistence.Retrieve())) + if (needToJudgeIntentStorage && string.IsNullOrEmpty(_paymentIntentPersistence.Retrieve())) { return new PaymentOperationStatusViewModel { @@ -227,8 +228,8 @@ string shoppingCartId // If we can't find a valid payment intent based on ID or if we can't find the associated order, then something // went wrong and continuing from here would only cause a crash anyway. - if (await GetPaymentIntentAsync(paymentIntent) is not { PaymentMethod: not null } fetchedPaymentIntent || - (await GetOrderPaymentByPaymentIntentIdAsync(paymentIntent))?.OrderId is not { } orderId || + if (await GetPaymentIntentAsync(paymentIntentId) is not { PaymentMethod: not null } fetchedPaymentIntent || + (await GetOrderPaymentByPaymentIntentIdAsync(paymentIntentId))?.OrderId is not { } orderId || await _contentManager.GetAsync(orderId) is not { } order) { return new PaymentOperationStatusViewModel @@ -236,7 +237,7 @@ await _contentManager.GetAsync(orderId) is not { } order) Status = PaymentOperationStatus.NotFound, ShowMessage = H[ "Couldn't find the payment intent \"{0}\" or the order associated with it.", - paymentIntent ?? string.Empty], + paymentIntentId ?? string.Empty], }; } From c6874871f3af517b8507c34e18628d850c60d796 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 15 Sep 2024 14:00:04 -0400 Subject: [PATCH 05/15] Fixes a error of Update API (#496) * Move Validate logic to the service * Fixes a logic error on update API * clean * Do not need isValid --- .../Abstractions/IPaymentService.cs | 8 ++++ .../Controllers/PaymentController.cs | 38 +------------------ .../Services/PaymentService.cs | 38 ++++++++++++++++++- .../Endpoints/Services/ShoppingCartService.cs | 9 ++--- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs index a69574ec3..88d8ce873 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs @@ -87,6 +87,14 @@ Task UpdateOrderToOrderedAsync( /// Free checkout. /// Task CheckoutWithoutPaymentAsync(string? shoppingCartId, bool mustBeFree = true); + + /// + /// Validate the checkout. + /// + /// The name of provider. + /// Shopping Cart. + /// The list of the errors. + Task> ValidateErrorsAsync(string providerName, string? shoppingCartId); } public delegate Task AlterOrderAsyncDelegate( diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index f4557839c..94902a18a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -1,33 +1,27 @@ using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType.Extensions; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.ViewModels; using OrchardCore.ContentManagement; -using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Controllers; public class PaymentController : PaymentBaseController { private readonly IAuthorizationService _authorizationService; - private readonly ILogger _logger; private readonly IContentManager _contentManager; - private readonly IUpdateModelAccessor _updateModelAccessor; private readonly IStringLocalizer T; private readonly IHtmlLocalizer H; private readonly INotifier _notifier; @@ -35,16 +29,13 @@ public class PaymentController : PaymentBaseController private readonly IPaymentService _paymentService; public PaymentController( IOrchardServices services, - IUpdateModelAccessor updateModelAccessor, INotifier notifier, IEnumerable paymentProviders, IPaymentService paymentService) : base(notifier, services.Logger.Value) { _authorizationService = services.AuthorizationService.Value; - _logger = services.Logger.Value; _contentManager = services.ContentManager.Value; - _updateModelAccessor = updateModelAccessor; T = services.StringLocalizer.Value; H = services.HtmlLocalizer.Value; _notifier = notifier; @@ -104,33 +95,8 @@ public async Task Validate(string providerName, [FromQuery] strin { if (string.IsNullOrEmpty(providerName)) return NotFound(); - try - { - await _paymentProviders - .WhereName(providerName) - .AwaitEachAsync(provider => provider.ValidateAsync(_updateModelAccessor, shoppingCartId)); - - var errors = _updateModelAccessor.ModelUpdater.GetModelErrorMessages().ToList(); - return Json(new { Errors = errors }); - } - catch (FrontendException exception) - { - return Json(new { Errors = exception.HtmlMessages }); - } - catch (Exception exception) - { - var shoppingCartIdForDisplay = shoppingCartId == null ? "(null)" : $"\"{shoppingCartId}\""; - - _logger.LogError( - exception, - "An exception has occurred during checkout form validation for shopping cart ID {ShoppingCartId}.", - shoppingCartIdForDisplay); - var errorMessage = HttpContext.IsDevelopmentAndLocalhost() - ? exception.ToString() - : T["An exception has occurred during checkout form validation for shopping cart ID {0}.", shoppingCartIdForDisplay].Value; - - return Json(new { Errors = new[] { errorMessage } }); - } + var errors = await _paymentService.ValidateErrorsAsync(providerName, shoppingCartId); + return Json(new { Errors = errors }); } [HttpGet("checkout/paymentrequest/{orderId}")] diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 0e7ae08a9..08d3b16f7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; @@ -46,7 +47,7 @@ public class PaymentService : IPaymentService private readonly INotifier _notifier; private readonly IMoneyService _moneyService; private readonly IHtmlLocalizer H; - + private readonly ILogger _logger; // We need all of them. #pragma warning disable S107 // Methods should not have too many parameters public PaymentService( @@ -77,6 +78,7 @@ public PaymentService( _moneyService = moneyService; _hca = services.HttpContextAccessor.Value; H = services.HtmlLocalizer.Value; + _logger = services.Logger.Value; } public async Task CreateCheckoutViewModelAsync( @@ -401,4 +403,38 @@ public async Task UpdateOrderToOrderedAsync( return (order, isNew); } + + public async Task> ValidateErrorsAsync(string providerName, string? shoppingCartId) + { + var errors = new List(); + try + { + await _paymentProvidersLazy + .Value + .WhereName(providerName) + .AwaitEachAsync(provider => provider.ValidateAsync(_updateModelAccessor, shoppingCartId)); + + errors = _updateModelAccessor.ModelUpdater.GetModelErrorMessages().ToList(); + } + catch (FrontendException exception) + { + errors = exception.HtmlMessages.Select(m => m.Value).ToList(); + } + catch (Exception exception) + { + var shoppingCartIdForDisplay = shoppingCartId == null ? "(null)" : $"\"{shoppingCartId}\""; + + _logger.LogError( + exception, + "An exception has occurred during checkout form validation for shopping cart ID {ShoppingCartId}.", + shoppingCartIdForDisplay); + var errorMessage = _hca.HttpContext.IsDevelopmentAndLocalhost() + ? exception.ToString() + : H["An exception has occurred during checkout form validation for shopping cart ID {0}.", shoppingCartIdForDisplay].Value; + + errors.Add(errorMessage); + } + + return errors; + } } diff --git a/src/Modules/OrchardCore.Commerce/Endpoints/Services/ShoppingCartService.cs b/src/Modules/OrchardCore.Commerce/Endpoints/Services/ShoppingCartService.cs index 820de6cfe..a7569a69e 100644 --- a/src/Modules/OrchardCore.Commerce/Endpoints/Services/ShoppingCartService.cs +++ b/src/Modules/OrchardCore.Commerce/Endpoints/Services/ShoppingCartService.cs @@ -111,25 +111,22 @@ public async Task UpdateAsync(ShoppingCartUpdateModel cart, string token foreach (var (line, item) in lines) { - var isValid = true; - await _workflowManagers.TriggerEventAsync( new { LineItem = item }, $"ShoppingCart-{token}-{shoppingCartId}"); + var sb = new StringBuilder(); foreach (var shoppingCartEvent in _shoppingCartEvents.OrderBy(provider => provider.Order)) { if (await shoppingCartEvent.VerifyingItemAsync(item) is { } errorMessage) { - var sb = new StringBuilder(); sb.AppendLine(errorMessage.Value); - errored = sb.ToString(); - isValid = false; } } + errored = sb.ToString(); // Preserve invalid lines in the cart, but modify their Quantity values to valid ones. - if (!isValid) + if (!string.IsNullOrEmpty(errored)) { var minOrderQuantity = (await _productService.GetProductAsync(line.ProductSku)) .As().MinimumOrderQuantity.Value; From 002223b66d9bfeeda8d786e8af7f43682688f939 Mon Sep 17 00:00:00 2001 From: andy Date: Tue, 24 Sep 2024 08:37:55 -0400 Subject: [PATCH 06/15] Tweet interface IStripePaymentService for flexible situations (#501) --- .../Abstractions/IStripePaymentService.cs | 4 ++-- .../Controllers/WebhookController.cs | 2 +- .../Services/StripePaymentService.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs index 107882c1e..2eb4c9aef 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs @@ -45,9 +45,9 @@ public interface IStripePaymentService Task UpdateOrderToOrderedAsync(PaymentIntent paymentIntent, string shoppingCartId); /// - /// Updates the corresponding order status to failed payment for the given . + /// Updates the corresponding order status to failed payment for the given paymentIntentId. /// - Task UpdateOrderToPaymentFailedAsync(PaymentIntent paymentIntent); + Task UpdateOrderToPaymentFailedAsync(string paymentIntentId); /// /// Return the saved for the given . diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs index 8f5098884..d5e835216 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs @@ -64,7 +64,7 @@ public async Task Index() else if (stripeEvent.Type == Events.PaymentIntentPaymentFailed) { var paymentIntent = stripeEvent.Data.Object as PaymentIntent; - await _stripePaymentService.UpdateOrderToPaymentFailedAsync(paymentIntent); + await _stripePaymentService.UpdateOrderToPaymentFailedAsync(paymentIntent.Id); } return Ok(); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index 052a018c9..52690a4e1 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -140,9 +140,9 @@ string shoppingCartId private static Func> CreateChargesProvider(PaymentIntent paymentIntent) => orderPart => orderPart.Charges.Select(charge => paymentIntent.CreatePayment(charge.Amount)); - public async Task UpdateOrderToPaymentFailedAsync(PaymentIntent paymentIntent) + public async Task UpdateOrderToPaymentFailedAsync(string paymentIntentId) { - var order = await GetOrderByPaymentIntentIdAsync(paymentIntent.Id); + var order = await GetOrderByPaymentIntentIdAsync(paymentIntentId); order.Alter(orderPart => orderPart.Status = new TextField { ContentItem = order, Text = OrderStatusCodes.PaymentFailed }); From d3cb2bf0338ab6fff164a8ae0a5396b22ed0fe04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= <92299130+Psichorex@users.noreply.github.com> Date: Sun, 29 Sep 2024 20:00:20 +0200 Subject: [PATCH 07/15] OCC-194: Upgrade to Orchard Core 2.0 once it's released and remove Newtonsoft.Json from the code base (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * OCC-245: Upgrade to latest OC preview to test System.Text.Json (#454) Co-authored-by: Mike Alhayek Co-authored-by: Zoltán Lehóczky * Upgrade OC packages and drivers (#480) * Upgrade OC packages and drivers * Update src/Modules/OrchardCore.Commerce/Drivers/PriceDisplaySettingsDisplayDriver.cs Co-authored-by: Sára El-Saig * Update src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs Co-authored-by: Sára El-Saig * Update src/Modules/OrchardCore.Commerce.ContentFields/Settings/PriceFieldSettingsDriver.cs Co-authored-by: Sára El-Saig * Update src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs Co-authored-by: Sára El-Saig * Fix all drivers --------- Co-authored-by: Sára El-Saig * OCC-280: Use new HL extension methods (#482) * Post merge fixup. * NEST-536: Update OC version (#474) * Update OC version. * Update OC preview version again (sigh). * Delete temporary extension. * Use the new AddDeployment extension that registers the underlying polymorphic JSON type too. * Suppress. * Update OC previews. * Update OC preview version. * Update OC preview version. * Rename to localizer. * Update OC preview version. * Update OC preview version. * Add new line --------- Co-authored-by: Hisham Bin Ateya * Re-enabling non preview packages and upgrading to OC 2.0.0 * Using System.Text.Json. * Setting the .Web project to OC 2.0.2. * Update src/OrchardCore.Commerce.Web/OrchardCore.Commerce.Web.csproj --------- Co-authored-by: Sára El-Saig Co-authored-by: Mike Alhayek Co-authored-by: Zoltán Lehóczky Co-authored-by: Hisham Bin Ateya --- .github/actions/spelling/allow/occ.txt | 3 +- Directory.Packages.props | 16 +- NuGet.config | 34 + Nuget.config | 21 - OrchardCore.Commerce.sln | 8 +- docs/guides/create-webshop.md | 2 +- docs/releases/3.0.0.md | 30 + mkdocs.yml | 4 +- .../Abstractions/IPayment.cs | 35 - .../IProductAttributeDeserializer.cs | 8 +- .../Abstractions/IProductAttributeValue.cs | 2 +- .../Abstractions/IUserService.cs | 7 +- .../Models/OrderLineItem.cs | 10 +- .../Models/OrderPart.cs | 18 +- .../Models/Payment.cs | 6 +- .../Models/PrioritizedPrice.cs | 5 +- .../Models/ShoppingCart.cs | 2 - .../Models/ShoppingCartItem.cs | 5 +- .../RawProductAttributeValue.cs | 5 +- .../LegacyPrioritizedPriceConverter.cs | 59 - ...LegacyRawProductAttributeValueConverter.cs | 20 - .../LegacyShoppingCartItemConverter.cs | 85 - .../PrioritizedPriceConverter.cs | 16 +- .../ProductAttributeValueConverter.cs | 65 +- .../ShoppingCartItemConverter.cs | 2 +- .../ViewModels/ShoppingCartLineViewModel.cs | 4 +- .../Amount.cs | 9 +- .../Currency.cs | 1 - .../Extensions/JObjectExtensions.cs | 17 - .../OrchardCore.Commerce.MoneyDataType.csproj | 1 - .../Serialization/AmountConverter.cs | 51 +- .../Serialization/CurrencyConverter.cs | 3 +- .../Serialization/LegacyAmountConverter.cs | 97 - .../Serialization/LegacyCurrencyConverter.cs | 19 - .../Drivers/AddressFieldDisplayDriver.cs | 19 +- .../Drivers/PriceFieldDisplayDriver.cs | 35 +- .../Extensions/UpdateModelExtensions.cs | 7 +- .../Services/FieldsOnlyDisplayManager.cs | 2 +- .../Settings/AddressFieldSettingsDriver.cs | 23 +- .../Settings/PriceFieldSettingsDriver.cs | 29 +- .../Startup.cs | 1 - .../Views/AddressField.cshtml | 5 +- ...sFieldEditor-WesternCommonNameParts.cshtml | 11 +- .../Views/AddressFieldEditor.cshtml | 4 +- .../Views/AddressField_Edit.cshtml | 8 +- .../Views/PriceField.Edit.cshtml | 3 +- .../pnpm-lock.yaml | 1733 +++++++++-------- .../Drivers/InventoryPartDisplayDriver.cs | 76 +- .../Drivers/ExactlySettingsDisplayDriver.cs | 45 +- .../Models/ChargeResponse.cs | 2 +- .../Services/ExactlyService.cs | 5 +- .../Views/CheckoutExactly.cshtml | 1 - .../Controllers/StripeController.cs | 5 +- .../Drivers/StripeApiSettingsDisplayDriver.cs | 82 +- .../Extensions/PaymentExtensions.cs | 7 +- .../StripeContentSecurityPolicyProvider.cs | 1 - .../Services/StripePaymentService.cs | 7 +- .../Views/CheckoutStripe.cshtml | 1 - .../Views/StripeApiSettings.Edit.cshtml | 1 - .../pnpm-lock.yaml | 1733 +++++++++-------- .../Abstractions/IPaymentService.cs | 4 +- .../Services/DummyPaymentProvider.cs | 2 +- .../Services/PaymentService.cs | 2 +- .../Views/PayButton.cshtml | 5 +- .../Views/Payment/PaymentRequest.cshtml | 3 +- .../Views/Payment/Success.cshtml | 4 +- .../pnpm-lock.yaml | 1733 +++++++++-------- .../Extensions/AdditionalDataExtensions.cs | 17 +- .../Views/DiscountPart.cshtml | 2 +- .../Views/DiscountPart_UpdateScript.cshtml | 2 +- .../Drivers/TaxRateSettingsDisplayDriver.cs | 84 +- .../Extensions/AdditionalDataExtensions.cs | 26 +- .../OrchardCore.Commerce.Tax/Startup.cs | 3 +- .../OrchardCoreCommerceConfigureOrder.cs | 7 + .../Controllers/ShoppingCartController.cs | 1 - .../Drivers/DiscountPartDisplayDriver.cs | 1 - ...OrderContentTypeDefinitionDisplayDriver.cs | 59 +- .../Drivers/OrderPartDisplayDriver.cs | 57 +- .../PriceDisplaySettingsDisplayDriver.cs | 39 +- .../Drivers/PriceVariantsPartDisplayDriver.cs | 33 +- .../Drivers/ProductAttributeFieldDriver.cs | 1 - .../Drivers/ProductPartDisplayDriver.cs | 7 +- .../Drivers/RegionSettingsDisplayDriver.cs | 57 +- .../Drivers/TaxRateTaxPartDisplayDriver.cs | 1 - .../Drivers/TieredPricePartDisplayDriver.cs | 109 +- .../Extentions/HttpContextExtensions.cs | 14 - .../Events/UserAddressFieldEvents.cs | 15 +- .../Events/UserSettingsOrderEvents.cs | 6 +- .../Events/WorkflowShoppingCartEvents.cs | 70 +- .../Extensions/JsonExtensions.cs | 21 - .../Fields/ProductAttributeField.cs | 11 +- .../Handlers/PricePartHandler.cs | 4 +- .../Liquid/AmountConverterFilter.cs | 17 +- .../Liquid/AmountToStringLiquidFilter.cs | 60 + ...temViewModelsAndTaxRatesConverterFilter.cs | 41 - .../OrderPartToOrderSummaryLiquidFilter.cs | 66 + .../Liquid/ProductFilter.cs | 11 +- .../LocalizationCurrencyRedirectMiddleware.cs | 1 - .../Migrations/OrderMigrations.cs | 81 +- .../OrchardCore.Commerce/Models/PricePart.cs | 4 +- ...e.Content.MultiCurrencyProduct.recipe.json | 2 +- ...dCore.Commerce.Content.Product.recipe.json | 2 +- ...dCore.Commerce.Samples.Product.recipe.json | 24 +- .../ContentLocalizationProductService.cs | 1 - .../Services/GlobalDiscountProvider.cs | 6 +- .../Services/MoneyService.cs | 11 +- .../Services/ProductAttributeProvider.cs | 5 +- .../Services/ProductAttributeService.cs | 2 +- .../Services/ShoppingCartPersistenceBase.cs | 7 +- .../Services/TaxRateTaxProvider.cs | 1 - .../TextProductAttributeDeserializer.cs | 14 +- .../Services/UserService.cs | 13 +- .../Settings/CurrencySettingsDisplayDriver.cs | 79 +- .../Settings/PricePartSettings.cs | 6 +- .../PricePartSettingsDisplayDriver.cs | 6 +- .../Settings/ProductAttributeFieldSettings.cs | 47 +- .../ProductAttributeFieldSettingsDriver.cs | 23 +- src/Modules/OrchardCore.Commerce/Startup.cs | 50 +- .../ViewModels/OrderPartViewModel.cs | 3 +- .../ViewModels/TieredPricePartViewModel.cs | 4 +- .../Views/BooleanProductAttributeField.cshtml | 1 - .../Views/BooleanProductAttributeValue.cshtml | 11 +- .../Views/Checkout.cshtml | 8 +- .../Views/NumericProductAttributeField.cshtml | 1 - .../Views/NumericProductAttributeValue.cshtml | 1 - .../Views/OrderPart_Edit.cshtml | 5 +- .../Views/ProductListPart.Filters.cshtml | 2 +- .../Views/ProductListPart.cshtml | 3 +- .../Views/ProductPart.cshtml | 5 +- .../Views/ShoppingCart/Index.cshtml | 4 - .../Views/ShoppingCartCell_Action.cshtml | 1 - .../Views/ShoppingCartCell_GrossPrice.cshtml | 3 +- .../Views/ShoppingCartCell_NetPrice.cshtml | 3 +- .../Views/ShoppingCartCell_OldPrice.cshtml | 13 +- .../Views/ShoppingCartCell_Product.cshtml | 1 - .../Views/ShoppingCartCell_Quantity.cshtml | 5 +- .../Views/ShoppingCartTable.cshtml | 22 +- .../Views/TextProductAttributeField.cshtml | 1 - .../Views/TextProductAttributeValue.cshtml | 1 - .../OrchardCore.Commerce/pnpm-lock.yaml | 1733 +++++++++-------- .../OrchardCore.Commerce.Web.csproj | 4 +- src/OrchardCore.Commerce.Web/Program.cs | 2 +- .../Controllers/OrderController.cs | 2 +- .../Tests/BasicTests/SecurityScanningTests.cs | 4 +- .../CheckoutTests/BehaviorCheckoutTests.cs | 1 - ...iourTests.cs => InventoryBehaviorTests.cs} | 4 +- ...ts.cs => LocalizedProductBehaviorTests.cs} | 6 +- ...aviourTests.cs => ProductBehaviorTests.cs} | 4 +- ...iourTests.cs => PromotionBehaviorTests.cs} | 4 +- ...xBehaviourTests.cs => TaxBehaviorTests.cs} | 4 +- .../Tests/UserTests/UserPersistenceTests.cs | 6 +- ...viourTests.cs => WorkflowBehaviorTests.cs} | 4 +- .../UITestBase.cs | 10 - .../OrchardCore.Commerce.Tests/AmountTests.cs | 22 +- .../Fakes/FakeContentManager.cs | 12 +- .../Fakes/TestMoneyService.cs | 4 +- .../MoneyServiceTests.cs | 6 +- 157 files changed, 4923 insertions(+), 4503 deletions(-) create mode 100644 NuGet.config delete mode 100644 Nuget.config create mode 100644 docs/releases/3.0.0.md delete mode 100644 src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs rename src/{Modules/OrchardCore.Commerce.Payment => Libraries/OrchardCore.Commerce.Abstractions}/Models/Payment.cs (53%) delete mode 100644 src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs delete mode 100644 src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs delete mode 100644 src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs delete mode 100644 src/Libraries/OrchardCore.Commerce.MoneyDataType/Extensions/JObjectExtensions.cs delete mode 100644 src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyAmountConverter.cs delete mode 100644 src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyCurrencyConverter.cs create mode 100644 src/Modules/OrchardCore.Commerce/Constants/OrchardCoreCommerceConfigureOrder.cs delete mode 100644 src/Modules/OrchardCore.Commerce/Endpoints/Extentions/HttpContextExtensions.cs delete mode 100644 src/Modules/OrchardCore.Commerce/Extensions/JsonExtensions.cs create mode 100644 src/Modules/OrchardCore.Commerce/Liquid/AmountToStringLiquidFilter.cs delete mode 100644 src/Modules/OrchardCore.Commerce/Liquid/OrderLineItemViewModelsAndTaxRatesConverterFilter.cs create mode 100644 src/Modules/OrchardCore.Commerce/Liquid/OrderPartToOrderSummaryLiquidFilter.cs rename test/OrchardCore.Commerce.Tests.UI/Tests/InventoryTests/{InventoryBehaviourTests.cs => InventoryBehaviorTests.cs} (98%) rename test/OrchardCore.Commerce.Tests.UI/Tests/LocalizedProductTests/{LocalizedProductBehaviourTests.cs => LocalizedProductBehaviorTests.cs} (94%) rename test/OrchardCore.Commerce.Tests.UI/Tests/ProductTests/{ProductBehaviourTests.cs => ProductBehaviorTests.cs} (96%) rename test/OrchardCore.Commerce.Tests.UI/Tests/PromotionTests/{PromotionBehaviourTests.cs => PromotionBehaviorTests.cs} (96%) rename test/OrchardCore.Commerce.Tests.UI/Tests/TaxTests/{TaxBehaviourTests.cs => TaxBehaviorTests.cs} (97%) rename test/OrchardCore.Commerce.Tests.UI/Tests/WorkflowTests/{WorkflowBehaviourTests.cs => WorkflowBehaviorTests.cs} (96%) diff --git a/.github/actions/spelling/allow/occ.txt b/.github/actions/spelling/allow/occ.txt index 7fbcf6a82..a13600e92 100644 --- a/.github/actions/spelling/allow/occ.txt +++ b/.github/actions/spelling/allow/occ.txt @@ -17,6 +17,7 @@ foobool footext GST htmlfield +JConvert LCID markdownlint Mastercard @@ -39,4 +40,4 @@ testproductvariant unpublish vnd webhooks -webshop \ No newline at end of file +webshop diff --git a/Directory.Packages.props b/Directory.Packages.props index 4ec0c6247..8ad07acdd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,20 +6,22 @@ - 1.8.0 + 2.0.0 + + 11.0.0 + 11.0.0 - - + + - - - + + + - diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 000000000..6c5975456 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Nuget.config b/Nuget.config deleted file mode 100644 index ddd7f13a1..000000000 --- a/Nuget.config +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/OrchardCore.Commerce.sln b/OrchardCore.Commerce.sln index 3d5421f04..82a6f2921 100644 --- a/OrchardCore.Commerce.sln +++ b/OrchardCore.Commerce.sln @@ -11,10 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props License.md = License.md mkdocs.yml = mkdocs.yml - Nuget.config = Nuget.config Readme.md = Readme.md Reset-Local.ps1 = Reset-Local.ps1 Directory.Packages.props = Directory.Packages.props + NuGet.config = NuGet.config EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B2D057AA-E3F7-404D-A713-C3C59F9DE562}" @@ -119,6 +119,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "guides", "guides", "{EF8008 docs\guides\create-webshop.md = docs\guides\create-webshop.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "releases", "releases", "{0A8AB166-9C4F-4161-BD69-362DBC73A11E}" + ProjectSection(SolutionItems) = preProject + docs\releases\3.0.0.md = docs\releases\3.0.0.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -215,6 +220,7 @@ Global {28DB6CBB-1527-42A1-8EFE-3D95BF185884} = {90913510-3D7F-4BCC-B55E-56343128F049} {73925C09-BF96-4727-91D8-57A88AD1601F} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} {EF8008F1-64F5-4053-A639-6285084AFA52} = {BEBA1764-178A-4722-A193-4DEF26DCE8D1} + {0A8AB166-9C4F-4161-BD69-362DBC73A11E} = {BEBA1764-178A-4722-A193-4DEF26DCE8D1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {456CBC78-579D-483F-A4C3-AF5C12AB3324} diff --git a/docs/guides/create-webshop.md b/docs/guides/create-webshop.md index a78a13863..747fdc832 100644 --- a/docs/guides/create-webshop.md +++ b/docs/guides/create-webshop.md @@ -91,7 +91,7 @@ Navigate to _Design > Settings > Zones_ and define the zone where you want the S _Header, Content, Footer, any zone can be created here. Except for Ozone, that's illegal._ - **Layers**: -A Layer is also going to be necessary for the Widget, so if you don't have one set up yet, see the [relevant documentation page](https://github.com/OrchardCMS/OrchardCore/tree/main/src/docs/reference/modules/Layers/). +A Layer is also going to be necessary for the Widget, so if you don't have one set up yet, see the [relevant documentation page](https://docs.orchardcore.net/en/latest/reference/modules/Layers/). - **Widget**: Widgets are content items that have their stereotype set to Widget. The Commerce module creates a simple Shopping Cart Widget when it's enabled, so we'll just use that. For more extensive documentation about Widgets, see the [usual place](https://docs.orchardcore.net/en/main/docs/reference/modules/Widgets/). diff --git a/docs/releases/3.0.0.md b/docs/releases/3.0.0.md new file mode 100644 index 000000000..cb775dda2 --- /dev/null +++ b/docs/releases/3.0.0.md @@ -0,0 +1,30 @@ +# Orchard Core Commerce 3.0.0 + +Release date: Not yet released + +## Important Upgrade Instructions + +Prior to making the leap to Orchard Core Commerce 3.0.0, please read and follow the instructions for the [Orchard Core 2.0.0 release](https://docs.orchardcore.net/en/latest/releases/2.0.0/), as it contains several major breaking changes. + +## Breaking Changes + +### Dropped `Newtonsoft.Json` Support + +The most important breaking change in OC 2.0 is the end of support for [Newtonsoft Json.NET](https://www.newtonsoft.com/json) and the switch to the [System.Text.Json](https://www.nuget.org/packages/System.Text.Json) (STJ) library. For OCC, all Newtonsoft converters are removed, STJ converters were written or updated as necessary. Any models and interfaces that use `JToken` or `JObject` (such as `OrderPart.AdditionalData` and `IUserService.AlterUserSettingAsync()`) now use `JsonNode` and `JsonObject` respectively. + +### Replaced `IPayment` with `Payment` Everywhere + +We've dropped the [`IPayment`](https://github.com/OrchardCMS/OrchardCore.Commerce/blob/34ae00470e954459f19f688c9bfc51d196c386ca/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs) interface, now services and models use `Payment` directly, which was also moved into the `OrchardCore.Commerce.Abstractions` library. Migrating to STJ meant that to retain the polymorphic deserialization support for models using `IPayment` would've taken on some additional complexity, while we already found the separate interface unnecessary. + +Update using references to `OrchardCore.Commerce.Abstractions.Models`. If you have a custom payment processor integration, update it to use `Payment`. + +### Liquid Filters + +The `order_line_item_view_models_and_tax_rates` Liquid filter has been removed. A new `order_part_to_order_summary` filter has been added instead, which can be treated as a drop-in replacement. It has additional `Amount` type properties: `UnitTax`, `SubTotal`, `TaxTotal`, and `Total`. These contain the calculated and appropriately rounded values. + +The new `amount_to_string` filter processes the input object as `Amount` (like the `amount` filter) and correctly formats it just like the `Amount.ToString()` override in C#. You can use `amount_to_string: dot: ","` to make it display a comma as the decimal separator when it would use a dot. Unlike `amount`, you can also use this filter on a number with the `currency: "three-letter-code""` argument (e.g. `{{ value | amount_to_string: currency: "EUR" }}`). This will display any numeric value as the given currency. + +## Change Logs + +Please check the GitHub release entry [here](https://github.com/OrchardCMS/OrchardCore.Commerce/releases/tag/v3.0.0). + diff --git a/mkdocs.yml b/mkdocs.yml index b15b08087..b8df9eadf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -100,4 +100,6 @@ nav: - NumericProductAttributeField: features/numeric-product-attribute-field.md - Resources: - Libraries: resources/libraries/README.md - - Releases: https://github.com/OrchardCMS/OrchardCore.Commerce/releases + - Releases and Notes: + - GitHub: https://github.com/OrchardCMS/OrchardCore.Commerce/releases + - 3.0.0: docs/releases/3.0.0.md diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs deleted file mode 100644 index 80179343f..000000000 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs +++ /dev/null @@ -1,35 +0,0 @@ -using OrchardCore.Commerce.MoneyDataType; -using System; - -namespace OrchardCore.Commerce.Abstractions.Abstractions; - -/// -/// Describes a payment transaction's details. -/// -public interface IPayment -{ - /// - /// Gets the kind of charge, such as "Credit Card", "Cash", "Bitcoin", etc. - /// - string Kind { get; } - - /// - /// Gets a unique ID for the transaction. The semantics of this can vary by provider. - /// - string TransactionId { get; } - - /// - /// Gets the text accompanying the charge. The semantics of this can vary by provider. - /// - string ChargeText { get; } - - /// - /// Gets the amount charged. - /// - Amount Amount { get; } - - /// - /// Gets the UTC creation date and time of the charge. - /// - DateTime CreatedUtc { get; } -} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs index cd501407c..3af599530 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; using System.Text.Json.Nodes; @@ -23,11 +22,6 @@ public interface IProductAttributeDeserializer /// string AttributeTypeName { get; } - /// - /// Deserializes using Newtonsoft.Json. - /// - IProductAttributeValue Deserialize(string attributeName, JObject attribute); - /// /// Deserializes using System.Text.Json. /// diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs index 6c7584f0b..7d744ba9d 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions.Serialization; using System; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Abstractions; diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs index 46b27012f..10235b1fc 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Http; -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using OrchardCore.Users.Models; using System; using System.Security.Claims; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace OrchardCore.Commerce.Abstractions.Abstractions; @@ -22,7 +22,7 @@ public interface IUserService /// Alters the JSON of a custom user setting content item and saves the result. If the setting doesn't exist for the /// user then also creates it. /// - Task AlterUserSettingAsync(User user, string contentType, Func updateContentItemJson); + Task AlterUserSettingAsync(User user, string contentType, Func updateContentItemJson); /// /// Retrieves a that belongs to the custom user setting. @@ -33,8 +33,7 @@ public interface IUserService public static class UserServiceExtensions { public static Task GetCurrentFullUserAsync(this IUserService service, IHttpContextAccessor hca) => - hca.HttpContext?.User is { } user && - hca.HttpContext.User.Identity?.IsAuthenticated == true + hca.HttpContext?.User is { Identity.IsAuthenticated: true } user ? service.GetFullUserAsync(user) : Task.FromResult(null); diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs index 3c6831525..227f19756 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs @@ -2,6 +2,7 @@ using OrchardCore.Commerce.MoneyDataType; using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Models; @@ -13,10 +14,15 @@ public class OrderLineItem public Amount UnitPrice { get; set; } public Amount LinePrice { get; set; } public string ContentItemVersion { get; set; } - public ISet Attributes { get; } - public IDictionary> SelectedAttributes { get; } = + public ISet Attributes { get; init; } + public IDictionary> SelectedAttributes { get; init; } = new Dictionary>(); + [JsonConstructor] + private OrderLineItem() + { + } + // These are necessary. #pragma warning disable S107 // Methods should not have too many parameters public OrderLineItem( diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs index 98d39f185..2b9090d5e 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs @@ -1,12 +1,11 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; using System; using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Models; @@ -26,16 +25,9 @@ public class OrderPart : ContentPart public IList AdditionalCosts { get; } = new List(); /// - /// Gets the amounts charged for this order. Typically a single credit card charge. + /// Gets the amounts charged for this order. Typically, a single credit card charge. /// - - // This is a temporary solution, it needs to be reworked in the future! -#pragma warning disable CA2326 // Do not use TypeNameHandling values other than None -#pragma warning disable SCS0028 // TypeNameHandling is set to the other value than 'None'. It may lead to deserialization vulnerability. - [JsonProperty(ItemTypeNameHandling = TypeNameHandling.Auto)] -#pragma warning restore SCS0028 // TypeNameHandling is set to the other value than 'None'. It may lead to deserialization vulnerability. -#pragma warning restore CA2326 // Do not use TypeNameHandling values other than None - public IList Charges { get; } = new List(); + public IList Charges { get; } = new List(); public TextField Email { get; set; } = new(); public TextField Phone { get; set; } = new(); @@ -46,7 +38,7 @@ public class OrderPart : ContentPart public BooleanField BillingAndShippingAddressesMatch { get; set; } = new(); public BooleanField IsCorporation { get; set; } = new(); - public IDictionary AdditionalData { get; } = new Dictionary(); + public IDictionary AdditionalData { get; } = new Dictionary(); [JsonIgnore] public bool IsPending => string.IsNullOrWhiteSpace(Status?.Text) || Status.Text.EqualsOrdinalIgnoreCase(OrderStatusCodes.Pending); diff --git a/src/Modules/OrchardCore.Commerce.Payment/Models/Payment.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/Payment.cs similarity index 53% rename from src/Modules/OrchardCore.Commerce.Payment/Models/Payment.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/Payment.cs index b23b4cc18..92f4d90eb 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Models/Payment.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/Payment.cs @@ -1,13 +1,11 @@ -using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.MoneyDataType; using System; -namespace OrchardCore.Commerce.Payment.Models; +namespace OrchardCore.Commerce.Abstractions.Models; public record Payment( string Kind, string TransactionId, string ChargeText, Amount Amount, - DateTime CreatedUtc) - : IPayment; + DateTime CreatedUtc); diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs index 7128fe5a3..be45f4130 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs @@ -1,16 +1,15 @@ -using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions.Serialization; using OrchardCore.Commerce.MoneyDataType; using System.Diagnostics; using System.Globalization; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Models; /// /// A price and its priority. /// -[JsonConverter(typeof(LegacyPrioritizedPriceConverter))] -[System.Text.Json.Serialization.JsonConverter(typeof(PrioritizedPriceConverter))] +[JsonConverter(typeof(PrioritizedPriceConverter))] [DebuggerDisplay("{DebuggerDisplay,nq}")] public class PrioritizedPrice { diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs index f69a3583e..6373b9529 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs @@ -21,14 +21,12 @@ public class ShoppingCart /// /// Gets the number of lines in the cart. /// - [Newtonsoft.Json.JsonIgnore] [JsonIgnore] public int Count => Items.Count; /// /// Gets the total number of items (i.e. products) in the cart. In other words, the sum of quantities of all lines. /// - [Newtonsoft.Json.JsonIgnore] [JsonIgnore] public int ItemCount => Items.Sum(item => item.Quantity); diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs index 4fd342798..0349ec1e2 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using OrchardCore.Commerce.Abstractions.Serialization; @@ -6,14 +5,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Models; /// /// A shopping cart item. /// -[JsonConverter(typeof(LegacyShoppingCartItemConverter))] -[System.Text.Json.Serialization.JsonConverter(typeof(ShoppingCartItemConverter))] +[JsonConverter(typeof(ShoppingCartItemConverter))] public sealed class ShoppingCartItem : IEquatable { /// diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs index e2752c05b..70ea92510 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs @@ -1,13 +1,12 @@ -using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions.Serialization; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.ProductAttributeValues; /// /// Used only to deserialize attributes, before they're post-processed into concrete attribute values. /// -[JsonConverter(typeof(LegacyRawProductAttributeValueConverter))] -[System.Text.Json.Serialization.JsonConverter(typeof(RawProductAttributeValueConverter))] +[JsonConverter(typeof(RawProductAttributeValueConverter))] internal sealed class RawProductAttributeValue : BaseProductAttributeValue { public RawProductAttributeValue(object value) diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs deleted file mode 100644 index adeb88085..000000000 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions.Models; -using OrchardCore.Commerce.MoneyDataType; -using System; - -namespace OrchardCore.Commerce.Abstractions.Serialization; - -internal sealed class LegacyPrioritizedPriceConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, PrioritizedPrice value, JsonSerializer serializer) - { - writer.WriteStartObject(); - - writer.WritePropertyName(PrioritizedPriceConverter.PriorityName); - writer.WriteValue(value.Priority); - - writer.WritePropertyName(PrioritizedPriceConverter.AmountName); - serializer.Serialize(writer, value.Price); - - writer.WriteEndObject(); - } - - public override PrioritizedPrice ReadJson( - JsonReader reader, - Type objectType, - PrioritizedPrice existingValue, - bool hasExistingValue, - JsonSerializer serializer) - { - var priority = int.MinValue; - var amount = Amount.Unspecified; - var token = JToken.ReadFrom(reader); - - if ((token as JObject)?.Properties() is { } properties) - { - foreach (var property in properties) - { - switch (property.Name) - { - case PrioritizedPriceConverter.PriorityName: - priority = property.Value.Value(); - break; - case PrioritizedPriceConverter.AmountName: - amount = property.Value.ToObject(); - break; - default: - throw new InvalidOperationException($"Unknown property name \"{property.Name}\"."); - } - } - } - else if (token.Type == JTokenType.String) - { - amount = token.ToObject(); - } - - return new PrioritizedPrice(priority, amount); - } -} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs deleted file mode 100644 index 01eea375d..000000000 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions.ProductAttributeValues; -using System; - -namespace OrchardCore.Commerce.Abstractions.Serialization; - -internal sealed class LegacyRawProductAttributeValueConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, RawProductAttributeValue value, JsonSerializer serializer) => - serializer.Serialize(writer, value.UntypedValue); - - public override RawProductAttributeValue ReadJson( - JsonReader reader, - Type objectType, - RawProductAttributeValue existingValue, - bool hasExistingValue, - JsonSerializer serializer) => - new(JToken.ReadFrom(reader)); -} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs deleted file mode 100644 index d1a8c2811..000000000 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions.Abstractions; -using OrchardCore.Commerce.Abstractions.Models; -using OrchardCore.Commerce.Abstractions.ProductAttributeValues; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace OrchardCore.Commerce.Abstractions.Serialization; - -internal sealed class LegacyShoppingCartItemConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, ShoppingCartItem value, JsonSerializer serializer) - { - writer.WriteStartObject(); - - writer.WritePropertyName(ShoppingCartItemConverter.QuantityName); - writer.WriteValue(value.Quantity); - - writer.WritePropertyName(ShoppingCartItemConverter.SkuName); - writer.WriteValue(value.ProductSku); - - if (value.Prices?.Any() == true) - { - writer.WritePropertyName(ShoppingCartItemConverter.PricesName); - serializer.Serialize(writer, value.Prices); - } - - if (value.Attributes?.Any() == true) - { - writer.WritePropertyName(ShoppingCartItemConverter.AttributesName); - serializer.Serialize(writer, value.Attributes.ToDictionary( - attribute => attribute.AttributeName, - attribute => attribute.UntypedValue)); - } - - writer.WriteEndObject(); - } - - public override ShoppingCartItem ReadJson( - JsonReader reader, - Type objectType, - ShoppingCartItem existingValue, - bool hasExistingValue, - JsonSerializer serializer) - { - var quantity = 1; - string sku = null; - HashSet attributes = []; - IList prices = null; - - var properties = (JToken.ReadFrom(reader) as JObject)?.Properties() ?? Enumerable.Empty(); - foreach (var property in properties) - { - var value = property.Value; - if (value.Type is JTokenType.Null or JTokenType.Undefined) continue; - - switch (property.Name) - { - case ShoppingCartItemConverter.QuantityName: - quantity = value.Value(); - break; - case ShoppingCartItemConverter.SkuName: - sku = value.Value(); - break; - case ShoppingCartItemConverter.PricesName: - prices = value.ToObject>(); - break; - case ShoppingCartItemConverter.AttributesName: - foreach (var (name, attribute) in value.ToObject>()) - { - attribute.SetAttributeName(name); - attributes.Add(attribute); - } - - break; - default: - throw new InvalidOperationException($"Unknown property name \"{property.Name}\"."); - } - } - - return new ShoppingCartItem(quantity, sku, attributes, prices); - } -} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs index 90a1b3e73..13b3bb1b4 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs @@ -1,12 +1,13 @@ using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.MoneyDataType.Serialization; using System; using System.Text.Json; using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Serialization; -internal sealed class PrioritizedPriceConverter : JsonConverter +public sealed class PrioritizedPriceConverter : JsonConverter { public const string PriorityName = "priority"; public const string AmountName = "amount"; @@ -16,6 +17,11 @@ public override PrioritizedPrice Read(ref Utf8JsonReader reader, Type typeToConv var priority = int.MinValue; var amount = Amount.Unspecified; + if (reader.TokenType == JsonTokenType.String) + { + return new(priority, AmountConverter.ReadString(reader.GetString())); + } + while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) { var propertyName = reader.GetString(); @@ -36,18 +42,18 @@ public override PrioritizedPrice Read(ref Utf8JsonReader reader, Type typeToConv if (priority > int.MinValue && !amount.Currency.Equals(Currency.UnspecifiedCurrency)) { - return new PrioritizedPrice(priority, amount); + return new(priority, amount); } return null; } - public override void Write(Utf8JsonWriter writer, PrioritizedPrice prioritizedPrice, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PrioritizedPrice value, JsonSerializerOptions options) { writer.WriteStartObject(); - writer.WriteNumber(PriorityName, prioritizedPrice.Priority); + writer.WriteNumber(PriorityName, value.Priority); writer.WritePropertyName(AmountName); - JsonSerializer.Serialize(writer, prioritizedPrice.Price, options); + JsonSerializer.Serialize(writer, value.Price, options); writer.WriteEndObject(); } } diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs index 97abe5649..19a9f58fa 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs @@ -1,28 +1,29 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions.Abstractions; using System; using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Abstractions.Serialization; -internal sealed class ProductAttributeValueConverter : JsonConverter +public sealed class ProductAttributeValueConverter : JsonConverter { - private const string Type = "type"; - private const string Value = "value"; - private const string AttributeName = "attributeName"; + private const string TypePropertyName = "type"; + private const string ValuePropertyName = "value"; + private const string AttributeNamePropertyName = "attributeName"; - public override IProductAttributeValue ReadJson( - JsonReader reader, - Type objectType, - IProductAttributeValue existingValue, - bool hasExistingValue, - JsonSerializer serializer) + public override IProductAttributeValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var attribute = (JObject)JToken.Load(reader); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new InvalidOperationException( + $"Expected object when deserializing {typeToConvert.Name}, got {reader.TokenType}"); + } - var attributeName = attribute.Get(AttributeName); - var typeName = attribute.Get(Type); + var attribute = JsonNode.Parse(ref reader)!.AsObject(); + var attributeName = attribute[AttributeNamePropertyName]?.GetValue(); + var typeName = attribute[TypePropertyName]?.GetValue(); var deserializer = IProductAttributeDeserializer.Deserializers.GetMaybe(typeName) ?? throw new InvalidOperationException($"Unknown or unsupported type \"{typeName}\"."); @@ -30,36 +31,20 @@ public override IProductAttributeValue ReadJson( return deserializer.Deserialize(attributeName, attribute); } - public override void WriteJson(JsonWriter writer, IProductAttributeValue productAttributeValue, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, IProductAttributeValue value, JsonSerializerOptions options) { - if (productAttributeValue is null) return; + if (value is null) return; writer.WriteStartObject(); - writer.WritePropertyName(Type); - writer.WriteValue(productAttributeValue.GetType().Name); - - var untypedValue = productAttributeValue.UntypedValue; - - writer.WritePropertyName(Value); - - if (untypedValue is IEnumerable values) - { - writer.WriteStartArray(); - - foreach (var value in values) - { - writer.WriteValue(value); - } - writer.WriteEndArray(); - } - else - { - writer.WriteValue(untypedValue); - } + writer.WriteString(TypePropertyName, value.GetType().Name); + writer.WriteString(AttributeNamePropertyName, value.AttributeName); - writer.WritePropertyName(AttributeName); - writer.WriteValue(productAttributeValue.AttributeName); + writer.WritePropertyName(ValuePropertyName); + var valueNode = value.UntypedValue is IEnumerable values + ? JArray.FromObject(values, options) + : JNode.FromObject(value.UntypedValue, options); + valueNode?.WriteTo(writer, options); writer.WriteEndObject(); } diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs index 10311f458..f63f69ea7 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs @@ -9,7 +9,7 @@ namespace OrchardCore.Commerce.Abstractions.Serialization; -internal sealed class ShoppingCartItemConverter : JsonConverter +public sealed class ShoppingCartItemConverter : JsonConverter { public const string QuantityName = "quantity"; public const string SkuName = "sku"; diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs index 4861091a5..4a654ffe6 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; -using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; namespace OrchardCore.Commerce.Abstractions.ViewModels; @@ -17,7 +17,7 @@ public class ShoppingCartLineViewModel : ILineItem public Amount UnitPrice { get; set; } public Amount LinePrice { get; set; } - public IDictionary AdditionalData { get; } = new Dictionary(); + public IDictionary AdditionalData { get; } = new Dictionary(); [BindNever] public ISkuHolderContent Product { get; set; } diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Amount.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Amount.cs index e7bc8fbfa..7aeccad36 100644 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Amount.cs +++ b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Amount.cs @@ -13,7 +13,6 @@ namespace OrchardCore.Commerce.MoneyDataType; /// A money amount, which is represented by a decimal number and a currency. /// [JsonConverter(typeof(AmountConverter))] -[Newtonsoft.Json.JsonConverter(typeof(LegacyAmountConverter))] [DebuggerDisplay("{DebuggerDisplay,nq}")] public readonly struct Amount : IEquatable, IComparable { @@ -98,15 +97,19 @@ private void ThrowIfCurrencyDoesntMatch(Amount other, string operation = "compar public static Amount operator +(Amount first, Amount second) { first.ThrowIfCurrencyDoesntMatch(second, operation: "add"); - return new Amount(first.Value + second.Value, first.Currency); + return new(first.Value + second.Value, first.Currency); } + public static Amount operator +(Amount first, decimal second) => new(first.Value + second, first.Currency); + public static Amount operator -(Amount first, Amount second) { first.ThrowIfCurrencyDoesntMatch(second, operation: "subtract"); - return new Amount(first.Value - second.Value, first.Currency); + return new(first.Value - second.Value, first.Currency); } + public static Amount operator -(Amount first, decimal second) => new(first.Value - second, first.Currency); + public static Amount operator -(Amount amount) => new(-amount.Value, amount.Currency); diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Currency.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Currency.cs index ebcefc087..98d4eb029 100644 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Currency.cs +++ b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Currency.cs @@ -9,7 +9,6 @@ namespace OrchardCore.Commerce.MoneyDataType; [JsonConverter(typeof(CurrencyConverter))] -[Newtonsoft.Json.JsonConverter(typeof(LegacyCurrencyConverter))] [DebuggerDisplay("{DebuggerDisplay,nq}")] public readonly partial struct Currency : ICurrency, IEquatable { diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Extensions/JObjectExtensions.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Extensions/JObjectExtensions.cs deleted file mode 100644 index e424269cc..000000000 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Extensions/JObjectExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Newtonsoft.Json.Linq; - -public static class JObjectExtensions -{ - public static T Get(this JObject attribute, params string[] keys) - { - foreach (var key in keys) - { - if (attribute.TryGetValue(key, out var token)) - { - return token.ToObject(); - } - } - - return default; - } -} diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/OrchardCore.Commerce.MoneyDataType.csproj b/src/Libraries/OrchardCore.Commerce.MoneyDataType/OrchardCore.Commerce.MoneyDataType.csproj index 6e60abd9f..faff36fe1 100644 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/OrchardCore.Commerce.MoneyDataType.csproj +++ b/src/Libraries/OrchardCore.Commerce.MoneyDataType/OrchardCore.Commerce.MoneyDataType.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/AmountConverter.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/AmountConverter.cs index 3262d700e..c018db964 100644 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/AmountConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/AmountConverter.cs @@ -1,12 +1,14 @@ using OrchardCore.Commerce.MoneyDataType.Abstractions; using System; +using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using static OrchardCore.Commerce.MoneyDataType.Currency; namespace OrchardCore.Commerce.MoneyDataType.Serialization; -internal sealed class AmountConverter : JsonConverter +public sealed class AmountConverter : JsonConverter { public const string ValueName = "value"; public const string CurrencyName = "currency"; @@ -27,6 +29,11 @@ public override Amount Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS string iso = null; int? decimalDigits = null; + if (reader.TokenType == JsonTokenType.String) + { + return ReadString(reader.GetString()); + } + while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) { var propertyName = reader.GetString(); @@ -61,37 +68,35 @@ public override Amount Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS } } - if (reader.TokenType == JsonTokenType.String) return LegacyAmountConverter.ReadString(reader.GetString()); - currency = HandleUnknownCurrency(currency, nativeName, englishName, symbol, iso, decimalDigits); - return new Amount(value, currency); + return new(value, currency); } - public override void Write(Utf8JsonWriter writer, Amount amount, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Amount value, JsonSerializerOptions options) { - if (amount.Currency is null) + if (value.Currency is null) { throw new InvalidOperationException("Amount must have a currency applied to allow serialization."); } writer.WriteStartObject(); - writer.WriteNumber(ValueName, amount.Value); + writer.WriteNumber(ValueName, value.Value); - if (IsKnownCurrency(amount.Currency.CurrencyIsoCode)) + if (IsKnownCurrency(value.Currency.CurrencyIsoCode)) { - writer.WriteString(CurrencyName, amount.Currency.CurrencyIsoCode); + writer.WriteString(CurrencyName, value.Currency.CurrencyIsoCode); } else { - writer.WriteString(NativeName, amount.Currency.NativeName); - writer.WriteString(EnglishName, amount.Currency.EnglishName); - writer.WriteString(Symbol, amount.Currency.Symbol); - writer.WriteString(Iso, amount.Currency.CurrencyIsoCode); + writer.WriteString(NativeName, value.Currency.NativeName); + writer.WriteString(EnglishName, value.Currency.EnglishName); + writer.WriteString(Symbol, value.Currency.Symbol); + writer.WriteString(Iso, value.Currency.CurrencyIsoCode); - if (amount.Currency.DecimalPlaces != DefaultDecimalDigits) + if (value.Currency.DecimalPlaces != DefaultDecimalDigits) { - writer.WriteNumber(DecimalDigits, amount.Currency.DecimalPlaces); + writer.WriteNumber(DecimalDigits, value.Currency.DecimalPlaces); } } @@ -120,4 +125,20 @@ private static ICurrency HandleUnknownCurrency( throw new InvalidOperationException($"Invalid amount format. Must include a {nameof(currency)}."); } + + public static Amount ReadString(string text) + { + ArgumentException.ThrowIfNullOrEmpty(text); + + var parts = text.Split(); + if (parts.Length < 2) + { + throw new InvalidOperationException($"Unable to parse string amount \"{text}\"."); + } + + var currency = FromIsoCode(parts[0]); + var value = decimal.Parse(string.Join(string.Empty, parts.Skip(1)), CultureInfo.InvariantCulture); + + return new(value, currency); + } } diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/CurrencyConverter.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/CurrencyConverter.cs index f2a16628a..8fb7f0a1a 100644 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/CurrencyConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/CurrencyConverter.cs @@ -1,10 +1,11 @@ using OrchardCore.Commerce.MoneyDataType.Abstractions; using System; using System.Text.Json; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.MoneyDataType.Serialization; -internal sealed class CurrencyConverter : System.Text.Json.Serialization.JsonConverter +public sealed class CurrencyConverter : JsonConverter { public override ICurrency Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Currency.FromIsoCode(reader.GetString()); diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyAmountConverter.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyAmountConverter.cs deleted file mode 100644 index a4776eb5c..000000000 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyAmountConverter.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Globalization; -using System.Linq; -using static OrchardCore.Commerce.MoneyDataType.Currency; -using static OrchardCore.Commerce.MoneyDataType.Serialization.AmountConverter; - -namespace OrchardCore.Commerce.MoneyDataType.Serialization; - -internal sealed class LegacyAmountConverter : JsonConverter -{ - public override Amount ReadJson( - JsonReader reader, - Type objectType, - Amount existingValue, - bool hasExistingValue, - JsonSerializer serializer) - { - var token = JToken.Load(reader); - if (token.Type == JTokenType.String) return ReadString(token.Value()); - - var attribute = (JObject)token; - - var value = attribute.Get(ValueName); - var currencyCode = attribute.Get(CurrencyName); - var nativeName = attribute.Get(Name, NativeName); - var englishName = attribute.Get(EnglishName); - var symbol = attribute.Get(Symbol); - var iso = attribute.Get(Iso); - var decimalDigits = attribute.Get(DecimalDigits); - var currency = string.IsNullOrEmpty(currencyCode ?? iso) ? null : FromIsoCode(currencyCode ?? iso); - - var currencyIsEmpty = string.IsNullOrEmpty(currency?.CurrencyIsoCode); - if (currencyIsEmpty && (string.IsNullOrEmpty(iso) || iso == UnspecifiedCurrency.CurrencyIsoCode)) - { - return Amount.Unspecified; - } - - if (currencyIsEmpty || !IsKnownCurrency(currency.CurrencyIsoCode)) - { - currency = new Currency( - nativeName, - englishName, - symbol, - iso, - decimalDigits.GetValueOrDefault(DefaultDecimalDigits)); - } - - return new Amount(value, currency); - } - - public override void WriteJson(JsonWriter writer, Amount amount, JsonSerializer serializer) - { - if (amount.Currency is null) return; - - writer.WriteStartObject(); - writer.WritePropertyName(ValueName); - writer.WriteValue(amount.Value); - - if (IsKnownCurrency(amount.Currency.CurrencyIsoCode)) - { - writer.WritePropertyName(CurrencyName); - writer.WriteValue(amount.Currency.CurrencyIsoCode); - } - else - { - writer.WritePropertyName(NativeName); - writer.WriteValue(amount.Currency.NativeName); - writer.WritePropertyName(EnglishName); - writer.WriteValue(amount.Currency.EnglishName); - writer.WritePropertyName(Symbol); - writer.WriteValue(amount.Currency.Symbol); - writer.WritePropertyName(Iso); - writer.WriteValue(amount.Currency.CurrencyIsoCode); - - if (amount.Currency.DecimalPlaces != DefaultDecimalDigits) - { - writer.WritePropertyName(DecimalDigits); - writer.WriteValue(amount.Currency.DecimalPlaces); - } - } - - writer.WriteEndObject(); - } - - public static Amount ReadString(string text) - { - var parts = text.Split(); - if (parts.Length < 2) throw new InvalidOperationException($"Unable to parse string amount \"{text}\"."); - - var currency = FromIsoCode(parts[0]); - var value = decimal.Parse(string.Join(string.Empty, parts.Skip(1)), CultureInfo.InvariantCulture); - - return new Amount(value, currency); - } -} diff --git a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyCurrencyConverter.cs b/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyCurrencyConverter.cs deleted file mode 100644 index ea89a5894..000000000 --- a/src/Libraries/OrchardCore.Commerce.MoneyDataType/Serialization/LegacyCurrencyConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; -using OrchardCore.Commerce.MoneyDataType.Abstractions; -using System; - -namespace OrchardCore.Commerce.MoneyDataType.Serialization; - -internal sealed class LegacyCurrencyConverter : JsonConverter -{ - public override ICurrency ReadJson( - JsonReader reader, - Type objectType, - ICurrency existingValue, - bool hasExistingValue, - JsonSerializer serializer) => - Currency.FromIsoCode(reader.ReadAsString()); - - public override void WriteJson(JsonWriter writer, ICurrency value, JsonSerializer serializer) => - writer.WriteValue(value.CurrencyIsoCode); -} diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs index 7501a738f..a029d821d 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs @@ -12,7 +12,7 @@ using OrchardCore.ContentManagement.Display.ContentDisplay; using OrchardCore.ContentManagement.Display.Models; using OrchardCore.ContentManagement.Metadata.Models; -using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using System; using System.Collections.Generic; @@ -80,24 +80,21 @@ public override IDisplayResult Edit(AddressField field, BuildFieldEditorContext PopulateViewModel(field, viewModel, context.PartFieldDefinition); }); - public override async Task UpdateAsync(AddressField field, IUpdateModel updater, UpdateFieldEditorContext context) + public override async Task UpdateAsync(AddressField field, UpdateFieldEditorContext context) { - if (await TryUpdateModelAsync(updater, Prefix) is { } viewModel) + if (await TryUpdateModelAsync(context) is { } viewModel) { field.Address = viewModel.Address; - await _addressFieldEvents.AwaitEachAsync(handler => handler.UpdatingAsync(viewModel, field, updater, context)); + await _addressFieldEvents.AwaitEachAsync(handler => handler.UpdatingAsync(viewModel, field, context.Updater, context)); } return await EditAsync(field, context); } - private async Task TryUpdateModelAsync(IUpdateModel updater, string prefix) + private async Task TryUpdateModelAsync(UpdateFieldEditorContext context) { - var viewModel = new AddressFieldViewModel(); - if (!await updater.TryUpdateModelAsync(viewModel, prefix) || viewModel.Address is not { } address) - { - return null; - } + var viewModel = await context.CreateModelAsync(Prefix); + if (viewModel.Address is not { } address) return null; await _addressUpdaters.AwaitEachAsync(addressUpdater => addressUpdater.UpdateAsync(address)); @@ -113,7 +110,7 @@ private async Task TryUpdateModelAsync(IUpdateModel updat foreach (var key in missingFields) { // This doesn't need to be too complex as it's just a fallback from the client-side validation. - updater.ModelState.AddModelError(key, T["A value is required for {0}.", key]); + context.AddModelError(key, T["A value is required for {0}.", key]); } return missingFields.Count != 0 ? null : viewModel; diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/PriceFieldDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/PriceFieldDisplayDriver.cs index cc4e0240f..df9bdbe58 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/PriceFieldDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/PriceFieldDisplayDriver.cs @@ -8,7 +8,7 @@ using OrchardCore.ContentManagement.Display.ContentDisplay; using OrchardCore.ContentManagement.Display.Models; using OrchardCore.ContentManagement.Metadata.Models; -using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using System; using System.Threading.Tasks; @@ -18,7 +18,8 @@ namespace OrchardCore.Commerce.ContentFields.Drivers; public class PriceFieldDisplayDriver : ContentFieldDisplayDriver { private readonly IMoneyService _moneyService; - private readonly IStringLocalizer T; + + internal readonly IStringLocalizer T; public PriceFieldDisplayDriver(IMoneyService moneyService, IStringLocalizer localizer) { @@ -77,30 +78,26 @@ public override IDisplayResult Edit(PriceField field, BuildFieldEditorContext co public override async Task UpdateAsync( PriceField field, - IUpdateModel updater, UpdateFieldEditorContext context) { - var viewModel = new PriceFieldEditViewModel(); + var viewModel = await context.CreateModelAsync(Prefix); - if (await updater.TryUpdateModelAsync(viewModel, Prefix)) - { - var settings = context.PartFieldDefinition.GetSettings(); - var isInvalid = IsCurrencyInvalid(viewModel.Currency); + var settings = context.PartFieldDefinition.GetSettings(); + var isInvalid = IsCurrencyInvalid(viewModel.Currency); - if (isInvalid && settings.Required) - { - var label = string.IsNullOrEmpty(settings.Label) - ? context.PartFieldDefinition.DisplayName() - : settings.Label; - updater.ModelState.AddModelError( - nameof(viewModel.Currency), - T["The field {0} is invalid.", label].Value); - } + if (isInvalid && settings.Required) + { + var label = string.IsNullOrEmpty(settings.Label) + ? context.PartFieldDefinition.DisplayName() + : settings.Label; + context.AddModelError( + nameof(viewModel.Currency), + T["The field {0} is invalid.", label]); + } - field.Amount = isInvalid + field.Amount = isInvalid ? Amount.Unspecified : _moneyService.Create(viewModel.Value, viewModel.Currency); - } return await EditAsync(field, context); } diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs index 524cb1ed4..49e1e8227 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs @@ -8,13 +8,16 @@ namespace OrchardCore.Commerce.ViewModels; public static class UpdateModelExtensions { + private const string ShippingPrefix = $"{nameof(OrderPart)}.{nameof(OrderPart.ShippingAddress)}"; + private const string BillingPrefix = $"{nameof(OrderPart)}.{nameof(OrderPart.BillingAddress)}"; + public static async Task<(AddressFieldViewModel Shipping, AddressFieldViewModel Billing)> CreateOrderPartAddressViewModelsAsync( this IUpdateModel updater) { var shippingViewModel = new AddressFieldViewModel(); var billingViewModel = new AddressFieldViewModel(); - if (!await updater.TryUpdateModelAsync(shippingViewModel, $"{nameof(OrderPart)}.{nameof(OrderPart.ShippingAddress)}") || - !await updater.TryUpdateModelAsync(billingViewModel, $"{nameof(OrderPart)}.{nameof(OrderPart.BillingAddress)}")) + if (!await updater.TryUpdateModelAsync(shippingViewModel, ShippingPrefix) || + !await updater.TryUpdateModelAsync(billingViewModel, BillingPrefix)) { throw new InvalidOperationException(updater.GetModelErrorMessages().JoinNotNullOrEmpty()); } diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Services/FieldsOnlyDisplayManager.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Services/FieldsOnlyDisplayManager.cs index 8465ce2a3..070192fa5 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Services/FieldsOnlyDisplayManager.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Services/FieldsOnlyDisplayManager.cs @@ -80,7 +80,7 @@ public async Task> DisplayFieldsAsync( var returnUrl = context.Request.PathBase + context.Request.Path + context.Request.QueryString; var editAction = context.ActionTask(controller => controller.Edit(null, false, returnUrl)); - var createAction = context.ActionTask(controller => controller.Create(false, returnUrl)); + var createAction = context.ActionTask(controller => controller.Create(null, false, returnUrl)); return (await GetFieldShapeTypesAsync(contentItem, displayType)) .Select(name => diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs index 2f70c0e33..11c2962dd 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs @@ -1,28 +1,25 @@ using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.ContentTypes.Editors; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using System; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace OrchardCore.Commerce.Settings; public class AddressFieldSettingsDriver : ContentPartFieldDefinitionDisplayDriver { - public override IDisplayResult Edit(ContentPartFieldDefinition model) => - Initialize( - "AddressFieldSettings_Edit", - (Action)model.PopulateSettings) - .PlaceInContent(); + public override IDisplayResult Edit(ContentPartFieldDefinition model, BuildEditorContext context) => + Initialize("AddressFieldSettings_Edit", viewModel => + { + var settings = model.Settings.ToObject(); + viewModel.Hint = settings.Hint; + }).PlaceInContent(); - public override async Task UpdateAsync( - ContentPartFieldDefinition model, - UpdatePartFieldEditorContext context) + public override async Task UpdateAsync(ContentPartFieldDefinition model, UpdatePartFieldEditorContext context) { - var viewModel = new AddressPartFieldSettings(); - - await context.Updater.TryUpdateModelAsync(viewModel, Prefix); - context.Builder.WithSettings(viewModel); + context.Builder.WithSettings(await context.CreateModelAsync(Prefix)); return await EditAsync(model, context); } diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Settings/PriceFieldSettingsDriver.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/PriceFieldSettingsDriver.cs index 659aecdca..bc250fcc7 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Settings/PriceFieldSettingsDriver.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/PriceFieldSettingsDriver.cs @@ -6,7 +6,7 @@ using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.ContentTypes.Editors; -using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using System; using System.Threading.Tasks; @@ -24,7 +24,7 @@ public PriceFieldSettingsDriver(IStringLocalizer local _moneyService = moneyService; } - public override IDisplayResult Edit(ContentPartFieldDefinition model, IUpdateModel updater) => + public override IDisplayResult Edit(ContentPartFieldDefinition model, BuildEditorContext context) => Initialize($"{nameof(PriceFieldSettings)}_Edit", (Action)(viewModel => { var settings = model.GetSettings(); @@ -55,23 +55,20 @@ public override IDisplayResult Edit(ContentPartFieldDefinition model, IUpdateMod public override async Task UpdateAsync(ContentPartFieldDefinition model, UpdatePartFieldEditorContext context) { - var viewModel = new PriceFieldSettingsEditViewModel(); + var viewModel = await context.CreateModelAsync(Prefix); - if (await context.Updater.TryUpdateModelAsync(viewModel, Prefix)) + context.Builder.WithSettings(new PriceFieldSettings { - context.Builder.WithSettings(new PriceFieldSettings - { - Hint = viewModel.Hint, - Label = viewModel.Label, - Required = viewModel.Required, - CurrencySelectionMode = viewModel.CurrencySelectionMode, - SpecificCurrencyIsoCode = viewModel.CurrencySelectionMode == CurrencySelectionMode.SpecificCurrency - ? viewModel.SpecificCurrencyIsoCode - : null, - }); - } + Hint = viewModel.Hint, + Label = viewModel.Label, + Required = viewModel.Required, + CurrencySelectionMode = viewModel.CurrencySelectionMode, + SpecificCurrencyIsoCode = viewModel.CurrencySelectionMode == CurrencySelectionMode.SpecificCurrency + ? viewModel.SpecificCurrencyIsoCode + : null, + }); - return await EditAsync(model, context.Updater); + return await EditAsync(model, context); } private sealed record CurrencySelectionModeItem(int Value, string Text) diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs index f446a316d..1323a069c 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs @@ -15,7 +15,6 @@ using OrchardCore.DisplayManagement.Descriptors; using OrchardCore.Modules; using OrchardCore.ResourceManagement; - using static OrchardCore.Commerce.ContentFields.Constants.FeatureIds; namespace OrchardCore.Commerce.ContentFields; diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField.cshtml index 73c2f525f..c856fc687 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField.cshtml +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField.cshtml @@ -1,7 +1,6 @@ -@model AddressFieldViewModel - -@using OrchardCore.Mvc.Utilities @using OrchardCore.ContentManagement.Metadata.Models +@using OrchardCore.Mvc.Utilities +@model AddressFieldViewModel @{ var definition = Model.PartFieldDefinition; diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor-WesternCommonNameParts.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor-WesternCommonNameParts.cshtml index b1d5dfea9..2d252e917 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor-WesternCommonNameParts.cshtml +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor-WesternCommonNameParts.cshtml @@ -1,15 +1,14 @@ @model AddressFieldEditorViewModel -@using Newtonsoft.Json +@using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.AspNetCore.Mvc.TagHelpers @using OrchardCore.Commerce.Abstractions.Fields @using OrchardCore.Commerce.Abstractions.Models @using OrchardCore.Commerce.AddressDataType @using OrchardCore.DisplayManagement @using OrchardCore.DisplayManagement.TagHelpers - +@using System.Text.Json @using static OrchardCore.Commerce.AddressDataType.Constants.CommonNameParts -@using Microsoft.AspNetCore.Mvc.TagHelpers -@using Microsoft.AspNetCore.Mvc.Localization @{ var namePrefix = Model.CityName[..^(nameof(Address.City).Length)]; @@ -24,7 +23,7 @@ var regions = Model.Regions.Select(region => { - region.Selected = region.Value == address?.Region; + region.Selected = region.Value == address.Region; return region; }); @@ -112,7 +111,7 @@