From 79d5eea7920a585317963aa166c75f0c5ae4f6b4 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 15:45:31 +0200 Subject: [PATCH 1/6] Write loyalty point order messages in the order's language Awarding, reducing and returning loyalty points for an order wrote the history message in the working language of whatever request triggered it. From a scheduled task, an admin action or the API that was the admin's language or nothing at all. The message now uses the language the customer placed the order in (Order.CustomerLanguageId). --- .../Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs | 4 +++- .../Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs | 4 +++- .../ReturnBackRedeemedLoyaltyPointsCommandHandler.cs | 4 +++- .../Orders/AwardLoyaltyPointsCommandHandlerTests.cs | 8 +++++--- .../Orders/ReduceLoyaltyPointsCommandHandlerTests.cs | 8 +++++--- .../ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs | 8 +++++--- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs index aaa7c116d5..02d84c74ce 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs @@ -49,7 +49,9 @@ await _currencyService.ConvertToPrimaryStoreCurrency( //add loyalty points await _loyaltyPointsService.AddLoyaltyPointsHistory(customer.Id, points, request.Order.StoreId, - string.Format(_translationService.GetResource("LoyaltyPoints.Message.EarnedForOrder"), + string.Format( + _translationService.GetResource("LoyaltyPoints.Message.EarnedForOrder", + request.Order.CustomerLanguageId), request.Order.OrderNumber)); request.Order.CalcLoyaltyPoints += points; diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs index a9604af1c6..50af1c671b 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs @@ -54,7 +54,9 @@ await _currencyService.ConvertToPrimaryStoreCurrency( //reduce loyalty points await _loyaltyPointsService.AddLoyaltyPointsHistory(customer.Id, -points, request.Order.StoreId, - string.Format(_translationService.GetResource("LoyaltyPoints.Message.ReducedForOrder"), + string.Format( + _translationService.GetResource("LoyaltyPoints.Message.ReducedForOrder", + request.Order.CustomerLanguageId), request.Order.OrderNumber)); request.Order.CalcLoyaltyPoints -= points; diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs index 75015db674..0b04eb2954 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs @@ -30,7 +30,9 @@ public async Task Handle(ReturnBackRedeemedLoyaltyPointsCommand request, C //return back await _loyaltyPointsService.AddLoyaltyPointsHistory(request.Order.CustomerId, -request.Order.RedeemedLoyaltyPoints, request.Order.StoreId, - string.Format(_translationService.GetResource("LoyaltyPoints.Message.ReturnedForOrder"), + string.Format( + _translationService.GetResource("LoyaltyPoints.Message.ReturnedForOrder", + request.Order.CustomerLanguageId), request.Order.OrderNumber)); return true; diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs index d1055d0dcd..84fffbc195 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs @@ -46,7 +46,7 @@ public async Task HandleTest() { //Arrange var command = new AwardLoyaltyPointsCommand { - Order = new Order { StoreId = "", OrderNumber = 1 } + Order = new Order { StoreId = "", OrderNumber = 1, CustomerLanguageId = "lang1" } }; var expectedCustomer = new Customer { Username = "John", Active = true }; _customerServiceMock.Setup(c => c.GetCustomerById(It.IsAny())) @@ -55,7 +55,9 @@ public async Task HandleTest() .Returns(() => Task.FromResult(new Currency())); _currencyServiceMock.Setup(c => c.ConvertToPrimaryStoreCurrency(100, It.IsAny())) .Returns(() => Task.FromResult((double)100)); - _translationServiceMock.Setup(x => x.GetResource(It.IsAny())).Returns("Name"); + //the message is written in the language the order was placed in + _translationServiceMock.Setup(x => x.GetResource(It.IsAny(), "lang1", It.IsAny(), + It.IsAny())).Returns("Name"); _mediatorMock.Setup(x => x.Send(It.IsAny(), default)) .Returns(Task.FromResult(100)); @@ -64,7 +66,7 @@ public async Task HandleTest() //Assert _loyaltyPointsServiceMock.Verify( - c => c.AddLoyaltyPointsHistory(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + c => c.AddLoyaltyPointsHistory(It.IsAny(), It.IsAny(), It.IsAny(), "Name", It.IsAny(), It.IsAny()), Times.Once); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs index ccc1885de0..c8e419f6e3 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs @@ -46,7 +46,7 @@ public async Task HandleTest() { //Arrange var command = new ReduceLoyaltyPointsCommand { - Order = new Order { StoreId = "", OrderNumber = 1, CalcLoyaltyPoints = 100 } + Order = new Order { StoreId = "", OrderNumber = 1, CalcLoyaltyPoints = 100, CustomerLanguageId = "lang1" } }; var expectedCustomer = new Customer { Username = "John", Active = true }; _customerServiceMock.Setup(c => c.GetCustomerById(It.IsAny())) @@ -55,7 +55,9 @@ public async Task HandleTest() .Returns(() => Task.FromResult(new Currency())); _currencyServiceMock.Setup(c => c.ConvertToPrimaryStoreCurrency(100, It.IsAny())) .Returns(() => Task.FromResult((double)100)); - _translationServiceMock.Setup(x => x.GetResource(It.IsAny())).Returns("Name"); + //the message is written in the language the order was placed in + _translationServiceMock.Setup(x => x.GetResource(It.IsAny(), "lang1", It.IsAny(), + It.IsAny())).Returns("Name"); _mediatorMock.Setup(x => x.Send(It.IsAny(), default)) .Returns(Task.FromResult(100)); @@ -65,6 +67,6 @@ public async Task HandleTest() //Assert _loyaltyPointsServiceMock.Verify( c => c.AddLoyaltyPointsHistory(It.IsAny(), It.Is(g => g < 0), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + "Name", It.IsAny(), It.IsAny()), Times.Once); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs index 062a4b373e..3c92e6403c 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs @@ -29,14 +29,16 @@ public async Task HandleTest() { //Arrange var command = new ReturnBackRedeemedLoyaltyPointsCommand { - Order = new Order { RedeemedLoyaltyPoints = 10 } + Order = new Order { RedeemedLoyaltyPoints = 10, CustomerLanguageId = "lang1" } }; - _translationServiceMock.Setup(x => x.GetResource(It.IsAny())).Returns("Name"); + //the message is written in the language the order was placed in + _translationServiceMock.Setup(x => x.GetResource(It.IsAny(), "lang1", It.IsAny(), + It.IsAny())).Returns("Name"); //Act await _handler.Handle(command, CancellationToken.None); _loyaltyPointsServiceMock.Verify( c => c.AddLoyaltyPointsHistory(It.IsAny(), It.Is(g => g < 0), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + "Name", It.IsAny(), It.IsAny()), Times.Once); } } \ No newline at end of file From 56e4a2c08d360a5d54f09209a3005e2054968db0 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 15:45:40 +0200 Subject: [PATCH 2/6] Pass an explicit language to cart warnings and sign-in events The cart validators and the loyalty points for registration read the working language of the current request through GetResource(name), so outside a storefront request (scheduled tasks, the API, background handlers) their messages came out empty or in the wrong language. - IShoppingCartValidator methods and the validator records take the language the warnings are written in. - IShoppingCartService.AddToCart, UpdateShoppingCartItem and MigrateShoppingCart, AddRequiredProductsCommand and ReOrderCommand carry it down to the validators. - CustomerLoggedInEvent and CustomerRegisteredEvent carry the working language of the request; IExternalAuthenticationService.Authenticate and IOidcCustomerResolver.ResolveForLogin take it from the controllers. A customer registered from an access token has none, and the registration loyalty points fall back to the store's default language. - EndAuctionsTask validates the winning bid in the admin language, since its warnings only reach the log. No GetResource(name) call is left in src/Business. --- .../Services/ExternalAuthenticationService.cs | 30 ++++--- .../Services/OidcCustomerResolver.cs | 20 +++-- .../AddRequiredProductsCommandHandler.cs | 4 +- .../Orders/PlaceOrderCommandHandler.cs | 4 +- .../Handlers/Orders/ReOrderCommandHandler.cs | 3 +- .../CustomerRegisteredEventHandler.cs | 32 +++++-- .../CustomerLoggedInEventHandler.cs | 2 +- .../Services/Orders/ShoppingCartService.cs | 29 +++--- .../Services/Orders/ShoppingCartValidator.cs | 77 ++++++++++------ .../ShoppingCartAuctionValidator.cs | 10 ++- ...ShoppingCartCheckoutAttributesValidator.cs | 10 ++- .../ShoppingCartCommonWarningsValidator.cs | 10 ++- .../ShoppingCartGiftVoucherValidator.cs | 12 +-- .../ShoppingCartInventoryProductValidator.cs | 30 ++++--- .../ShoppingCartItemAttributeValidator.cs | 12 +-- .../ShoppingCartRequiredProductValidator.cs | 6 +- ...ShoppingCartReservationProductValidator.cs | 28 +++--- .../ShoppingCartStandardValidator.cs | 22 ++--- .../ShoppingCartTotalAmountValidator.cs | 10 ++- .../ShoppingCartWarningsValidator.cs | 10 ++- .../Orders/AddRequiredProductsCommand.cs | 6 ++ .../Checkout/Orders/ReOrderCommand.cs | 8 +- .../Events/Customers/CustomerLoggedInEvent.cs | 9 +- .../Customers/CustomerRegisteredEvent.cs | 10 ++- .../IExternalAuthenticationService.cs | 4 +- .../Authentication/IOidcCustomerResolver.cs | 5 +- .../Checkout/Orders/IShoppingCartService.cs | 11 ++- .../Checkout/Orders/IShoppingCartValidator.cs | 34 ++++--- .../BackgroundServices/EndAuctionsTask.cs | 12 ++- .../FacebookAuthenticationController.cs | 3 +- .../GoogleAuthenticationController.cs | 3 +- .../Controllers/OidcLoginController.cs | 5 +- .../ExternalAuthenticationServiceTests.cs | 15 ++-- .../OidcCustomerResolverLoginTests.cs | 88 ++++++++++--------- .../AddRequiredProductsCommandHandlerTests.cs | 5 +- .../CustomerRegisteredEventHandlerTests.cs | 68 ++++++++++++-- .../CustomerLoggedInEventHandlerTests.cs | 13 +-- .../Orders/ShoppingCartServiceTests.cs | 68 +++++++------- .../Orders/ShoppingCartValidatorTests.cs | 10 ++- .../Validators/ShoppingCartValidatorsTests.cs | 84 +++++++++++++----- ...ustomerLoggedInNotificationHandlerTests.cs | 4 +- .../OpenIdConnect/OidcLoginControllerTests.cs | 41 +++++---- .../BackgroundService/EndAuctionsTaskTests.cs | 19 +++- .../Controllers/BaseLoginController.cs | 3 +- .../Services/CustomerViewModelService.cs | 1 + .../Services/OrderViewModelService.cs | 5 +- .../Controllers/AccountController.cs | 6 +- .../Controllers/ActionCartController.cs | 16 ++-- .../Controllers/CheckoutController.cs | 8 +- .../Grand.Web/Controllers/OrderController.cs | 5 +- .../Controllers/ShoppingCartController.cs | 4 +- .../Controllers/WishlistController.cs | 6 +- .../ShoppingCart/GetShoppingCartHandler.cs | 4 +- .../ShoppingCart/GetWishlistHandler.cs | 4 +- 54 files changed, 623 insertions(+), 325 deletions(-) diff --git a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs index e1d42be685..2cb6fd3b4a 100644 --- a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs @@ -8,6 +8,7 @@ using Grand.Business.Core.Utilities.Customers; using Grand.Data; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Stores; using Grand.Infrastructure.Extensions; using Grand.SharedKernel.Extensions; @@ -70,14 +71,15 @@ public ExternalAuthenticationService( /// Current logged-in user /// Customer of the request (a guest when nobody is logged in) /// Store identifier + /// Working language of the request /// URL to which the user will return after authentication /// Result of an authentication protected virtual async Task AuthenticateExistingUser(Customer associatedUser, - Customer currentLoggedInUser, Customer customer, string storeId, string returnUrl) + Customer currentLoggedInUser, Customer customer, string storeId, Language language, string returnUrl) { //log in guest user if (currentLoggedInUser == null) - return await LoginUser(associatedUser, customer, storeId, returnUrl); + return await LoginUser(associatedUser, customer, storeId, language, returnUrl); //account is already assigned to another user if (currentLoggedInUser.Id != associatedUser.Id) @@ -95,10 +97,11 @@ protected virtual async Task AuthenticateExistingUser(Customer as /// Authentication parameters received from external authentication method /// Customer of the request (a guest when nobody is logged in) /// Store identifier + /// Working language of the request /// URL to which the user will return after authentication /// Result of an authentication protected virtual async Task AuthenticateNewUser(Customer currentLoggedInUser, - ExternalAuthParam parameters, Customer customer, string storeId, string returnUrl) + ExternalAuthParam parameters, Customer customer, string storeId, Language language, string returnUrl) { //associate external account with logged-in user if (currentLoggedInUser != null) @@ -111,7 +114,7 @@ protected virtual async Task AuthenticateNewUser(Customer current //or try to register new user if (_customerSettings.UserRegistrationType != UserRegistrationType.Disabled) - return await RegisterNewUser(parameters, customer, storeId, returnUrl); + return await RegisterNewUser(parameters, customer, storeId, language, returnUrl); //registration is disabled return Error(["Registration is disabled"]); @@ -123,10 +126,11 @@ protected virtual async Task AuthenticateNewUser(Customer current /// Authentication parameters received from external authentication method /// Guest customer of the request; registration turns it into the new user /// Store identifier + /// Working language of the request /// URL to which the user will return after authentication /// Result of an authentication protected virtual async Task RegisterNewUser(ExternalAuthParam parameters, Customer customer, - string storeId, string returnUrl) + string storeId, Language language, string returnUrl) { var approved = _customerSettings.UserRegistrationType is UserRegistrationType.Standard @@ -147,7 +151,7 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard await _mediator.Publish(new RegisteredByExternalMethod(customer, parameters)); //raise customer registered event - await _mediator.Publish(new CustomerRegisteredEvent(customer)); + await _mediator.Publish(new CustomerRegisteredEvent(customer, language)); //associate external account with registered user await AssociateCustomer(customer, parameters); @@ -169,13 +173,14 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard /// User to login /// Customer of the request before the login (the guest whose cart is merged) /// Store identifier + /// Working language of the request /// URL to which the user will return after authentication /// Result of an authentication protected virtual async Task LoginUser(Customer user, Customer previousCustomer, string storeId, - string returnUrl) + Language language, string returnUrl) { //raise event - await _mediator.Publish(new CustomerLoggedInEvent(user, previousCustomer, storeId)); + await _mediator.Publish(new CustomerLoggedInEvent(user, previousCustomer, storeId, language)); //authenticate await _authenticationService.SignIn(user, false); @@ -270,14 +275,16 @@ public virtual bool AuthenticationProviderIsAvailable(string systemName, Custome /// External authentication parameters /// Customer of the request (a guest when nobody is logged in) /// Store of the request + /// Working language of the request /// URL to which the user will return after authentication /// Result of an authentication public virtual async Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, - string returnUrl = null) + Language language, string returnUrl = null) { ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(language); if (!AuthenticationProviderIsAvailable(parameters.ProviderSystemName, customer, store)) return Error(["External authentication method cannot be loaded"]); @@ -288,10 +295,11 @@ public virtual async Task Authenticate(ExternalAuthParam paramete //authenticate associated user if already exists var associatedUser = await GetCustomer(parameters); if (associatedUser != null) - return await AuthenticateExistingUser(associatedUser, currentLoggedInUser, customer, store.Id, returnUrl); + return await AuthenticateExistingUser(associatedUser, currentLoggedInUser, customer, store.Id, language, + returnUrl); //or associate and authenticate new user - return await AuthenticateNewUser(currentLoggedInUser, parameters, customer, store.Id, returnUrl); + return await AuthenticateNewUser(currentLoggedInUser, parameters, customer, store.Id, language, returnUrl); } #endregion diff --git a/src/Business/Grand.Business.Authentication/Services/OidcCustomerResolver.cs b/src/Business/Grand.Business.Authentication/Services/OidcCustomerResolver.cs index 6d4c3871be..41fa7461a5 100644 --- a/src/Business/Grand.Business.Authentication/Services/OidcCustomerResolver.cs +++ b/src/Business/Grand.Business.Authentication/Services/OidcCustomerResolver.cs @@ -10,6 +10,7 @@ using Grand.Data; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Infrastructure.Configuration; using Grand.Mediator; @@ -146,7 +147,8 @@ await RequiresPassword(linkedCustomer, storeId)) return result; } - public virtual async Task ResolveForLogin(string sub, string email, bool emailVerified, string storeId) + public virtual async Task ResolveForLogin(string sub, string email, bool emailVerified, string storeId, + Language language) { if (string.IsNullOrEmpty(sub)) return OidcLoginResult.Refused(OidcLoginRefusal.InvalidIdentity); @@ -186,7 +188,7 @@ public virtual async Task ResolveForLogin(string sub, string em { //provider-only with automatic accounts: the provider decides who gets in, no form and no registration type if (_loginConfig is { ProviderOnly: true, AutoProvision: true }) - return await ProvisionForLogin(sub, email, emailVerified, storeId); + return await ProvisionForLogin(sub, email, emailVerified, storeId, language); var customerSettings = await _settingService.LoadSetting(storeId); if (customerSettings.UserRegistrationType == UserRegistrationType.Disabled) @@ -277,7 +279,8 @@ private async Task Register(string sub, string email, string if (existing != null) return await Usable(await _customerService.GetCustomerById(existing.CustomerId), sub, storeId); - var (outcome, customer, winner) = await CreateLinkedAccount(sub, email, storeId); + //an access token carries no working language: the registration falls back to the store's + var (outcome, customer, winner) = await CreateLinkedAccount(sub, email, storeId, null); return outcome switch { Provisioning.UserNameTaken => OidcCustomerResult.Forbidden("The user name is already taken"), Provisioning.LinkedToOtherIdentity => LinkedToOtherIdentity(customer, sub), @@ -290,14 +293,15 @@ private async Task Register(string sub, string email, string /// ProviderOnly + AutoProvision: the account of an unknown, provider-verified e-mail, created and linked at its first /// browser sign-in (the profile sync of the sign-in fills in the names) /// - private async Task ProvisionForLogin(string sub, string email, bool emailVerified, string storeId) + private async Task ProvisionForLogin(string sub, string email, bool emailVerified, string storeId, + Language language) { - var (outcome, customer, _) = await CreateLinkedAccount(sub, email, storeId); + var (outcome, customer, _) = await CreateLinkedAccount(sub, email, storeId, language); return outcome switch { Provisioning.UserNameTaken => OidcLoginResult.Refused(OidcLoginRefusal.AccountUnusable), Provisioning.LinkedToOtherIdentity => OidcLoginResult.Refused(OidcLoginRefusal.LinkedToOtherIdentity), //another request of the same user created the account first: its link answers now - Provisioning.LostRace => await ResolveForLogin(sub, email, emailVerified, storeId), + Provisioning.LostRace => await ResolveForLogin(sub, email, emailVerified, storeId, language), _ => OidcLoginResult.SignIn(customer) }; } @@ -315,7 +319,7 @@ private enum Provisioning /// e-mail validation / admin approval does not apply: access comes from the provider and from permissions an admin grants /// private async Task<(Provisioning Outcome, Customer Customer, Customer Winner)> CreateLinkedAccount(string sub, - string email, string storeId) + string email, string storeId, Language language) { //the e-mail becomes the user name - another account may hold it as its user name without that e-mail var normalizedEmail = email.ToLowerInvariant(); @@ -347,7 +351,7 @@ await _customerManagerService.RegisterCustomer(new RegistrationRequest(customer, } //only after the link is stored, so the loser of a race sends no welcome message - await _mediator.Publish(new CustomerRegisteredEvent(customer)); + await _mediator.Publish(new CustomerRegisteredEvent(customer, language)); _logger.LogInformation("Subject {Subject} registered as customer {CustomerId} (store {StoreId})", sub, customer.Id, storeId); return (Provisioning.Created, customer, null); diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs index 48167b835c..9736e8399b 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs @@ -55,6 +55,7 @@ public async Task Handle(AddRequiredProductsCommand request, CancellationT rp.Id, shoppingCartType, storeId, + request.Language, automaticallyAddRequiredProductsIfEnabled: false, validator: new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false }); @@ -63,7 +64,8 @@ await Handle(new AddRequiredProductsCommand { Customer = customer, ShoppingCartType = shoppingCartType, StoreId = storeId, - Product = rp + Product = rp, + Language = request.Language }, cancellationToken); } } diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index f8e8280201..01527bb29d 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -458,7 +458,7 @@ await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, d //validate the entire shopping cart var warnings = await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, details.Context, - true, true); + details.WorkingLanguage, true, true); if (warnings.Any()) throw new GrandException(string.Join(", ", warnings)); //validate individual cart items @@ -466,7 +466,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check { var product = await _productService.GetProductById(sci.ProductId); var sciWarnings = await _shoppingCartValidator.GetShoppingCartItemWarnings(details.Customer, sci, product, - new ShoppingCartValidatorOptions()); + new ShoppingCartValidatorOptions(), details.WorkingLanguage); if (sciWarnings.Any()) { var warningsSb = new StringBuilder(); diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs index 56dca1e45c..598645678c 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs @@ -32,6 +32,7 @@ public ReOrderCommandHandler( public async Task> Handle(ReOrderCommand request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request.Order); + ArgumentNullException.ThrowIfNull(request.Language); var warnings = new List(); var customer = await _customerService.GetCustomerById(request.Order.CustomerId); @@ -43,7 +44,7 @@ public async Task> Handle(ReOrderCommand request, CancellationToke { if (product.ProductTypeId == ProductType.SimpleProduct) warnings.AddRange((await _shoppingCartService.AddToCart(customer, orderItem.ProductId, - ShoppingCartType.ShoppingCart, request.Order.StoreId, orderItem.WarehouseId, + ShoppingCartType.ShoppingCart, request.Order.StoreId, request.Language, orderItem.WarehouseId, orderItem.Attributes, product.EnteredPrice ? _taxSettings.PricesIncludeTax ? orderItem.UnitPriceInclTax : orderItem.UnitPriceExclTax diff --git a/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs b/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs index dbcaf218d6..dda999ff99 100644 --- a/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs +++ b/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Domain.Orders; using Grand.Mediator; @@ -8,29 +9,46 @@ namespace Grand.Business.Checkout.Events.Customers; public class CustomerRegisteredEventHandler : INotificationHandler { + private readonly ILanguageService _languageService; private readonly ILoyaltyPointsService _loyaltyPointsService; private readonly LoyaltyPointsSettings _loyaltyPointsSettings; + private readonly IStoreService _storeService; private readonly ITranslationService _translationService; public CustomerRegisteredEventHandler( ITranslationService translationService, ILoyaltyPointsService loyaltyPointsService, - LoyaltyPointsSettings loyaltyPointsSettings + LoyaltyPointsSettings loyaltyPointsSettings, + IStoreService storeService, + ILanguageService languageService ) { _translationService = translationService; _loyaltyPointsService = loyaltyPointsService; _loyaltyPointsSettings = loyaltyPointsSettings; + _storeService = storeService; + _languageService = languageService; } public async Task Handle(CustomerRegisteredEvent notification, CancellationToken cancellationToken) { //Add loyalty points for customer registration (if enabled) - if (_loyaltyPointsSettings.Enabled && - _loyaltyPointsSettings.PointsForRegistration > 0) - await _loyaltyPointsService.AddLoyaltyPointsHistory(notification.Customer.Id, - _loyaltyPointsSettings.PointsForRegistration, - notification.Customer.StoreId, - _translationService.GetResource("LoyaltyPoints.Message.EarnedForRegistration")); + if (!_loyaltyPointsSettings.Enabled || _loyaltyPointsSettings.PointsForRegistration <= 0) + return; + + var languageId = notification.Language?.Id ?? await GetStoreLanguageId(notification.Customer.StoreId); + await _loyaltyPointsService.AddLoyaltyPointsHistory(notification.Customer.Id, + _loyaltyPointsSettings.PointsForRegistration, + notification.Customer.StoreId, + _translationService.GetResource("LoyaltyPoints.Message.EarnedForRegistration", languageId)); + } + + private async Task GetStoreLanguageId(string storeId) + { + var store = await _storeService.GetStoreById(storeId); + if (!string.IsNullOrEmpty(store?.DefaultLanguageId)) + return store.DefaultLanguageId; + + return (await _languageService.GetAllLanguages(storeId: storeId)).FirstOrDefault()?.Id; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs b/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs index 22e9efb41c..750c49cce1 100644 --- a/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs +++ b/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs @@ -17,6 +17,6 @@ public async Task Handle(CustomerLoggedInEvent notification, CancellationToken c { //migrate shopping cart await _shoppingCartService.MigrateShoppingCart(notification.PreviousCustomer, notification.Customer, - notification.StoreId, true); + notification.StoreId, notification.Language, true); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs index ba0a992d36..f9a3d22a1c 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs @@ -7,6 +7,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Infrastructure.Extensions; using Grand.Mediator; @@ -170,6 +171,7 @@ public virtual async Task FindShoppingCartItem(IListProduct id /// ShoppingCartType /// Store id + /// Language the warnings are written in /// Warehouse id /// Attributes /// EnteredPrice @@ -184,7 +186,7 @@ public virtual async Task FindShoppingCartItem(IList(warnings, shoppingCartItem) public virtual async Task<(IList warnings, ShoppingCartItem shoppingCartItem)> AddToCart(Customer customer, string productId, - ShoppingCartType shoppingCartType, string storeId, + ShoppingCartType shoppingCartType, string storeId, Language language, string warehouseId = null, IList attributes = null, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, @@ -206,7 +208,7 @@ public virtual async Task FindShoppingCartItem(IList FindShoppingCartItem(IList InsertNewItem( - Customer customer, Product product, ShoppingCartItem shoppingCartItem, + Customer customer, Product product, ShoppingCartItem shoppingCartItem, Language language, bool automaticallyAddRequiredProductsIfEnabled) { customer.ShoppingCartItems.Add(shoppingCartItem); @@ -289,7 +292,8 @@ await _mediator.Send(new AddRequiredProductsCommand { Customer = customer, Product = product, ShoppingCartType = shoppingCartItem.ShoppingCartTypeId, - StoreId = shoppingCartItem.StoreId + StoreId = shoppingCartItem.StoreId, + Language = language }); return shoppingCartItem; @@ -302,6 +306,7 @@ await _mediator.Send(new AddRequiredProductsCommand { /// Shopping cart item identifier /// /// Attributes + /// Language the warnings are written in /// New customer entered price /// Rental start date /// Rental end date @@ -311,7 +316,7 @@ await _mediator.Send(new AddRequiredProductsCommand { /// /// Warnings public virtual async Task> UpdateShoppingCartItem(Customer customer, - string shoppingCartItemId, string warehouseId, IList attributes, + string shoppingCartItemId, string warehouseId, IList attributes, Language language, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, int quantity = 1, bool resetCheckoutData = true, string reservationId = "", string sciId = "") @@ -342,7 +347,7 @@ public virtual async Task> UpdateShoppingCartItem(Customer custome shoppingCartItem.IsGiftVoucher = product.IsGiftVoucher; //check warnings warnings.AddRange(await _shoppingCartValidator.GetShoppingCartItemWarnings(customer, shoppingCartItem, - product, new ShoppingCartValidatorOptions())); + product, new ShoppingCartValidatorOptions(), language)); if (warnings.Any()) return warnings; //if everything is OK, then update a shopping cart item await _customerService.UpdateShoppingCartItem(customer.Id, shoppingCartItem); @@ -395,12 +400,13 @@ public virtual async Task DeleteShoppingCartItem(Customer customer, ShoppingCart /// /// Store whose selected checkout attributes move along, when the source customer has selected any there /// + /// Language the items are validated in /// /// A value indicating whether to coupon codes (discount and gift voucher) should be also /// re-applied /// public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, - bool includeCouponCodes) + Language language, bool includeCouponCodes) { ArgumentNullException.ThrowIfNull(fromCustomer); ArgumentNullException.ThrowIfNull(toCustomer); @@ -416,10 +422,11 @@ public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer to { var product = await _productService.GetProductById(sci.ProductId); if (product != null && - (await _shoppingCartValidator.GetRequiredProductWarnings(fromCustomer, sci, product, sci.StoreId)).Any()) + (await _shoppingCartValidator.GetRequiredProductWarnings(fromCustomer, sci, product, sci.StoreId, + language)).Any()) continue; - await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, sci.WarehouseId, + await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, language, sci.WarehouseId, sci.Attributes, sci.EnteredPrice, sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false, sci.ReservationId, sci.Parameter, sci.Duration, diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs index 47c4cfdd6b..b3127762e5 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs @@ -5,6 +5,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Infrastructure.Validators; using Grand.Mediator; @@ -25,15 +26,16 @@ public ShoppingCartValidator( } public virtual async Task> GetStandardWarnings(Customer customer, Product product, - ShoppingCartItem shoppingCartItem) + ShoppingCartItem shoppingCartItem, Language language) { ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(product); + ArgumentNullException.ThrowIfNull(language); var warnings = new List(); var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartStandardValidatorRecord(customer, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartStandardValidatorRecord(customer, product, shoppingCartItem, language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -41,15 +43,16 @@ public virtual async Task> GetStandardWarnings(Customer customer, } public virtual async Task> GetShoppingCartItemAttributeWarnings(Customer customer, Product product, - ShoppingCartItem shoppingCartItem, bool ignoreNonCombinableAttributes = false) + ShoppingCartItem shoppingCartItem, Language language, bool ignoreNonCombinableAttributes = false) { ArgumentNullException.ThrowIfNull(product); + ArgumentNullException.ThrowIfNull(language); var warnings = new List(); var result = await _validatorFactory.GetValidator() .ValidateAsync(new ShoppingCartItemAttributeValidatorRecord(customer, product, shoppingCartItem, - ignoreNonCombinableAttributes)); + ignoreNonCombinableAttributes, language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -58,16 +61,17 @@ public virtual async Task> GetShoppingCartItemAttributeWarnings(Cu public virtual async Task> GetShoppingCartItemGiftVoucherWarnings(Customer customer, - Product product, ShoppingCartItem shoppingCartItem) + Product product, ShoppingCartItem shoppingCartItem, Language language) { ArgumentNullException.ThrowIfNull(product); + ArgumentNullException.ThrowIfNull(language); var warnings = new List(); //gift vouchers if (!product.IsGiftVoucher) return warnings; var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartGiftVoucherValidatorRecord(customer, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartGiftVoucherValidatorRecord(customer, product, shoppingCartItem, language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -75,12 +79,15 @@ public virtual async Task> GetShoppingCartItemGiftVoucherWarnings( } public virtual async Task> GetInventoryProductWarnings(Customer customer, Product product, - ShoppingCartItem shoppingCartItem) + ShoppingCartItem shoppingCartItem, Language language) { + ArgumentNullException.ThrowIfNull(language); + var warnings = new List(); var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartInventoryProductValidatorRecord(customer, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartInventoryProductValidatorRecord(customer, product, shoppingCartItem, + language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -88,12 +95,15 @@ public virtual async Task> GetInventoryProductWarnings(Customer cu } - public virtual async Task> GetAuctionProductWarning(double bid, Product product, Customer customer) + public virtual async Task> GetAuctionProductWarning(double bid, Product product, Customer customer, + Language language) { + ArgumentNullException.ThrowIfNull(language); + var warnings = new List(); if (product.ProductTypeId != ProductType.Auction) return warnings; var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartAuctionValidatorRecord(customer, product, null, bid)); + .ValidateAsync(new ShoppingCartAuctionValidatorRecord(customer, product, null, bid, language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -101,15 +111,18 @@ public virtual async Task> GetAuctionProductWarning(double bid, Pr } public virtual async Task> GetReservationProductWarnings(Customer customer, Product product, - ShoppingCartItem shoppingCartItem) + ShoppingCartItem shoppingCartItem, Language language) { + ArgumentNullException.ThrowIfNull(language); + var warnings = new List(); if (product.ProductTypeId != ProductType.Reservation) return warnings; var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartReservationProductValidatorRecord(customer, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartReservationProductValidatorRecord(customer, product, shoppingCartItem, + language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -117,16 +130,18 @@ public virtual async Task> GetReservationProductWarnings(Customer } public virtual async Task> GetShoppingCartWarnings(IList shoppingCart, - IList checkoutAttributes, CalculationContext context, bool validateCheckoutAttributes, - bool validateAmount) + IList checkoutAttributes, CalculationContext context, Language language, + bool validateCheckoutAttributes, bool validateAmount) { ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(language); var warnings = new List(); checkoutAttributes ??= new List(); var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartWarningsValidatorRecord(context.Customer, context.Store, shoppingCart)); + .ValidateAsync(new ShoppingCartWarningsValidatorRecord(context.Customer, context.Store, shoppingCart, + language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -136,7 +151,7 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( new ShoppingCartCheckoutAttributesValidatorRecord(context.Customer, context.Store, - shoppingCart, checkoutAttributes)); + shoppingCart, checkoutAttributes, language)); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); } @@ -146,7 +161,7 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( - new ShoppingCartTotalAmountValidatorRecord(shoppingCart, context)); + new ShoppingCartTotalAmountValidatorRecord(shoppingCart, context, language)); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); } @@ -160,13 +175,15 @@ public virtual async Task> GetShoppingCartWarnings(IList> CheckCommonWarnings(Customer customer, IList currentCart, Product product, ShoppingCartType shoppingCartType, DateTime? rentalStartDate, DateTime? rentalEndDate, - int quantity, string reservationId) + int quantity, string reservationId, Language language) { + ArgumentNullException.ThrowIfNull(language); + var warnings = new List(); var result = await _validatorFactory.GetValidator().ValidateAsync( new ShoppingCartCommonWarningsValidatorRecord(customer, currentCart, - product, shoppingCartType, rentalStartDate, rentalEndDate, quantity, reservationId)); + product, shoppingCartType, rentalStartDate, rentalEndDate, quantity, reservationId, language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -175,7 +192,7 @@ public async Task> CheckCommonWarnings(Customer customer, IList> GetShoppingCartItemWarnings(Customer customer, ShoppingCartItem shoppingCartItem, - Product product, ShoppingCartValidatorOptions options) + Product product, ShoppingCartValidatorOptions options, Language language) { ArgumentNullException.ThrowIfNull(product); @@ -183,28 +200,30 @@ public virtual async Task> GetShoppingCartItemWarnings(Customer cu //standard properties if (options.GetStandardWarnings) - warnings.AddRange(await GetStandardWarnings(customer, product, shoppingCartItem)); + warnings.AddRange(await GetStandardWarnings(customer, product, shoppingCartItem, language)); //inventory properties if (options.GetInventoryWarnings) - warnings.AddRange(await GetInventoryProductWarnings(customer, product, shoppingCartItem)); + warnings.AddRange(await GetInventoryProductWarnings(customer, product, shoppingCartItem, language)); //selected attributes if (options.GetAttributesWarnings) - warnings.AddRange(await GetShoppingCartItemAttributeWarnings(customer, product, shoppingCartItem)); + warnings.AddRange( + await GetShoppingCartItemAttributeWarnings(customer, product, shoppingCartItem, language)); //gift vouchers if (options.GetGiftVoucherWarnings) - warnings.AddRange(await GetShoppingCartItemGiftVoucherWarnings(customer, product, shoppingCartItem)); + warnings.AddRange( + await GetShoppingCartItemGiftVoucherWarnings(customer, product, shoppingCartItem, language)); //required products if (options.GetRequiredProductWarnings) warnings.AddRange(await GetRequiredProductWarnings(customer, shoppingCartItem, product, - shoppingCartItem.StoreId)); + shoppingCartItem.StoreId, language)); //reservation products if (options.GetReservationWarnings) - warnings.AddRange(await GetReservationProductWarnings(customer, product, shoppingCartItem)); + warnings.AddRange(await GetReservationProductWarnings(customer, product, shoppingCartItem, language)); //event notification await _mediator.ShoppingCartItemWarningsAdded(warnings, customer, shoppingCartItem, product); @@ -213,17 +232,19 @@ public virtual async Task> GetShoppingCartItemWarnings(Customer cu } public virtual async Task> GetRequiredProductWarnings(Customer customer, - ShoppingCartItem shoppingCartItem, Product product, string storeId) + ShoppingCartItem shoppingCartItem, Product product, string storeId, Language language) { ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(product); + ArgumentNullException.ThrowIfNull(language); var warnings = new List(); if (!product.RequireOtherProducts) return warnings; var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartRequiredProductValidatorRecord(customer, storeId, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartRequiredProductValidatorRecord(customer, storeId, product, shoppingCartItem, + language)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); return warnings; diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartAuctionValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartAuctionValidator.cs index 2543c9b174..85565c7b2e 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartAuctionValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartAuctionValidator.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; @@ -10,7 +11,8 @@ public record ShoppingCartAuctionValidatorRecord( Customer Customer, Product Product, ShoppingCartItem ShoppingCartItem, - double Bid); + double Bid, + Language Language); public class ShoppingCartAuctionValidator : AbstractValidator { @@ -19,13 +21,13 @@ public ShoppingCartAuctionValidator(ITranslationService translationService) RuleFor(x => x).Custom((value, context) => { if (value.Bid <= value.Product.HighestBid || value.Bid <= value.Product.StartPrice) - context.AddFailure(translationService.GetResource("ShoppingCart.BidMustBeHigher")); + context.AddFailure(translationService.GetResource("ShoppingCart.BidMustBeHigher", value.Language.Id)); if (!value.Product.AvailableEndDateTimeUtc.HasValue) - context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable")); + context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable", value.Language.Id)); if (value.Product.AvailableEndDateTimeUtc < DateTime.UtcNow) - context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable")); + context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable", value.Language.Id)); }); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs index 8ff102e60a..4b543f98db 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs @@ -5,6 +5,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; @@ -14,7 +15,8 @@ public record ShoppingCartCheckoutAttributesValidatorRecord( Customer Customer, Store Store, IList ShoppingCarts, - IList CheckoutAttributes); + IList CheckoutAttributes, + Language Language); public class ShoppingCartCheckoutAttributesValidator : AbstractValidator { @@ -50,7 +52,7 @@ await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(value. //if not found if (!found) context.AddFailure(a2.TextPrompt ?? - string.Format(translationService.GetResource("ShoppingCart.SelectAttribute"), + string.Format(translationService.GetResource("ShoppingCart.SelectAttribute", value.Language.Id), a2.Name)); } @@ -69,7 +71,7 @@ await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(value. if (ca.ValidationMinLength.Value > enteredTextLength) context.AddFailure(string.Format( - translationService.GetResource("ShoppingCart.TextBoxMinimumLength"), ca.Name, + translationService.GetResource("ShoppingCart.TextBoxMinimumLength", value.Language.Id), ca.Name, ca.ValidationMinLength.Value)); } @@ -84,7 +86,7 @@ await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(value. if (ca.ValidationMaxLength.Value < enteredTextLength) context.AddFailure(string.Format( - translationService.GetResource("ShoppingCart.TextBoxMaximumLength"), ca.Name, + translationService.GetResource("ShoppingCart.TextBoxMaximumLength", value.Language.Id), ca.Name, ca.ValidationMaxLength.Value)); } } diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs index 43bd391384..b3d481a60d 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Permissions; @@ -16,7 +17,8 @@ public record ShoppingCartCommonWarningsValidatorRecord( DateTime? RentalStartDate, DateTime? RentalEndDate, int Quantity, - string ReservationId); + string ReservationId, + Language Language); public class ShoppingCartCommonWarningsValidator : AbstractValidator { @@ -32,7 +34,7 @@ public ShoppingCartCommonWarningsValidator(ITranslationService translationServic { if (value.ShoppingCarts.Count >= shoppingCartSettings.MaximumShoppingCartItems) context.AddFailure(string.Format( - translationService.GetResource("ShoppingCart.MaximumShoppingCartItems"), + translationService.GetResource("ShoppingCart.MaximumShoppingCartItems", value.Language.Id), shoppingCartSettings.MaximumShoppingCartItems)); } break; @@ -40,7 +42,7 @@ public ShoppingCartCommonWarningsValidator(ITranslationService translationServic { if (value.ShoppingCarts.Count >= shoppingCartSettings.MaximumWishlistItems) context.AddFailure(string.Format( - translationService.GetResource("ShoppingCart.MaximumWishlistItems"), + translationService.GetResource("ShoppingCart.MaximumWishlistItems", value.Language.Id), shoppingCartSettings.MaximumWishlistItems)); } break; @@ -59,7 +61,7 @@ public ShoppingCartCommonWarningsValidator(ITranslationService translationServic } if (value.Quantity <= 0) - context.AddFailure(translationService.GetResource("ShoppingCart.QuantityShouldPositive")); + context.AddFailure(translationService.GetResource("ShoppingCart.QuantityShouldPositive", value.Language.Id)); }); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartGiftVoucherValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartGiftVoucherValidator.cs index a98e725c34..f156b21ae5 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartGiftVoucherValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartGiftVoucherValidator.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.SharedKernel.Extensions; @@ -10,7 +11,8 @@ namespace Grand.Business.Checkout.Validators; public record ShoppingCartGiftVoucherValidatorRecord( Customer Customer, Product Product, - ShoppingCartItem ShoppingCartItem); + ShoppingCartItem ShoppingCartItem, + Language Language); public class ShoppingCartGiftVoucherValidator : AbstractValidator { @@ -23,21 +25,21 @@ public ShoppingCartGiftVoucherValidator(ITranslationService translationService) out var giftVoucherSenderName, out var giftVoucherSenderEmail, out _); if (string.IsNullOrEmpty(giftVoucherRecipientName)) - context.AddFailure(translationService.GetResource("ShoppingCart.RecipientNameError")); + context.AddFailure(translationService.GetResource("ShoppingCart.RecipientNameError", value.Language.Id)); if (value.Product.GiftVoucherTypeId == GiftVoucherType.Virtual) //validate for virtual gift vouchers only if (string.IsNullOrEmpty(giftVoucherRecipientEmail) || !CommonHelper.IsValidEmail(giftVoucherRecipientEmail)) - context.AddFailure(translationService.GetResource("ShoppingCart.RecipientEmailError")); + context.AddFailure(translationService.GetResource("ShoppingCart.RecipientEmailError", value.Language.Id)); if (string.IsNullOrEmpty(giftVoucherSenderName)) - context.AddFailure(translationService.GetResource("ShoppingCart.SenderNameError")); + context.AddFailure(translationService.GetResource("ShoppingCart.SenderNameError", value.Language.Id)); if (value.Product.GiftVoucherTypeId != GiftVoucherType.Virtual) return; //validate for virtual gift vouchers only if (string.IsNullOrEmpty(giftVoucherSenderEmail) || !CommonHelper.IsValidEmail(giftVoucherSenderEmail)) - context.AddFailure(translationService.GetResource("ShoppingCart.SenderEmailError")); + context.AddFailure(translationService.GetResource("ShoppingCart.SenderEmailError", value.Language.Id)); }); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartInventoryProductValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartInventoryProductValidator.cs index 6373cdb789..fe4b13b822 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartInventoryProductValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartInventoryProductValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; @@ -10,7 +11,8 @@ namespace Grand.Business.Checkout.Validators; public record ShoppingCartInventoryProductValidatorRecord( Customer Customer, Product Product, - ShoppingCartItem ShoppingCartItem); + ShoppingCartItem ShoppingCartItem, + Language Language); public class ShoppingCartInventoryProductValidator : AbstractValidator { @@ -23,25 +25,25 @@ public ShoppingCartInventoryProductValidator(ITranslationService translationServ var hasQtyWarnings = false; if (value.ShoppingCartItem.Quantity < value.Product.OrderMinimumQuantity) { - context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.MinimumQuantity"), + context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.MinimumQuantity", value.Language.Id), value.Product.OrderMinimumQuantity)); hasQtyWarnings = true; } if (value.ShoppingCartItem.Quantity > value.Product.OrderMaximumQuantity) { - context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.MaximumQuantity"), + context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.MaximumQuantity", value.Language.Id), value.Product.OrderMaximumQuantity)); hasQtyWarnings = true; } var allowedQuantities = value.Product.ParseAllowedQuantities(); if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(value.ShoppingCartItem.Quantity)) - context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.AllowedQuantities"), + context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.AllowedQuantities", value.Language.Id), string.Join(", ", allowedQuantities))); if (shoppingCartSettings.AllowToSelectWarehouse && string.IsNullOrEmpty(value.ShoppingCartItem.WarehouseId)) - context.AddFailure(translationService.GetResource("ShoppingCart.RequiredWarehouse")); + context.AddFailure(translationService.GetResource("ShoppingCart.RequiredWarehouse", value.Language.Id)); var warehouseId = !string.IsNullOrEmpty(value.ShoppingCartItem.WarehouseId) ? value.ShoppingCartItem.WarehouseId @@ -73,9 +75,9 @@ public ShoppingCartInventoryProductValidator(ITranslationService translationServ stockQuantityService.GetTotalStockQuantity(value.Product, warehouseId: warehouseId); if (maximumQuantityCanBeAdded < qty) context.AddFailure(maximumQuantityCanBeAdded <= 0 - ? translationService.GetResource("ShoppingCart.OutOfStock") + ? translationService.GetResource("ShoppingCart.OutOfStock", value.Language.Id) : string.Format( - translationService.GetResource("ShoppingCart.QuantityExceedsStock"), + translationService.GetResource("ShoppingCart.QuantityExceedsStock", value.Language.Id), maximumQuantityCanBeAdded)); } } @@ -93,7 +95,7 @@ public ShoppingCartInventoryProductValidator(ITranslationService translationServ stockQuantityService.GetTotalStockQuantity(p1, warehouseId: warehouseId); if (maximumQuantityCanBeAdded < qty) context.AddFailure(string.Format( - translationService.GetResource("ShoppingCart.OutOfStock.BundleProduct"), + translationService.GetResource("ShoppingCart.OutOfStock.BundleProduct", value.Language.Id), p1.Name)); } @@ -110,15 +112,15 @@ public ShoppingCartInventoryProductValidator(ITranslationService translationServ context.AddFailure(stockQuantity <= 0 ? string.Format( translationService.GetResource( - "ShoppingCart.OutOfStock.BundleProduct"), p1.Name) + "ShoppingCart.OutOfStock.BundleProduct", value.Language.Id), p1.Name) : string.Format( translationService.GetResource( - "ShoppingCart.QuantityExceedsStock.BundleProduct"), p1.Name, + "ShoppingCart.QuantityExceedsStock.BundleProduct", value.Language.Id), p1.Name, stockQuantity)); } else { - context.AddFailure(translationService.GetResource("ShoppingCart.Combination.NotExist")); + context.AddFailure(translationService.GetResource("ShoppingCart.Combination.NotExist", value.Language.Id)); } } } @@ -135,14 +137,14 @@ public ShoppingCartInventoryProductValidator(ITranslationService translationServ warehouseId: warehouseId); if (!combination.AllowOutOfStockOrders && stockQuantity < value.ShoppingCartItem.Quantity) context.AddFailure(stockQuantity <= 0 - ? translationService.GetResource("ShoppingCart.OutOfStock") + ? translationService.GetResource("ShoppingCart.OutOfStock", value.Language.Id) : string.Format( - translationService.GetResource("ShoppingCart.QuantityExceedsStock"), + translationService.GetResource("ShoppingCart.QuantityExceedsStock", value.Language.Id), stockQuantity)); } else { - context.AddFailure(translationService.GetResource("ShoppingCart.Combination.NotExist")); + context.AddFailure(translationService.GetResource("ShoppingCart.Combination.NotExist", value.Language.Id)); } } break; diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartItemAttributeValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartItemAttributeValidator.cs index 4473ef08f4..bbf8cff53c 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartItemAttributeValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartItemAttributeValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.SharedKernel.Extensions; @@ -12,7 +13,8 @@ public record ShoppingCartItemAttributeValidatorRecord( Customer Customer, Product Product, ShoppingCartItem ShoppingCartItem, - bool IgnoreNonCombinableAttributes); + bool IgnoreNonCombinableAttributes, + Language Language); public class ShoppingCartItemAttributeValidator : AbstractValidator { @@ -73,7 +75,7 @@ await productAttributeService.GetProductAttributeById(productAttributeMapping var attributeName = productAttribute.Name; var attributeValueName = attributeValue.Name; warnings.Add(string.Format( - translationService.GetResource("ShoppingCart.AssociatedAttributeWarning"), + translationService.GetResource("ShoppingCart.AssociatedAttributeWarning", value.Language.Id), attributeName, attributeValueName, associatedProductWarning)); } } @@ -157,7 +159,7 @@ where attributeValuesStr.Any(str1 => !string.IsNullOrEmpty(str1.Trim())) { var notFoundWarning = !string.IsNullOrEmpty(a2.TextPrompt) ? a2.TextPrompt - : string.Format(_translationService.GetResource("ShoppingCart.SelectAttribute"), paa.Name); + : string.Format(_translationService.GetResource("ShoppingCart.SelectAttribute", value.Language.Id), paa.Name); warnings.Add(notFoundWarning); } @@ -201,7 +203,7 @@ where attributeValuesStr.Any(str1 => !string.IsNullOrEmpty(str1.Trim())) { var productAttribute = await _productAttributeService.GetProductAttributeById(pam.ProductAttributeId); - warnings.Add(string.Format(_translationService.GetResource("ShoppingCart.TextboxMinimumLength"), + warnings.Add(string.Format(_translationService.GetResource("ShoppingCart.TextboxMinimumLength", value.Language.Id), productAttribute.Name, pam.ValidationMinLength.Value)); } } @@ -217,7 +219,7 @@ where attributeValuesStr.Any(str1 => !string.IsNullOrEmpty(str1.Trim())) if (pam.ValidationMaxLength.Value >= enteredTextLength) continue; var productAttribute = await _productAttributeService.GetProductAttributeById(pam.ProductAttributeId); - warnings.Add(string.Format(_translationService.GetResource("ShoppingCart.TextboxMaximumLength"), + warnings.Add(string.Format(_translationService.GetResource("ShoppingCart.TextboxMaximumLength", value.Language.Id), productAttribute.Name, pam.ValidationMaxLength.Value)); } } diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs index cc455c493b..fdf6a45f79 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; @@ -11,7 +12,8 @@ public record ShoppingCartRequiredProductValidatorRecord( Customer Customer, string StoreId, Product Product, - ShoppingCartItem ShoppingCartItem); + ShoppingCartItem ShoppingCartItem, + Language Language); public class ShoppingCartRequiredProductValidator : AbstractValidator { @@ -37,7 +39,7 @@ public ShoppingCartRequiredProductValidator(ITranslationService translationServi let alreadyInTheCart = cart.Any(sci => sci.ProductId == rp.Id) where !alreadyInTheCart select rp) - context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.RequiredProductWarning"), + context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.RequiredProductWarning", value.Language.Id), rp.Name)); }); } diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs index 4169b68dd9..fd918639d1 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartReservationProductValidator.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; @@ -11,7 +12,8 @@ namespace Grand.Business.Checkout.Validators; public record ShoppingCartReservationProductValidatorRecord( Customer Customer, Product Product, - ShoppingCartItem ShoppingCartItem); + ShoppingCartItem ShoppingCartItem, + Language Language); public class ShoppingCartReservationProductValidator : AbstractValidator { @@ -25,7 +27,7 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe var reservations = await productReservationService.GetCustomerReservationsHelpers(value.Customer.Id); if (value.Customer.ShoppingCartItems.All(x => x.Id != value.ShoppingCartItem.Id)) if (reservations.Any(x => x.ReservationId == value.ShoppingCartItem.ReservationId)) - context.AddFailure(translationService.GetResource("ShoppingCart.AlreadyReservation")); + context.AddFailure(translationService.GetResource("ShoppingCart.AlreadyReservation", value.Language.Id)); } if (value.ShoppingCartItem.RentalStartDateUtc.HasValue && value.ShoppingCartItem.RentalEndDateUtc.HasValue) @@ -46,7 +48,7 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe if (!canBeBook) { context.AddFailure( - translationService.GetResource("ShoppingCart.Reservation.NoFreeReservationsInThisPeriod")); + translationService.GetResource("ShoppingCart.Reservation.NoFreeReservationsInThisPeriod", value.Language.Id)); return; } } @@ -54,7 +56,7 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe if (string.IsNullOrEmpty(value.ShoppingCartItem.ReservationId) && value.Product.IntervalUnitId != IntervalUnit.Day) { - context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.NoReservationFound")); + context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.NoReservationFound", value.Language.Id)); return; } @@ -64,12 +66,12 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe await productReservationService.GetProductReservation(value.ShoppingCartItem.ReservationId); if (reservation == null) { - context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.ReservationDeleted")); + context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.ReservationDeleted", value.Language.Id)); } else { if (!string.IsNullOrEmpty(reservation.OrderId)) - context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.AlreadyReserved")); + context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.AlreadyReserved", value.Language.Id)); } } else @@ -77,7 +79,7 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe if (!(value.ShoppingCartItem.RentalStartDateUtc.HasValue && value.ShoppingCartItem.RentalEndDateUtc.HasValue)) { - context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.ChooseBothDates")); + context.AddFailure(translationService.GetResource("ShoppingCart.Reservation.ChooseBothDates", value.Language.Id)); } else { @@ -87,7 +89,7 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe value.ShoppingCartItem.RentalEndDateUtc.Value) context.AddFailure( translationService.GetResource( - "ShoppingCart.Reservation.EndDateMustBeLaterThanStartDate")); + "ShoppingCart.Reservation.EndDateMustBeLaterThanStartDate", value.Language.Id)); } else { @@ -95,14 +97,14 @@ public ShoppingCartReservationProductValidator(ITranslationService translationSe value.ShoppingCartItem.RentalEndDateUtc.Value) context.AddFailure( translationService.GetResource( - "ShoppingCart.Reservation.EndDateMustBeLaterThanStartDate")); + "ShoppingCart.Reservation.EndDateMustBeLaterThanStartDate", value.Language.Id)); } if (value.ShoppingCartItem.RentalStartDateUtc.Value < DateTime.UtcNow || value.ShoppingCartItem.RentalEndDateUtc.Value < DateTime.UtcNow) context.AddFailure( translationService.GetResource( - "ShoppingCart.Reservation.ReservationDatesMustBeLaterThanToday")); + "ShoppingCart.Reservation.ReservationDatesMustBeLaterThanToday", value.Language.Id)); if (value.Customer.ShoppingCartItems.Any(x => x.Id == value.ShoppingCartItem.Id)) { @@ -111,7 +113,7 @@ await productReservationService.GetCustomerReservationsHelperBySciId(value.Shopp .Id); if (!reserved.Any()) context.AddFailure( - translationService.GetResource("ShoppingCart.Reservation.ReservationNotExists")); + translationService.GetResource("ShoppingCart.Reservation.ReservationNotExists", value.Language.Id)); else foreach (var item in reserved) { @@ -120,13 +122,13 @@ await productReservationService.GetCustomerReservationsHelperBySciId(value.Shopp if (reservation == null) { context.AddFailure( - translationService.GetResource("ShoppingCart.Reservation.ReservationDeleted")); + translationService.GetResource("ShoppingCart.Reservation.ReservationDeleted", value.Language.Id)); break; } if (string.IsNullOrEmpty(reservation.OrderId)) continue; context.AddFailure( - translationService.GetResource("ShoppingCart.Reservation.AlreadyReserved")); + translationService.GetResource("ShoppingCart.Reservation.AlreadyReserved", value.Language.Id)); break; } } diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartStandardValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartStandardValidator.cs index 89cdc96c9f..5e20d81224 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartStandardValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartStandardValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; @@ -10,7 +11,8 @@ namespace Grand.Business.Checkout.Validators; public record ShoppingCartStandardValidatorRecord( Customer Customer, Product Product, - ShoppingCartItem ShoppingCartItem); + ShoppingCartItem ShoppingCartItem, + Language Language); public class ShoppingCartStandardValidator : AbstractValidator { @@ -19,7 +21,7 @@ public ShoppingCartStandardValidator(ITranslationService translationService, IAc RuleFor(x => x).Custom((value, context) => { if (!value.Product.Published) - context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished")); + context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished", value.Language.Id)); //we can't add grouped value.Product if (value.Product.ProductTypeId == ProductType.GroupedProduct) @@ -27,27 +29,27 @@ public ShoppingCartStandardValidator(ITranslationService translationService, IAc //ACL if (!aclService.Authorize(value.Product, value.Customer)) - context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished")); + context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished", value.Language.Id)); //Store acl if (!aclService.Authorize(value.Product, value.ShoppingCartItem.StoreId)) - context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished")); + context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished", value.Language.Id)); switch (value.ShoppingCartItem.ShoppingCartTypeId) { //disabled "add to cart" button case ShoppingCartType.ShoppingCart when value.Product.DisableBuyButton: - context.AddFailure(translationService.GetResource("ShoppingCart.BuyingDisabled")); + context.AddFailure(translationService.GetResource("ShoppingCart.BuyingDisabled", value.Language.Id)); break; //disabled "add to wishlist" button case ShoppingCartType.Wishlist when value.Product.DisableWishlistButton: - context.AddFailure(translationService.GetResource("ShoppingCart.WishlistDisabled")); + context.AddFailure(translationService.GetResource("ShoppingCart.WishlistDisabled", value.Language.Id)); break; } //call for price if (value.ShoppingCartItem.ShoppingCartTypeId == ShoppingCartType.ShoppingCart && - value.Product.CallForPrice) context.AddFailure(translationService.GetResource("Products.CallForPrice")); + value.Product.CallForPrice) context.AddFailure(translationService.GetResource("Products.CallForPrice", value.Language.Id)); //customer entered price if (value.Product.EnteredPrice) @@ -56,7 +58,7 @@ public ShoppingCartStandardValidator(ITranslationService translationService, IAc if (shoppingCartItemEnteredPrice < value.Product.MinEnteredPrice || shoppingCartItemEnteredPrice > value.Product.MaxEnteredPrice) context.AddFailure(string.Format( - translationService.GetResource("ShoppingCart.CustomerEnteredPrice.RangeError"), + translationService.GetResource("ShoppingCart.CustomerEnteredPrice.RangeError", value.Language.Id), value.Product.MinEnteredPrice, value.Product.MaxEnteredPrice)); } @@ -70,7 +72,7 @@ public ShoppingCartStandardValidator(ITranslationService translationService, IAc DateTime.SpecifyKind(value.Product.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc); if (availableStartDateTime.CompareTo(now) > 0) { - context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable")); + context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable", value.Language.Id)); availableStartDateError = true; } } @@ -82,7 +84,7 @@ public ShoppingCartStandardValidator(ITranslationService translationService, IAc var availableEndDateTime = DateTime.SpecifyKind(value.Product.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc); if (availableEndDateTime.CompareTo(now) < 0) - context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable")); + context.AddFailure(translationService.GetResource("ShoppingCart.NotAvailable", value.Language.Id)); } }); } diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs index ae3966a703..18959add4f 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs @@ -4,13 +4,15 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; public record ShoppingCartTotalAmountValidatorRecord( IList Cart, - CalculationContext Context); + CalculationContext Context, + Language Language); public class ShoppingCartTotalAmountValidator : AbstractValidator { @@ -40,9 +42,9 @@ public ShoppingCartTotalAmountValidator( if (shoppingCartTotalBase.HasValue && (shoppingCartTotalBase.Value < minOrderAmount || shoppingCartTotalBase.Value > maxOrderAmount)) - context.AddFailure(translationService.GetResource("Checkout.MinMaxOrderTotalAmount")); + context.AddFailure(translationService.GetResource("Checkout.MinMaxOrderTotalAmount", value.Language.Id)); else if (shoppingCartTotalBase.HasValue && shoppingCartTotalBase.Value < orderSettings.MinOrderTotalAmount) - context.AddFailure(translationService.GetResource("Checkout.MinMaxOrderTotalAmount")); + context.AddFailure(translationService.GetResource("Checkout.MinMaxOrderTotalAmount", value.Language.Id)); //subtotal var (_, _, subTotalWithoutDiscount, _, _) = @@ -52,7 +54,7 @@ public ShoppingCartTotalAmountValidator( var minOrderSubtotalAmount = await currencyService.ConvertFromPrimaryStoreCurrency(orderSettings.MinOrderSubtotalAmount, value.Context.Currency); - context.AddFailure(string.Format(translationService.GetResource("Checkout.MinOrderSubtotalAmount"), + context.AddFailure(string.Format(translationService.GetResource("Checkout.MinOrderSubtotalAmount", value.Language.Id), priceFormatter.FormatPrice(minOrderSubtotalAmount, value.Context.Currency))); } }); diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartWarningsValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartWarningsValidator.cs index 78e747b0d8..738f8f716d 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartWarningsValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartWarningsValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; @@ -11,7 +12,8 @@ namespace Grand.Business.Checkout.Validators; public record ShoppingCartWarningsValidatorRecord( Customer Customer, Store Store, - IList ShoppingCarts); + IList ShoppingCarts, + Language Language); public class ShoppingCartWarningsValidator : AbstractValidator { @@ -31,7 +33,7 @@ public ShoppingCartWarningsValidator(ITranslationService translationService, IPr var product = await productService.GetProductById(sci.ProductId); if (product == null) { - context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.CannotLoadProduct"), + context.AddFailure(string.Format(translationService.GetResource("ShoppingCart.CannotLoadProduct", value.Language.Id), sci.ProductId)); return; } @@ -56,11 +58,11 @@ public ShoppingCartWarningsValidator(ITranslationService translationService, IPr //don't mix standard and recurring products if (hasStandardProducts && hasRecurringProducts) - context.AddFailure(translationService.GetResource("ShoppingCart.CannotMixStandardAndAutoshipProducts")); + context.AddFailure(translationService.GetResource("ShoppingCart.CannotMixStandardAndAutoshipProducts", value.Language.Id)); //don't mix recurring products if (hasRecurringProducts && hasRecurringProductsMix) - context.AddFailure(translationService.GetResource("ShoppingCart.CannotMixRecurringProducts")); + context.AddFailure(translationService.GetResource("ShoppingCart.CannotMixRecurringProducts", value.Language.Id)); }); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/AddRequiredProductsCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/AddRequiredProductsCommand.cs index 763c3747e5..c630f70c17 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/AddRequiredProductsCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/AddRequiredProductsCommand.cs @@ -1,5 +1,6 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Mediator; @@ -11,4 +12,9 @@ public class AddRequiredProductsCommand : IRequest public ShoppingCartType ShoppingCartType { get; set; } public Product Product { get; set; } public string StoreId { get; set; } + + /// + /// Language the cart warnings are written in + /// + public Language Language { get; set; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ReOrderCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ReOrderCommand.cs index 87655bb253..e61701ebbc 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ReOrderCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ReOrderCommand.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Orders; +using Grand.Domain.Localization; +using Grand.Domain.Orders; using Grand.Mediator; namespace Grand.Business.Core.Commands.Checkout.Orders; @@ -6,4 +7,9 @@ namespace Grand.Business.Core.Commands.Checkout.Orders; public class ReOrderCommand : IRequest> { public Order Order { get; set; } + + /// + /// Language the returned warnings are written in + /// + public Language Language { get; set; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs b/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs index 7b05608c7d..6984219a94 100644 --- a/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs +++ b/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs @@ -1,4 +1,5 @@ using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Mediator; namespace Grand.Business.Core.Events.Customers; @@ -8,11 +9,12 @@ namespace Grand.Business.Core.Events.Customers; /// public class CustomerLoggedInEvent : INotification { - public CustomerLoggedInEvent(Customer customer, Customer previousCustomer, string storeId) + public CustomerLoggedInEvent(Customer customer, Customer previousCustomer, string storeId, Language language) { Customer = customer; PreviousCustomer = previousCustomer; StoreId = storeId; + Language = language; } /// @@ -32,4 +34,9 @@ public Customer Customer { /// Store of the request the customer logged in through /// public string StoreId { get; } + + /// + /// Working language of the request the customer logged in through + /// + public Language Language { get; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Events/Customers/CustomerRegisteredEvent.cs b/src/Business/Grand.Business.Core/Events/Customers/CustomerRegisteredEvent.cs index 5815ae0e06..9f98eae665 100644 --- a/src/Business/Grand.Business.Core/Events/Customers/CustomerRegisteredEvent.cs +++ b/src/Business/Grand.Business.Core/Events/Customers/CustomerRegisteredEvent.cs @@ -1,4 +1,5 @@ using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Mediator; namespace Grand.Business.Core.Events.Customers; @@ -8,9 +9,10 @@ namespace Grand.Business.Core.Events.Customers; /// public class CustomerRegisteredEvent : INotification { - public CustomerRegisteredEvent(Customer customer) + public CustomerRegisteredEvent(Customer customer, Language language) { Customer = customer; + Language = language; } /// @@ -20,4 +22,10 @@ public Customer Customer { get; private set; } + + /// + /// Working language of the request the customer registered through; null when the account was registered + /// without one (from an access token) + /// + public Language Language { get; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs b/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs index d9037a6936..4d76f2e639 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Utilities.Authentication; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Stores; using Microsoft.AspNetCore.Mvc; @@ -18,9 +19,10 @@ public interface IExternalAuthenticationService /// External authentication parameters /// Customer of the request (a guest when nobody is logged in) /// Store of the request + /// Working language of the request /// Defines the URL of user return, when successfully authenticated /// Result of an authentication - Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, + Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, Language language, string returnUrl = null); #endregion diff --git a/src/Business/Grand.Business.Core/Interfaces/Authentication/IOidcCustomerResolver.cs b/src/Business/Grand.Business.Core/Interfaces/Authentication/IOidcCustomerResolver.cs index 38f70af1d8..51a802b891 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Authentication/IOidcCustomerResolver.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Authentication/IOidcCustomerResolver.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Utilities.Authentication; using Grand.Domain.Customers; +using Grand.Domain.Localization; using System.Security.Claims; namespace Grand.Business.Core.Interfaces.Authentication; @@ -23,7 +24,9 @@ public interface IOidcCustomerResolver /// E-mail of the provider identity /// The provider verified the e-mail /// Current store - Task ResolveForLogin(string sub, string email, bool emailVerified, string storeId); + /// Working language of the sign-in request + Task ResolveForLogin(string sub, string email, bool emailVerified, string storeId, + Language language); /// /// Stores the link between the provider subject and the customer (one subject per customer, one customer per subject; diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs index cd67b2b767..1840710129 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.Orders; @@ -48,6 +49,7 @@ Task FindShoppingCartItem(IList shoppingCart /// Product id /// ShoppingCartType /// Store id + /// Language the warnings are written in /// Warehouse id /// Attributes /// EnteredPrice @@ -61,7 +63,7 @@ Task FindShoppingCartItem(IList shoppingCart /// ShoppingCartValidatorOptions /// (warnings, shoppingCartItem) Task<(IList warnings, ShoppingCartItem shoppingCartItem)> AddToCart(Customer customer, string productId, - ShoppingCartType shoppingCartType, string storeId, string warehouseId = null, + ShoppingCartType shoppingCartType, string storeId, Language language, string warehouseId = null, IList attributes = null, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, @@ -77,6 +79,7 @@ Task FindShoppingCartItem(IList shoppingCart /// Shopping cart item identifier /// Warehouse ident /// Attributes + /// Language the warnings are written in /// New customer entered price /// Rental start date /// Rental end date @@ -86,7 +89,7 @@ Task FindShoppingCartItem(IList shoppingCart /// /// Warnings Task> UpdateShoppingCartItem(Customer customer, - string shoppingCartItemId, string warehouseId, IList attributes, + string shoppingCartItemId, string warehouseId, IList attributes, Language language, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, int quantity = 1, bool resetCheckoutData = true, string reservationId = "", string sciId = ""); @@ -110,9 +113,11 @@ Task DeleteShoppingCartItem(Customer customer, ShoppingCartItem shoppingCartItem /// From customer /// To customer /// Store whose selected checkout attributes move along + /// Language the items are validated in /// /// A value indicating whether to coupon codes (discount and gift voucher) should be also /// re-applied /// - Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, bool includeCouponCodes); + Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, Language language, + bool includeCouponCodes); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs index c80fb60fa5..8e6b3df2aa 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs @@ -2,6 +2,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.Orders; @@ -14,8 +15,10 @@ public interface IShoppingCartValidator /// Customer /// Product /// Shopping cart item + /// Language the warnings are written in /// Warnings - Task> GetStandardWarnings(Customer customer, Product product, ShoppingCartItem shoppingCartItem); + Task> GetStandardWarnings(Customer customer, Product product, ShoppingCartItem shoppingCartItem, + Language language); /// /// Validates shopping cart item attributes @@ -23,10 +26,11 @@ public interface IShoppingCartValidator /// Customer /// Product /// Shopping cart item + /// Language the warnings are written in /// A value indicating whether we should ignore non-combinable attributes /// Warnings Task> GetShoppingCartItemAttributeWarnings(Customer customer, - Product product, ShoppingCartItem shoppingCartItem, + Product product, ShoppingCartItem shoppingCartItem, Language language, bool ignoreNonCombinableAttributes = false); /// @@ -35,9 +39,10 @@ Task> GetShoppingCartItemAttributeWarnings(Customer customer, /// Customer /// Product /// Shopping cart item + /// Language the warnings are written in /// Warnings Task> GetShoppingCartItemGiftVoucherWarnings(Customer customer, - Product product, ShoppingCartItem shoppingCartItem); + Product product, ShoppingCartItem shoppingCartItem, Language language); /// /// Validate bid @@ -45,8 +50,9 @@ Task> GetShoppingCartItemGiftVoucherWarnings(Customer customer, /// /// /// + /// Language the warnings are written in /// - Task> GetAuctionProductWarning(double bid, Product product, Customer customer); + Task> GetAuctionProductWarning(double bid, Product product, Customer customer, Language language); /// /// Validates shopping cart item @@ -55,9 +61,10 @@ Task> GetShoppingCartItemGiftVoucherWarnings(Customer customer, /// Shopping cart type /// Product /// Options + /// Language the warnings are written in /// Warnings Task> GetShoppingCartItemWarnings(Customer customer, ShoppingCartItem shoppingCartItem, - Product product, ShoppingCartValidatorOptions options); + Product product, ShoppingCartValidatorOptions options, Language language); /// /// Validates whether this shopping cart is valid @@ -65,12 +72,13 @@ Task> GetShoppingCartItemWarnings(Customer customer, ShoppingCartI /// Shopping cart /// Checkout attributes /// Customer, store, currency and tax display the cart is validated for + /// Language the warnings are written in /// A value indicating whether to validate checkout attributes /// A value indicating whether to validate subtotal/total attributes /// Warnings Task> GetShoppingCartWarnings(IList shoppingCart, - IList checkoutAttributes, CalculationContext context, bool validateCheckoutAttributes, - bool validateAmount); + IList checkoutAttributes, CalculationContext context, Language language, + bool validateCheckoutAttributes, bool validateAmount); /// /// Validates shopping cart item for reservation products @@ -78,9 +86,10 @@ Task> GetShoppingCartWarnings(IList shoppingCart /// Customer /// product /// ShoppingCartItem + /// Language the warnings are written in /// Warnings Task> GetReservationProductWarnings(Customer customer, Product product, - ShoppingCartItem shoppingCartItem); + ShoppingCartItem shoppingCartItem, Language language); /// /// Validates shopping cart item for inventory @@ -88,9 +97,10 @@ Task> GetReservationProductWarnings(Customer customer, Product pro /// Customer /// product /// ShoppingCartItem + /// Language the warnings are written in /// Warnings Task> GetInventoryProductWarnings(Customer customer, Product product, - ShoppingCartItem shoppingCartItem); + ShoppingCartItem shoppingCartItem, Language language); /// /// Validates common problems @@ -103,10 +113,11 @@ Task> GetInventoryProductWarnings(Customer customer, Product produ /// rental end date /// quantity /// reservation Id + /// Language the warnings are written in /// Warnings Task> CheckCommonWarnings(Customer customer, IList currentCart, Product product, ShoppingCartType shoppingCartType, DateTime? rentalStartDate, DateTime? rentalEndDate, int quantity, - string reservationId); + string reservationId, Language language); /// /// Validates required products (products which require some other products to be added to the cart) @@ -115,8 +126,9 @@ Task> CheckCommonWarnings(Customer customer, IListShopping cart type /// Product /// Store identifier + /// Language the warnings are written in /// Warnings Task> GetRequiredProductWarnings(Customer customer, ShoppingCartItem shoppingCartItem, Product product, - string storeId); + string storeId, Language language); } \ No newline at end of file diff --git a/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs b/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs index 82faf4f22c..1d9d479700 100644 --- a/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs +++ b/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Interfaces.System.ScheduleTasks; @@ -17,6 +18,7 @@ public class EndAuctionsTask : IScheduleTask { private readonly IAuctionService _auctionService; private readonly ICustomerService _customerService; + private readonly ILanguageService _languageService; private readonly LanguageSettings _languageSettings; private readonly ILogger _logger; private readonly IMessageProviderService _messageProviderService; @@ -25,8 +27,9 @@ public class EndAuctionsTask : IScheduleTask public EndAuctionsTask(IAuctionService auctionService, IMessageProviderService messageProviderService, LanguageSettings translationService, IShoppingCartService shoppingCartService, - ICustomerService customerService, ILogger logger) + ICustomerService customerService, ILanguageService languageService, ILogger logger) { + _languageService = languageService; _auctionService = auctionService; _messageProviderService = messageProviderService; _languageSettings = translationService; @@ -41,6 +44,11 @@ public EndAuctionsTask(IAuctionService auctionService, public async Task Execute() { var auctionsToEnd = await _auctionService.GetAuctionsToEnd(); + if (!auctionsToEnd.Any()) return; + + //cart warnings only reach the log, so they are written in the admin language + var language = await _languageService.GetLanguageById(_languageSettings.DefaultAdminLanguageId) ?? + (await _languageService.GetAllLanguages()).FirstOrDefault(); foreach (var auctionToEnd in auctionsToEnd) { var bid = (await _auctionService.GetBidsByProductId(auctionToEnd.Id)).MaxBy(x => x.Amount); @@ -54,7 +62,7 @@ await _messageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, var warnings = (await _shoppingCartService.AddToCart(await _customerService.GetCustomerById(bid.CustomerId), bid.ProductId, ShoppingCartType.Auctions, - bid.StoreId, bid.WarehouseId, customerEnteredPrice: bid.Amount, + bid.StoreId, language, bid.WarehouseId, customerEnteredPrice: bid.Amount, validator: new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false })).warnings; if (!warnings.Any()) diff --git a/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs b/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs index 0e72bc1adc..5dec5505b5 100644 --- a/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs +++ b/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs @@ -77,7 +77,8 @@ public async Task FacebookLoginCallback(string returnUrl) //authenticate Grand user return await _externalAuthenticationService.Authenticate(authenticationParameters, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, returnUrl); + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingLanguage, returnUrl); } public IActionResult FacebookSignInFailed(string error_code, string error_message, string state) diff --git a/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs b/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs index 368c4acdff..686bec8886 100644 --- a/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs +++ b/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs @@ -76,7 +76,8 @@ public async Task GoogleLoginCallback(string returnUrl) //authenticate grand user return await _externalAuthenticationService.Authenticate(authenticationParameters, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, returnUrl); + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingLanguage, returnUrl); } public IActionResult GoogleSignInFailed(string error_message) diff --git a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs index 216949c58d..d37e88bc30 100644 --- a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs +++ b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs @@ -106,7 +106,7 @@ public async Task Callback(string returnUrl) returnUrl = LocalOrNull(returnUrl); var result = await _oidcCustomerResolver.ResolveForLogin(pending.Subject, pending.Email, pending.EmailVerified, - _contextAccessor.StoreContext.CurrentStore.Id); + _contextAccessor.StoreContext.CurrentStore.Id, _contextAccessor.WorkContext.WorkingLanguage); //a store session is never replaced by another account, and a signed-in visitor does not register; the same //account signing in again (for example right after "Connect your account") gets a session-backed cookie @@ -145,7 +145,8 @@ public async Task Callback(string returnUrl) //the identity provider is the sign-in: its multi-factor policy applies, not the store's two-factor code await _mediator.Publish(new CustomerLoggedInEvent(result.Customer, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + _contextAccessor.WorkContext.WorkingLanguage)); await _authenticationService.SignIn(result.Customer, false, sessionId); return RedirectLocal(returnUrl); } diff --git a/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs b/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs index 065e400a0f..3a9e786213 100644 --- a/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs +++ b/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs @@ -8,6 +8,7 @@ using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Stores; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -123,18 +124,20 @@ public void AuthenticationProviderIsAvailable_NullCustomerOrStore_Throws() } [TestMethod] - public async Task Authenticate_AssociatedUserFromGuest_PublishesLoggedInEventWithPassedCustomerAndStore() + public async Task Authenticate_AssociatedUserFromGuest_PublishesLoggedInEventWithPassedCustomerStoreAndLanguage() { var user = new Customer { Id = "user" }; var guest = new Customer { Id = "guest" }; + var language = new Language { Id = "lang1" }; _customerServiceMock.Setup(c => c.GetCustomerById("user")).ReturnsAsync(user); var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "1" }; await _externalAuthenticationService.AssociateCustomer(user, parameters); - await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }); + await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }, language); _mediatorMock.Verify(m => m.Publish(It.Is(e => - e.Customer == user && e.PreviousCustomer == guest && e.StoreId == "store1"), default), Times.Once); + e.Customer == user && e.PreviousCustomer == guest && e.StoreId == "store1" && e.Language == language), + default), Times.Once); _authenticationServiceMock.Verify(a => a.SignIn(user, false), Times.Once); } @@ -142,15 +145,17 @@ public async Task Authenticate_AssociatedUserFromGuest_PublishesLoggedInEventWit public async Task Authenticate_UnknownIdentity_RegistersPassedCustomerInPassedStore() { var guest = new Customer { Id = "guest" }; + var language = new Language { Id = "lang1" }; var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "new", Email = "new@test.com" }; - await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }); + await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }, language); _customerManagerServiceMock.Verify(m => m.RegisterCustomer(It.Is(r => r.Customer == guest && r.StoreId == "store1" && r.Email == "new@test.com")), Times.Once); - _mediatorMock.Verify(m => m.Publish(It.Is(e => e.Customer == guest), default), + _mediatorMock.Verify(m => m.Publish(It.Is(e => + e.Customer == guest && e.Language == language), default), Times.Once); _authenticationServiceMock.Verify(a => a.SignIn(guest, false), Times.Once); Assert.AreEqual("guest", _externalAuthenticationRecordRepository.Table.Single().CustomerId); diff --git a/src/Tests/Grand.Business.Authentication.Tests/Services/OidcCustomerResolverLoginTests.cs b/src/Tests/Grand.Business.Authentication.Tests/Services/OidcCustomerResolverLoginTests.cs index 8b5e5529bd..f9f0c73dd1 100644 --- a/src/Tests/Grand.Business.Authentication.Tests/Services/OidcCustomerResolverLoginTests.cs +++ b/src/Tests/Grand.Business.Authentication.Tests/Services/OidcCustomerResolverLoginTests.cs @@ -11,6 +11,7 @@ using Grand.Data.Tests.MongoDb; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Infrastructure.Configuration; using Grand.Mediator; @@ -77,10 +78,11 @@ private List LinksOf(string sub) => private const string Store = "store1"; + private static readonly Language Language = new() { Id = "lang1" }; [TestMethod] public async Task NoSubject_Refused_InvalidIdentity() => - Assert.AreEqual(OidcLoginRefusal.InvalidIdentity, (await Resolver().ResolveForLogin(null, "a@b.c", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.InvalidIdentity, (await Resolver().ResolveForLogin(null, "a@b.c", true, Store, Language)).Refusal); [TestMethod] public async Task ProviderChanged_Linked_RefusedNothingLoaded() @@ -89,7 +91,7 @@ public async Task ProviderChanged_Linked_RefusedNothingLoaded() await Link("c1"); Assert.AreEqual(OidcLoginRefusal.ProviderChanged, - (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Refusal); + (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Refusal); _customerService.Verify(x => x.GetCustomerById(It.IsAny()), Times.Never); } @@ -109,7 +111,7 @@ public async Task Linked_SignIn_WithoutEmail() var customer = new Customer { Id = "c1", Active = true }; _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(customer); await Link("c1"); - var result = await Resolver().ResolveForLogin("sub-1", null, false, Store); + var result = await Resolver().ResolveForLogin("sub-1", null, false, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); Assert.AreSame(customer, result.Customer); } @@ -119,19 +121,19 @@ public async Task LinkedInactive_Refused_AccountInactive() { _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(new Customer { Id = "c1", Active = false }); await Link("c1"); - Assert.AreEqual(OidcLoginRefusal.AccountInactive, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.AccountInactive, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Refusal); } [TestMethod] public async Task NoEmail_Refused_InvalidIdentity() => - Assert.AreEqual(OidcLoginRefusal.InvalidIdentity, (await Resolver().ResolveForLogin("sub-1", " ", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.InvalidIdentity, (await Resolver().ResolveForLogin("sub-1", " ", true, Store, Language)).Refusal); [TestMethod] public async Task ExistingCustomer_Verified_LinkedAndSignIn() { var customer = new Customer { Id = "c1", Active = true, Email = "john@example.com" }; _customerService.Setup(x => x.GetCustomerByEmail("John@Example.com", "")).ReturnsAsync(customer); - var result = await Resolver().ResolveForLogin("sub-1", "John@Example.com", true, Store); + var result = await Resolver().ResolveForLogin("sub-1", "John@Example.com", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); var link = await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1"); Assert.AreEqual("c1", link.CustomerId); @@ -144,7 +146,7 @@ public async Task ExistingCustomer_Unverified_LinkRequired_NotLinked() { var customer = new Customer { Id = "c1", Active = true }; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(customer); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store, Language); Assert.AreEqual(OidcLoginOutcome.LinkRequired, result.Outcome); Assert.AreSame(customer, result.Customer); Assert.IsNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); @@ -156,7 +158,7 @@ public async Task ExistingAdmin_Verified_LinkRequired_NotLinked() var customer = new Customer { Id = "c1", Active = true }; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(customer); _permissionService.Setup(x => x.Authorize(StandardPermission.ManageAccessAdminPanel, customer)).ReturnsAsync(true); - Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); Assert.IsNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); } @@ -167,7 +169,7 @@ public async Task ExistingVendorOrStoreStaff_Verified_LinkRequired(string vendor { var customer = new Customer { Id = "c1", Active = true, VendorId = vendorId, StaffStoreId = staffStoreId }; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(customer); - Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); Assert.IsNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); } @@ -184,7 +186,7 @@ public async Task ExistingTwoFactorCustomer_Verified_LinkRequired_NotLinked() _customerSettings.TwoFactorAuthenticationEnabled = true; var customer = TwoFactorCustomer(); _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(customer); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.LinkRequired, result.Outcome); Assert.AreSame(customer, result.Customer); Assert.IsNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); @@ -195,7 +197,7 @@ public async Task ExistingTwoFactorCustomer_StoreTwoFactorOff_Verified_SignIn() { _customerSettings.TwoFactorAuthenticationEnabled = false; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(TwoFactorCustomer()); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); Assert.IsNotNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); } @@ -206,7 +208,7 @@ public async Task LinkedTwoFactorCustomer_SignIn() var customer = TwoFactorCustomer(); _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(customer); await Link("c1"); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -214,7 +216,7 @@ public async Task LinkMadeByEmail_PlainCustomer_Verified_SignIn() { _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(new Customer { Id = "c1", Active = true }); await Link("c1", byEmail: true); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -224,7 +226,7 @@ public async Task LinkMadeByEmail_Unverified_LinkRequired() var customer = new Customer { Id = "c1", Active = true }; _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(customer); await Link("c1", byEmail: true); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store, Language); Assert.AreEqual(OidcLoginOutcome.LinkRequired, result.Outcome); Assert.AreSame(customer, result.Customer); } @@ -235,7 +237,7 @@ public async Task LinkMadeByEmail_TwoFactorCustomer_LinkRequired() _customerSettings.TwoFactorAuthenticationEnabled = true; _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(TwoFactorCustomer()); await Link("c1", byEmail: true); - Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -250,7 +252,7 @@ public async Task LinkMadeByEmail_BackOfficeAccount_LinkRequired(string vendorId if (vendorId == null && staffStoreId == null) _permissionService.Setup(x => x.Authorize(StandardPermission.ManageAccessAdminPanel, customer)).ReturnsAsync(true); await Link("c1", byEmail: true); - Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -260,7 +262,7 @@ public async Task LinkedAdmin_SignIn() _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(customer); _permissionService.Setup(x => x.Authorize(StandardPermission.ManageAccessAdminPanel, customer)).ReturnsAsync(true); await Link("c1"); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -269,7 +271,7 @@ public async Task ProviderOnly_Staff_LinkedByVerifiedEmail() _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; var staff = new Customer { Id = "c1", Active = true, StaffStoreId = "s1" }; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(staff); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); Assert.AreEqual("c1", result.Customer.Id); Assert.IsTrue((await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")).LinkedByEmail); @@ -281,7 +283,7 @@ public async Task ProviderOnly_TwoFactor_LinkedByVerifiedEmail() _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; _customerSettings.TwoFactorAuthenticationEnabled = true; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(TwoFactorCustomer()); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); Assert.IsNotNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); } @@ -292,7 +294,7 @@ public async Task ProviderOnly_UnverifiedEmail_ExistingAccount_Refused() _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())) .ReturnsAsync(new Customer { Id = "c1", Active = true }); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store, Language); Assert.AreEqual(OidcLoginRefusal.EmailNotVerified, result.Refusal); Assert.IsNull(await _links.GetOneAsync(x => x.ExternalIdentifier == "sub-1")); } @@ -301,7 +303,7 @@ public async Task ProviderOnly_UnverifiedEmail_ExistingAccount_Refused() public async Task ProviderOnly_UnverifiedEmail_Unknown_RefusedNotRegistered() { _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; - var result = await Resolver().ResolveForLogin("sub-new", "new@shop.com", false, Store); + var result = await Resolver().ResolveForLogin("sub-new", "new@shop.com", false, Store, Language); Assert.AreEqual(OidcLoginOutcome.Refused, result.Outcome); Assert.AreEqual(OidcLoginRefusal.EmailNotVerified, result.Refusal); } @@ -311,7 +313,7 @@ public async Task ProviderOnly_VerifiedEmail_Unknown_RegistrationRequired() { _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; Assert.AreEqual(OidcLoginOutcome.RegistrationRequired, - (await Resolver().ResolveForLogin("sub-new", "new@shop.com", true, Store)).Outcome); + (await Resolver().ResolveForLogin("sub-new", "new@shop.com", true, Store, Language)).Outcome); } [TestMethod] @@ -324,7 +326,7 @@ public async Task ProviderOnly_AutoProvision_Unknown_AccountCreatedLinkedAndSign _loginConfig.AutoProvision = true; _customerSettings.UserRegistrationType = type; - var result = await Resolver().ResolveForLogin("sub-new", "New@Shop.com", true, Store); + var result = await Resolver().ResolveForLogin("sub-new", "New@Shop.com", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); Assert.AreEqual("new1", result.Customer.Id); @@ -334,7 +336,9 @@ public async Task ProviderOnly_AutoProvision_Unknown_AccountCreatedLinkedAndSign var link = LinksOf("sub-new").Single(); Assert.AreEqual("new1", link.CustomerId); Assert.IsFalse(link.LinkedByEmail); - _mediator.Verify(x => x.Publish(It.IsAny(), It.IsAny()), Times.Once); + //the event carries the language of the sign-in request + _mediator.Verify(x => x.Publish(It.Is(e => e.Language == Language), + It.IsAny()), Times.Once); } [TestMethod] @@ -344,7 +348,7 @@ public async Task ProviderOnly_AutoProvision_UnverifiedEmail_RefusedNothingCreat _loginConfig.AutoProvision = true; Assert.AreEqual(OidcLoginRefusal.EmailNotVerified, - (await Resolver().ResolveForLogin("sub-new", "new@shop.com", false, Store)).Refusal); + (await Resolver().ResolveForLogin("sub-new", "new@shop.com", false, Store, Language)).Refusal); _customerService.Verify(x => x.InsertGuestCustomer(It.IsAny()), Times.Never); } @@ -357,7 +361,7 @@ public async Task ProviderOnly_AutoProvision_UserNameTaken_RefusedNothingCreated .ReturnsAsync(new Customer { Id = "other" }); Assert.AreEqual(OidcLoginRefusal.AccountUnusable, - (await Resolver().ResolveForLogin("sub-new", "new@shop.com", true, Store)).Refusal); + (await Resolver().ResolveForLogin("sub-new", "new@shop.com", true, Store, Language)).Refusal); _customerService.Verify(x => x.InsertGuestCustomer(It.IsAny()), Times.Never); } @@ -367,7 +371,7 @@ public async Task Mixed_AutoProvisionIgnored_RegistrationRequired() _loginConfig.AutoProvision = true; Assert.AreEqual(OidcLoginOutcome.RegistrationRequired, - (await Resolver().ResolveForLogin("sub-new", "new@shop.com", true, Store)).Outcome); + (await Resolver().ResolveForLogin("sub-new", "new@shop.com", true, Store, Language)).Outcome); _customerService.Verify(x => x.InsertGuestCustomer(It.IsAny()), Times.Never); } @@ -378,7 +382,7 @@ public async Task ProviderOnly_LinkMadeByEmail_StaffSignsIn() _customerService.Setup(x => x.GetCustomerById("c1")) .ReturnsAsync(new Customer { Id = "c1", Active = true, StaffStoreId = "s1" }); await Link("c1", byEmail: true); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -388,7 +392,7 @@ public async Task ProviderOnly_LinkMadeByEmail_Unverified_SignsIn() _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(new Customer { Id = "c1", Active = true }); await Link("c1", byEmail: true); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store, Language)).Outcome); } [TestMethod] @@ -398,7 +402,7 @@ public async Task ProviderOnly_ExistingAccount_LinkedToOtherSubject_Refused() _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())) .ReturnsAsync(new Customer { Id = "c1", Active = true }); await Link("c1", "other-sub"); - Assert.AreEqual(OidcLoginRefusal.LinkedToOtherIdentity, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.LinkedToOtherIdentity, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Refusal); } [TestMethod] @@ -406,7 +410,7 @@ public async Task Mixed_Staff_LinkRequired() { var staff = new Customer { Id = "c1", Active = true, StaffStoreId = "s1" }; _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())).ReturnsAsync(staff); - Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.LinkRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); } [TestMethod] @@ -415,7 +419,7 @@ public async Task ExistingCustomerLinkedToOtherSubject_Refused() _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())) .ReturnsAsync(new Customer { Id = "c1", Active = true }); await Link("c1", "other-sub"); - Assert.AreEqual(OidcLoginRefusal.LinkedToOtherIdentity, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.LinkedToOtherIdentity, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Refusal); } [TestMethod] @@ -424,14 +428,14 @@ public async Task ExistingGuestOrSystem_Refused_AccountUnusable() _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())) .ReturnsAsync(new Customer { Id = "c1", Active = true }); _groupService.Setup(x => x.IsRegistered(It.IsAny())).ReturnsAsync(false); - Assert.AreEqual(OidcLoginRefusal.AccountUnusable, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.AccountUnusable, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Refusal); } [TestMethod] public async Task PerStore_EmailLookupScopedToStore() { _customerConfig.RegisterCustomersPerStore = true; - await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store); + await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language); _customerService.Verify(x => x.GetCustomerByEmail("a@b.c", Store), Times.Once); } @@ -439,7 +443,7 @@ public async Task PerStore_EmailLookupScopedToStore() public async Task NoCustomer_RegistrationRequired_NothingCreated_EvenWithAutoRegister() { _config.AutoRegister = true; - Assert.AreEqual(OidcLoginOutcome.RegistrationRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store)).Outcome); + Assert.AreEqual(OidcLoginOutcome.RegistrationRequired, (await Resolver().ResolveForLogin("sub-1", "a@b.c", false, Store, Language)).Outcome); _customerService.Verify(x => x.InsertGuestCustomer(It.IsAny()), Times.Never); } @@ -447,7 +451,7 @@ public async Task NoCustomer_RegistrationRequired_NothingCreated_EvenWithAutoReg public async Task NoCustomer_RegistrationDisabled_Refused() { _customerSettings.UserRegistrationType = UserRegistrationType.Disabled; - Assert.AreEqual(OidcLoginRefusal.RegistrationDisabled, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Refusal); + Assert.AreEqual(OidcLoginRefusal.RegistrationDisabled, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Refusal); } [TestMethod] @@ -519,7 +523,7 @@ public async Task ResolveForLogin_PerStore_SubjectLinkedInAnotherStore_SignsInTh var customer = new Customer { Id = "c1", Active = true, StoreId = Store }; _customerService.Setup(x => x.GetCustomerByEmail("a@b.c", Store)).ReturnsAsync(customer); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); Assert.AreEqual("c1", result.Customer.Id); @@ -534,8 +538,8 @@ public async Task ResolveForLogin_PerStore_GlobalCustomerLink_SignsInInEveryStor await Link("c1", storeId: ""); _customerService.Setup(x => x.GetCustomerById("c1")).ReturnsAsync(new Customer { Id = "c1", Active = true, StoreId = "" }); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store)).Outcome); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, "store2")).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language)).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, "store2", Language)).Outcome); } [TestMethod] @@ -577,12 +581,12 @@ public async Task ResolveForLogin_PerStore_LegacyGlobalLinkToOtherStoreCustomer_ _customerService.Setup(x => x.GetCustomerById("c9")).ReturnsAsync(new Customer { Id = "c9", Active = true, StoreId = "store2" }); _customerService.Setup(x => x.GetCustomerByEmail("a@b.c", Store)).ReturnsAsync(new Customer { Id = "c1", Active = true, StoreId = Store }); - var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store); + var result = await Resolver().ResolveForLogin("sub-1", "a@b.c", true, Store, Language); Assert.AreEqual(OidcLoginOutcome.SignIn, result.Outcome); Assert.AreEqual("c1", result.Customer.Id); Assert.AreEqual(Store, LinksOf("sub-1").Single(x => x.CustomerId == "c1").StoreId); - Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, "store2")).Outcome); + Assert.AreEqual(OidcLoginOutcome.SignIn, (await Resolver().ResolveForLogin("sub-1", "a@b.c", true, "store2", Language)).Outcome); } [TestMethod] @@ -647,7 +651,7 @@ public async Task ResolveForLogin_LostRaceToOtherSubjectOfTheCustomer_RefusedLin _customerService.Setup(x => x.GetCustomerByEmail(It.IsAny(), It.IsAny())) .ReturnsAsync(new Customer { Id = "c1", Active = true }); - var result = await Resolver(links).ResolveForLogin("sub-1", "a@b.c", true, Store); + var result = await Resolver(links).ResolveForLogin("sub-1", "a@b.c", true, Store, Language); Assert.AreEqual(OidcLoginRefusal.LinkedToOtherIdentity, result.Refusal); var stored = links.Table.Where(x => x.CustomerId == "c1").ToList(); diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs index 148483d7b4..b709a679ac 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs @@ -5,6 +5,7 @@ using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -42,6 +43,7 @@ public async Task HandleTest() { Id = "1", RequireOtherProducts = true, RequiredProductIds = "2,3", AutoAddRequiredProducts = true }; command.ShoppingCartType = ShoppingCartType.ShoppingCart; command.StoreId = ""; + command.Language = new Language { Id = "lang1" }; _productServiceMock.Setup(a => a.GetProductById("2", false)).Returns(() => Task.FromResult(new Product { Id = "2", Published = true, Price = 10 })); @@ -53,7 +55,8 @@ public async Task HandleTest() //Assert _shoppingCartServiceMock.Verify( - c => c.AddToCart(It.IsAny(), It.IsAny(), ShoppingCartType.ShoppingCart, "", null, null, + c => c.AddToCart(It.IsAny(), It.IsAny(), ShoppingCartType.ShoppingCart, "", + command.Language, null, null, null, null, null, 1, false, "", "", "", It.IsAny()), Times.AtLeastOnce); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs index 12049d0a53..559991c8b2 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs @@ -2,8 +2,11 @@ using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; +using Grand.Domain.Stores; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -13,26 +16,33 @@ namespace Grand.Business.Checkout.Tests.Events.Customers; public class CustomerRegisteredEventHandlerTests { private CustomerRegisteredEventHandler _customerRegisteredEventHandler; + private Mock _languageServiceMock; private Mock _loyaltyPointsServiceMock; private LoyaltyPointsSettings _loyaltyPointsSettings; + private Mock _storeServiceMock; [TestInitialize] public void Init() { var translationServiceMock = new Mock(); - translationServiceMock.Setup(x => x.GetResource(It.IsAny())).Returns("Name"); + translationServiceMock.Setup(x => x.GetResource(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) + .Returns((string name, string languageId, string _, bool _) => $"{name}@{languageId}"); _loyaltyPointsServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _languageServiceMock = new Mock(); _loyaltyPointsSettings = new LoyaltyPointsSettings { Enabled = true, PointsForRegistration = 10 }; _customerRegisteredEventHandler = new CustomerRegisteredEventHandler(translationServiceMock.Object, - _loyaltyPointsServiceMock.Object, _loyaltyPointsSettings); + _loyaltyPointsServiceMock.Object, _loyaltyPointsSettings, _storeServiceMock.Object, + _languageServiceMock.Object); } [TestMethod] public async Task HandleTest_LoyaltyPointsSettings_Enabled() { //Act - await _customerRegisteredEventHandler.Handle(new CustomerRegisteredEvent(new Customer()), - CancellationToken.None); + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer(), new Language { Id = "lang1" }), CancellationToken.None); //Assert _loyaltyPointsServiceMock.Verify( c => c.AddLoyaltyPointsHistory(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -44,11 +54,57 @@ public async Task HandleTest_LoyaltyPointsSettings_Disabled() { _loyaltyPointsSettings.Enabled = false; //Act - await _customerRegisteredEventHandler.Handle(new CustomerRegisteredEvent(new Customer()), - CancellationToken.None); + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer(), new Language { Id = "lang1" }), CancellationToken.None); //Assert _loyaltyPointsServiceMock.Verify( c => c.AddLoyaltyPointsHistory(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + + [TestMethod] + public async Task Handle_WritesTheMessageInTheLanguageOfTheEvent() + { + //Act + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer { StoreId = "store1" }, new Language { Id = "lang1" }), + CancellationToken.None); + //Assert + _loyaltyPointsServiceMock.Verify( + c => c.AddLoyaltyPointsHistory(It.IsAny(), 10, "store1", + "LoyaltyPoints.Message.EarnedForRegistration@lang1", It.IsAny(), It.IsAny()), + Times.Once); + _storeServiceMock.Verify(c => c.GetStoreById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Handle_WithoutLanguage_WritesTheMessageInTheStoreDefaultLanguage() + { + _storeServiceMock.Setup(c => c.GetStoreById("store1")) + .ReturnsAsync(new Store { Id = "store1", DefaultLanguageId = "store-lang" }); + //Act + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer { StoreId = "store1" }, null), CancellationToken.None); + //Assert + _loyaltyPointsServiceMock.Verify( + c => c.AddLoyaltyPointsHistory(It.IsAny(), 10, "store1", + "LoyaltyPoints.Message.EarnedForRegistration@store-lang", It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Handle_WithoutLanguageAndStoreDefault_WritesTheMessageInTheFirstStoreLanguage() + { + _storeServiceMock.Setup(c => c.GetStoreById("store1")).ReturnsAsync(new Store { Id = "store1" }); + _languageServiceMock.Setup(c => c.GetAllLanguages(false, "store1")) + .ReturnsAsync(new List { new() { Id = "first-lang" } }); + //Act + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer { StoreId = "store1" }, null), CancellationToken.None); + //Assert + _loyaltyPointsServiceMock.Verify( + c => c.AddLoyaltyPointsHistory(It.IsAny(), 10, "store1", + "LoyaltyPoints.Message.EarnedForRegistration@first-lang", It.IsAny(), It.IsAny()), + Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs index 483434ff51..cb7d8fd9e5 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -25,27 +26,29 @@ public void Init() public async Task HandleTest() { //Arrange - var notification = new CustomerLoggedInEvent(new Customer(), new Customer(), ""); + var notification = new CustomerLoggedInEvent(new Customer(), new Customer(), "", new Language()); //Act await _customerLoggedInEventHandler.Handle(notification, CancellationToken.None); //Assert _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(It.IsAny(), It.IsAny(), - It.IsAny(), true), + It.IsAny(), It.IsAny(), true), Times.Once); } [TestMethod] - public async Task Handle_MigratesFromThePreviousCustomerInTheEventStore() + public async Task Handle_MigratesFromThePreviousCustomerInTheEventStoreAndLanguage() { //Arrange var guest = new Customer(); var customer = new Customer(); + var language = new Language { Id = "lang-b" }; //Act - await _customerLoggedInEventHandler.Handle(new CustomerLoggedInEvent(customer, guest, "store-b"), + await _customerLoggedInEventHandler.Handle(new CustomerLoggedInEvent(customer, guest, "store-b", language), CancellationToken.None); //Assert - _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(guest, customer, "store-b", true), Times.Once); + _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(guest, customer, "store-b", language, true), + Times.Once); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs index 7dabe6e627..d6568b3068 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs @@ -7,6 +7,7 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Directory; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; using Grand.Domain.Tax; @@ -22,6 +23,7 @@ namespace Grand.Business.Checkout.Tests.Services.Orders; [TestClass] public class ShoppingCartServiceTests { + private static readonly Language Language = new() { Id = "lang1" }; private Mock _customerServiceMock; private Mock _mediatorMock; private Mock _productServiceMock; @@ -100,14 +102,15 @@ public async Task AddToCartTest_Success() Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), It.IsAny>(), It.IsAny(), ShoppingCartType.ShoppingCart, null, null, - It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); //Act - var result = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); + var result = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); //Assert Assert.IsNotNull(result.shoppingCartItem); @@ -124,14 +127,15 @@ public async Task AddToCartTest_Warning() Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), It.IsAny>(), It.IsAny(), ShoppingCartType.ShoppingCart, null, null, - It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) .Returns(() => Task.FromResult((IList)new List { "Error" })); //Act - var result = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); + var result = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); //Assert Assert.HasCount(1, result.warnings); @@ -148,16 +152,17 @@ public async Task UpdateShoppingCartItemTest() Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), It.IsAny>(), It.IsAny(), ShoppingCartType.ShoppingCart, null, null, - It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); - var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); + var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); //Act var result = await _shoppingCartService.UpdateShoppingCartItem(customer, item.shoppingCartItem.Id, "", - new List(), null); + new List(), Language, null); //Assert _customerServiceMock.Verify(x => x.UpdateShoppingCartItem(It.IsAny(), It.IsAny()), Times.Once); @@ -174,13 +179,14 @@ public async Task DeleteShoppingCartItemTest() Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), It.IsAny>(), It.IsAny(), ShoppingCartType.ShoppingCart, null, null, - It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); - var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); + var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); //Act await _shoppingCartService.DeleteShoppingCartItem(customer, item.shoppingCartItem); //Assert @@ -199,19 +205,20 @@ public async Task MigrateShoppingCartTest() Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), It.IsAny>(), It.IsAny(), ShoppingCartType.ShoppingCart, null, null, - It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); - var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); + var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); var customer2 = new Customer(); //Act - await _shoppingCartService.MigrateShoppingCart(customer, customer2, "", false); + await _shoppingCartService.MigrateShoppingCart(customer, customer2, "", Language, false); //Assert Assert.IsNotEmpty(customer2.ShoppingCartItems); Assert.IsEmpty(customer.ShoppingCartItems); @@ -246,22 +253,23 @@ public async Task MigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetC .ReturnsAsync(new Product { Id = "A", Published = true }); _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), null, null, - It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) .ReturnsAsync(new List()); _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); //Act - await _shoppingCartService.MigrateShoppingCart(guest, customer, "", false); + await _shoppingCartService.MigrateShoppingCart(guest, customer, "", Language, false); //Assert //the guest's whole cart moves together, so a required product may still be on its way to the target cart _shoppingCartValidatorMock.Verify(x => x.GetShoppingCartItemWarnings(customer, It.IsAny(), - It.IsAny(), It.Is(o => !o.GetRequiredProductWarnings)), Times.Once); + It.IsAny(), It.Is(o => !o.GetRequiredProductWarnings), It.IsAny()), Times.Once); } [TestMethod] @@ -275,16 +283,16 @@ public async Task MigrateShoppingCart_ChecksRequiredProductsAgainstTheSourceCart var customer = new Customer(); var product = new Product { Id = "A", Published = true, RequireOtherProducts = true, RequiredProductIds = "B" }; _productServiceMock.Setup(a => a.GetProductById("A", false)).ReturnsAsync(product); - _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(guest, item, product, "store")) + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(guest, item, product, "store", Language)) .ReturnsAsync(new List { "Product B is required" }); //Act - await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", false); + await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", Language, false); //Assert Assert.IsEmpty(customer.ShoppingCartItems); _shoppingCartValidatorMock.Verify(x => x.GetShoppingCartItemWarnings(It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -296,7 +304,7 @@ public async Task MigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStore() var toCustomer = new Customer(); //Act - await _shoppingCartService.MigrateShoppingCart(fromCustomer, toCustomer, "store-b", false); + await _shoppingCartService.MigrateShoppingCart(fromCustomer, toCustomer, "store-b", Language, false); //Assert _customerServiceMock.Verify(x => x.UpdateUserField(toCustomer, SystemCustomerFieldNames.CheckoutAttributes, @@ -313,7 +321,7 @@ public async Task MigrateShoppingCart_GuestWithoutCheckoutAttributes_KeepsTheAcc AddCheckoutAttributes(customer, "store", "gift-wrapping", "yes"); //Act - await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", false); + await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", Language, false); //Assert _customerServiceMock.Verify(x => x.UpdateUserField(It.IsAny(), @@ -331,7 +339,7 @@ public async Task MigrateShoppingCart_GuestWithCheckoutAttributes_ReplacesTheAcc AddCheckoutAttributes(customer, "store", "gift-wrapping", "yes"); //Act - await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", false); + await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", Language, false); //Assert _customerServiceMock.Verify(x => x.UpdateUserField(customer, SystemCustomerFieldNames.CheckoutAttributes, @@ -348,7 +356,7 @@ public async Task MigrateShoppingCart_AccountWithAttributesInAnotherStore_TakesT AddCheckoutAttributes(customer, "store-a", "gift-wrapping", "no"); //Act - await _shoppingCartService.MigrateShoppingCart(guest, customer, "store-b", false); + await _shoppingCartService.MigrateShoppingCart(guest, customer, "store-b", Language, false); //Assert _customerServiceMock.Verify(x => x.UpdateUserField(customer, SystemCustomerFieldNames.CheckoutAttributes, diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs index b2503596ba..eb3848b7d0 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs @@ -7,6 +7,7 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Directory; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; using Grand.Domain.Tax; @@ -21,6 +22,7 @@ namespace Grand.Business.Checkout.Tests.Services.Orders; [TestClass] public class ShoppingCartValidatorTests { + private static readonly Language Language = new() { Id = "lang1" }; private readonly List _records = []; private ShoppingCartValidator _validator; private Mock _validatorFactoryMock; @@ -54,17 +56,20 @@ public async Task GetShoppingCartWarnings_ValidatesForTheContextCustomerAndStore var cart = new List(); //Act - await _validator.GetShoppingCartWarnings(cart, new List(), context, true, true); + await _validator.GetShoppingCartWarnings(cart, new List(), context, Language, true, true); //Assert var warnings = _records.OfType().Single(); Assert.AreSame(context.Customer, warnings.Customer); Assert.AreSame(context.Store, warnings.Store); + Assert.AreSame(Language, warnings.Language); var attributes = _records.OfType().Single(); Assert.AreSame(context.Customer, attributes.Customer); Assert.AreSame(context.Store, attributes.Store); + Assert.AreSame(Language, attributes.Language); var amount = _records.OfType().Single(); Assert.AreSame(context, amount.Context); + Assert.AreSame(Language, amount.Language); } [TestMethod] @@ -75,11 +80,12 @@ public async Task GetRequiredProductWarnings_UsesTheGivenCustomerAndStore() var product = new Product { RequireOtherProducts = true }; //Act - await _validator.GetRequiredProductWarnings(customer, new ShoppingCartItem(), product, "store-b"); + await _validator.GetRequiredProductWarnings(customer, new ShoppingCartItem(), product, "store-b", Language); //Assert var record = _records.OfType().Single(); Assert.AreSame(customer, record.Customer); Assert.AreEqual("store-b", record.StoreId); + Assert.AreSame(Language, record.Language); } } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs index 259ffd2ffe..c68f23a03a 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs @@ -9,6 +9,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Permissions; using Grand.Domain.Stores; @@ -20,6 +21,7 @@ namespace Grand.Business.Checkout.Tests.Validators; [TestClass] public class ShoppingCartValidatorsTests { + private static readonly Language Language = new() { Id = "lang1" }; private Mock _aclServiceMock; private Mock _checkoutAttributeParserMock; private Mock _checkoutAttributeServiceMock; @@ -42,7 +44,8 @@ public void Init() _productReservationServiceMock = new Mock(); _permissionServiceMock = new Mock(); _stockQuantityService = new StockQuantityService(); - _translationServiceMock.Setup(x => x.GetResource(It.IsAny())).Returns("Name"); + _translationServiceMock.Setup(x => x.GetResource(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())).Returns("Name"); } [TestMethod] @@ -52,7 +55,7 @@ public void ShoppingCartAuctionValidator_Success() var shoppingCartAuctionValidator = new ShoppingCartAuctionValidator(_translationServiceMock.Object); //Act var result = shoppingCartAuctionValidator.Validate(new ShoppingCartAuctionValidatorRecord(new Customer(), - new Product { AvailableEndDateTimeUtc = DateTime.UtcNow.AddDays(1) }, new ShoppingCartItem(), 10)); + new Product { AvailableEndDateTimeUtc = DateTime.UtcNow.AddDays(1) }, new ShoppingCartItem(), 10, Language)); //Assert Assert.IsTrue(result.IsValid); } @@ -65,7 +68,7 @@ public void ShoppingCartAuctionValidator_Fail() //Act var result = shoppingCartAuctionValidator.Validate(new ShoppingCartAuctionValidatorRecord(new Customer(), new Product { AvailableEndDateTimeUtc = DateTime.UtcNow.AddDays(1), StartPrice = 20 }, - new ShoppingCartItem(), 10)); + new ShoppingCartItem(), 10, Language)); //Assert Assert.IsFalse(result.IsValid); } @@ -85,7 +88,7 @@ public void ShoppingCartGiftVoucherValidator_Success() var result = shoppingCartGiftVoucherValidator.Validate(new ShoppingCartGiftVoucherValidatorRecord( new Customer(), new Product { GiftVoucherTypeId = GiftVoucherType.Virtual - }, new ShoppingCartItem { Attributes = customAttributes })); + }, new ShoppingCartItem { Attributes = customAttributes }, Language)); //Assert Assert.IsTrue(result.IsValid); } @@ -105,7 +108,7 @@ public void ShoppingCartGiftVoucherValidator_Fail_Wrong_SenderEmail() var result = shoppingCartGiftVoucherValidator.Validate(new ShoppingCartGiftVoucherValidatorRecord( new Customer(), new Product { GiftVoucherTypeId = GiftVoucherType.Virtual - }, new ShoppingCartItem { Attributes = customAttributes })); + }, new ShoppingCartItem { Attributes = customAttributes }, Language)); //Assert Assert.IsFalse(result.IsValid); } @@ -141,7 +144,8 @@ public async Task ShoppingCartCheckoutAttributesValidator_Success() new Customer(), new Store(), new List { new() }, - new List { new() { Key = "1", Value = "test" } } + new List { new() { Key = "1", Value = "test" } }, + Language )); //Assert @@ -181,7 +185,8 @@ public async Task ShoppingCartCheckoutAttributesValidator_Fail() new Customer(), new Store(), new List { new() }, - new List { new() { Key = "1", Value = "" } } + new List { new() { Key = "1", Value = "" } }, + Language )); //Assert @@ -202,7 +207,8 @@ public async Task ShoppingCartInventoryProductValidator_Success() Id = "1", StockQuantity = 1, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10, ManageInventoryMethodId = ManageInventoryMethod.ManageStock }, - new ShoppingCartItem { ProductId = "1", Quantity = 1 } + new ShoppingCartItem { ProductId = "1", Quantity = 1 }, + Language )); //Assert @@ -223,7 +229,8 @@ public async Task ShoppingCartInventoryProductValidator_Fail() Id = "1", StockQuantity = 0, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10, ManageInventoryMethodId = ManageInventoryMethod.ManageStock }, - new ShoppingCartItem { ProductId = "1", Quantity = 1 } + new ShoppingCartItem { ProductId = "1", Quantity = 1 }, + Language )); //Assert @@ -262,7 +269,8 @@ public async Task ShoppingCartItemAttributeValidator_Success() new Customer(), product, new ShoppingCartItem { ProductId = "1", Quantity = 1, Attributes = attributes }, - false + false, + Language )); //Assert @@ -301,7 +309,8 @@ public async Task ShoppingCartItemAttributeValidator_Fail() new Customer(), product, new ShoppingCartItem { ProductId = "1", Quantity = 1, Attributes = attributes }, - false + false, + Language )); //Assert @@ -341,7 +350,8 @@ public async Task ShoppingCartRequiredProductValidator_Success() "1", product, new ShoppingCartItem - { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } + { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" }, + Language )); //Assert @@ -379,7 +389,8 @@ public async Task ShoppingCartRequiredProductValidator_Fail() "1", product, new ShoppingCartItem - { ProductId = "2", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } + { ProductId = "2", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" }, + Language )); //Assert @@ -437,7 +448,8 @@ public async Task ShoppingCartReservationProductValidator_Success() StoreId = "1", RentalStartDateUtc = DateTime.UtcNow.AddDays(1).Date, RentalEndDateUtc = DateTime.UtcNow.AddDays(2).Date - } + }, + Language )); //Assert @@ -494,7 +506,8 @@ public async Task ShoppingCartReservationProductValidator_Fail() StoreId = "1", RentalStartDateUtc = DateTime.UtcNow.AddDays(1).Date, RentalEndDateUtc = DateTime.UtcNow.AddDays(2).Date - } + }, + Language )); //Assert @@ -538,7 +551,8 @@ public async Task ShoppingCartReservationProductValidator_ReservationsNotStoredA StoreId = "1", RentalStartDateUtc = DateTime.UtcNow.AddDays(1).Date, RentalEndDateUtc = DateTime.UtcNow.AddDays(3).Date - } + }, + Language )); //Assert @@ -573,7 +587,7 @@ public async Task ShoppingCartCommonWarningsValidator_Success() ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } }, - product, ShoppingCartType.ShoppingCart, null, null, 1, "") + product, ShoppingCartType.ShoppingCart, null, null, 1, "", Language) ); //Assert @@ -608,7 +622,7 @@ public async Task ShoppingCartCommonWarningsValidator_Fail() ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } }, - product, ShoppingCartType.ShoppingCart, null, null, 1, "") + product, ShoppingCartType.ShoppingCart, null, null, 1, "", Language) ); //Assert @@ -634,13 +648,36 @@ public async Task ShoppingCartStandardValidator_Success() customer, product, new ShoppingCartItem - { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } + { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" }, + Language )); //Assert Assert.IsTrue(result.IsValid); } + [TestMethod] + public async Task ShoppingCartStandardValidator_WritesWarningsInTheGivenLanguage() + { + //Arrange + var shoppingCartStandardValidator = + new ShoppingCartStandardValidator(_translationServiceMock.Object, _aclServiceMock.Object); + _aclServiceMock.Setup(x => x.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); + _aclServiceMock.Setup(x => x.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); + _translationServiceMock.Setup(x => x.GetResource("ShoppingCart.ProductUnpublished", "lang-pl", It.IsAny(), + It.IsAny())).Returns("Produkt niedostÄ™pny"); + + //Act + var result = await shoppingCartStandardValidator.ValidateAsync(new ShoppingCartStandardValidatorRecord( + new Customer(), + new Product { Id = "1", Published = false }, + new ShoppingCartItem { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart }, + new Language { Id = "lang-pl" })); + + //Assert + Assert.AreEqual("Produkt niedostÄ™pny", result.Errors.Single().ErrorMessage); + } + [TestMethod] public async Task ShoppingCartStandardValidator_Fail() { @@ -660,7 +697,8 @@ public async Task ShoppingCartStandardValidator_Fail() customer, product, new ShoppingCartItem - { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } + { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" }, + Language )); //Assert @@ -696,7 +734,8 @@ public async Task ShoppingCartWarningsValidator_Success() new Store(), new List { new() { ProductId = "1", Quantity = 1, Attributes = attributes } - } + }, + Language )); //Assert @@ -736,7 +775,8 @@ public async Task ShoppingCartWarningsValidator_Fail() new List { new() { ProductId = "1", Quantity = 1, Attributes = attributes }, new() { ProductId = "2", Quantity = 1, Attributes = attributes } - } + }, + Language )); //Assert diff --git a/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs b/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs index 879f426e88..b70dcc918e 100644 --- a/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs +++ b/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Customers.Events.Handlers; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -29,7 +30,8 @@ public async Task HandleTest() CannotLoginUntilDateUtc = DateTime.Now }; //Act - await _handler.Handle(new CustomerLoggedInEvent(customer, new Customer(), ""), CancellationToken.None); + await _handler.Handle(new CustomerLoggedInEvent(customer, new Customer(), "", new Language()), + CancellationToken.None); //Assert Assert.AreEqual(0, customer.FailedLoginAttempts); diff --git a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs index cdef82939b..214a2b85fe 100644 --- a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs +++ b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs @@ -10,6 +10,7 @@ using Grand.Business.Core.Utilities.Authentication; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Stores; using Grand.Infrastructure; using Grand.Mediator; @@ -30,6 +31,7 @@ namespace Grand.Modules.Tests.Plugins.OpenIdConnect; [TestClass] public class OidcLoginControllerTests { + private static readonly Language Language = new() { Id = "lang1" }; private static readonly ExternalRegistrationIdentity Pending = new(OidcLoginDefaults.ProviderSystemName, "sub-1", "john@example.com", true, "John", "Doe"); @@ -85,6 +87,7 @@ public void Init() _current = new Customer { Id = "c1", Email = "John@Example.com" }; var work = new Mock(); work.Setup(x => x.CurrentCustomer).Returns(() => _current); + work.Setup(x => x.WorkingLanguage).Returns(Language); var store = new Mock(); store.Setup(x => x.CurrentStore).Returns(new Store { Id = "s1" }); var ctx = new Mock(); @@ -165,10 +168,10 @@ public async Task Callback_SignIn_SignsInClearsRedirects() { var customer = new Customer { Id = "c9" }; _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin("sub-1", "john@example.com", true, "s1")).ReturnsAsync(OidcLoginResult.SignIn(customer)); + _resolver.Setup(x => x.ResolveForLogin("sub-1", "john@example.com", true, "s1", Language)).ReturnsAsync(OidcLoginResult.SignIn(customer)); var result = await _controller.Callback("/cart"); _auth.Verify(x => x.SignIn(customer, false, "session-1"), Times.Once); - _mediator.Verify(x => x.Publish(It.Is(e => e.Customer == customer), It.IsAny()), Times.Once); + _mediator.Verify(x => x.Publish(It.Is(e => e.Customer == customer && e.Language == Language), It.IsAny()), Times.Once); _external.Verify(x => x.Clear(), Times.Once); Assert.AreEqual("/cart", ((RedirectResult)result).Url); } @@ -177,7 +180,7 @@ public async Task Callback_SignIn_SignsInClearsRedirects() public async Task Callback_RegistrationRequired_ToRegister_PendingKept() { _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.RegistrationRequired()); Assert.AreEqual("Register", ((RedirectToRouteResult)await _controller.Callback("/")).RouteName); _external.Verify(x => x.Clear(), Times.Never); @@ -187,7 +190,7 @@ public async Task Callback_RegistrationRequired_ToRegister_PendingKept() public async Task Callback_LinkRequired_ToLink() { _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.LinkRequired(new Customer())); Assert.AreEqual("OidcLoginLink", ((RedirectToRouteResult)await _controller.Callback("/")).RouteName); } @@ -196,7 +199,7 @@ public async Task Callback_LinkRequired_ToLink() public async Task Callback_Refused_ClearsAndFailsWithReason() { _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.Refused(OidcLoginRefusal.AccountInactive)); var result = (RedirectToRouteResult)await _controller.Callback("/"); Assert.AreEqual("OidcLoginFailed", result.RouteName); @@ -289,7 +292,7 @@ public async Task Callback_MethodInactive_ClearsAndFails_NothingResolved() _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(false); Assert.AreEqual("OidcLoginFailed", ((RedirectToRouteResult)await _controller.Callback("/")).RouteName); _external.Verify(x => x.Clear(), Times.Once); - _resolver.Verify(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + _resolver.Verify(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -299,7 +302,7 @@ public async Task Callback_OtherProviderPending_ClearsAndFails_NothingResolved() _external.Setup(x => x.GetPending()).ReturnsAsync(Pending with { ProviderSystemName = "Other" }); Assert.AreEqual("OidcLoginFailed", ((RedirectToRouteResult)await _controller.Callback("/")).RouteName); _external.Verify(x => x.Clear(), Times.Once); - _resolver.Verify(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + _resolver.Verify(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -333,7 +336,7 @@ public async Task Callback_SignIn_TwoFactorCustomer_SignsInWithoutStoreCode() var customer = new Customer { Id = "c9", Email = "john@example.com" }; customer.UserFields.Add(new UserField { Key = SystemCustomerFieldNames.TwoFactorEnabled, Value = "True", StoreId = "" }); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); Assert.AreEqual("/cart", ((RedirectResult)await _controller.Callback("/cart")).Url); _auth.Verify(x => x.SignIn(customer, false, "session-1"), Times.Once); @@ -344,7 +347,7 @@ public async Task Callback_SignIn_OtherAccountSignedIn_ClearsAndFails_SessionKep { _groups.Setup(x => x.IsRegistered(_current)).ReturnsAsync(true); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(new Customer { Id = "other" })); var result = (RedirectToRouteResult)await _controller.Callback("/"); Assert.AreEqual("OidcLoginFailed", result.RouteName); @@ -360,7 +363,7 @@ public async Task Callback_SameCustomerSignedIn_ReplacesCookieWithSession() { _groups.Setup(x => x.IsRegistered(_current)).ReturnsAsync(true); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(_current)); var result = (RedirectResult)await _controller.Callback("/account"); @@ -375,7 +378,7 @@ public async Task Callback_RegistrationRequired_SignedIn_ClearsAndFails() { _groups.Setup(x => x.IsRegistered(_current)).ReturnsAsync(true); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.RegistrationRequired()); var result = (RedirectToRouteResult)await _controller.Callback("/"); Assert.AreEqual("OidcLoginFailed", result.RouteName); @@ -388,7 +391,7 @@ public async Task Callback_LinkRequired_SignedIn_StillToLink() { _groups.Setup(x => x.IsRegistered(_current)).ReturnsAsync(true); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.LinkRequired(new Customer { Id = "other" })); Assert.AreEqual("OidcLoginLink", ((RedirectToRouteResult)await _controller.Callback("/")).RouteName); } @@ -462,7 +465,7 @@ public async Task Callback_SignIn_StartsSessionWithTokensOfTheSignIn() { var customer = new Customer { Id = "c2" }; _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin("sub-1", "john@example.com", true, "s1")).ReturnsAsync(OidcLoginResult.SignIn(customer)); + _resolver.Setup(x => x.ResolveForLogin("sub-1", "john@example.com", true, "s1", Language)).ReturnsAsync(OidcLoginResult.SignIn(customer)); await _controller.Callback("/"); @@ -476,7 +479,7 @@ public async Task Callback_SessionNotStarted_FailedPage_NotSignedIn() { var customer = new Customer { Id = "c2" }; _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); _sessions.Setup(x => x.Start(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string)null); @@ -502,7 +505,7 @@ public async Task Callback_Signin_ExternalItemsMissing_StartsSessionWithoutToken var customer = new Customer { Id = "c2" }; _externalItems.Clear(); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); await _controller.Callback("/"); _sessions.Verify(x => x.Start(customer, "sub-1", null, null, "s1"), Times.Once); @@ -513,7 +516,7 @@ public async Task Callback_SignIn_ExternalReturnUrl_GoesHome() { var customer = new Customer { Id = "c9" }; _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); Assert.AreEqual("HomePage", ((RedirectToRouteResult)await _controller.Callback("https://evil.example/")).RouteName); } @@ -594,7 +597,7 @@ public async Task Callback_ProviderOnly_SignIn_SyncsProfile() _loginConfig.SignInMode = OidcSignInMode.ProviderOnly; var customer = new Customer { Id = "c9", Email = "john@example.com" }; _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); await _controller.Callback("/"); _customers.Verify(x => x.UpdateUserField(customer, SystemCustomerFieldNames.FirstName, "John", ""), Times.Once); @@ -610,7 +613,7 @@ public async Task Callback_SyncThrows_StillSignsIn() _customers.Setup(x => x.UpdateUserField(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException()); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); await _controller.Callback("/"); _auth.Verify(x => x.SignIn(customer, false, "session-1"), Times.Once); @@ -621,7 +624,7 @@ public async Task Callback_Mixed_SignIn_NoSync() { var customer = new Customer { Id = "c9", Email = "john@example.com" }; _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _resolver.Setup(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(OidcLoginResult.SignIn(customer)); await _controller.Callback("/"); _customers.Verify(x => x.UpdateUserField(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); diff --git a/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs b/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs index e66c04b438..24e1063208 100644 --- a/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs +++ b/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Utilities.Checkout; @@ -21,6 +22,7 @@ public class EndAuctionsTaskTests { private Mock _auctionMock; private Mock _customerServiceMock; + private Mock _languageServiceMock; private Mock> _loggerMock; private Mock _messageProviderMock; private LanguageSettings _settings; @@ -35,9 +37,12 @@ public void Init() _shoppingCartMock = new Mock(); _customerServiceMock = new Mock(); _loggerMock = new Mock>(); - _settings = new LanguageSettings(); + _settings = new LanguageSettings { DefaultAdminLanguageId = "admin-lang" }; + _languageServiceMock = new Mock(); + _languageServiceMock.Setup(c => c.GetLanguageById("admin-lang")) + .ReturnsAsync(new Language { Id = "admin-lang" }); _task = new EndAuctionsTask(_auctionMock.Object, _messageProviderMock.Object, _settings, - _shoppingCartMock.Object, _customerServiceMock.Object, + _shoppingCartMock.Object, _customerServiceMock.Object, _languageServiceMock.Object, _loggerMock.Object); } @@ -63,7 +68,7 @@ public async Task Execute_HasWarnings_InvokeExpectedMethos() _auctionMock.Setup(c => c.GetBidsByProductId(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => new PagedList { new() }); _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -71,6 +76,12 @@ public async Task Execute_HasWarnings_InvokeExpectedMethos() .ReturnsAsync((new List { "warning" }, null)); await _task.Execute(); + //the warnings only reach the log, so they are written in the admin language + _shoppingCartMock.Verify(c => c.AddToCart(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.Is(l => l.Id == "admin-lang"), + It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); _loggerMock.Verify(c => c.Log( It.IsAny(), It.IsAny(), @@ -86,7 +97,7 @@ public async Task Execute_Valid_InvokeExpectedMethos() _auctionMock.Setup(c => c.GetBidsByProductId(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => new PagedList { new() }); _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index fad33c836c..57e0912557 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -222,7 +222,8 @@ protected async Task SignInAction(Customer customer, bool createP //raise event await _mediator.Publish(new CustomerLoggedInEvent(customer, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + _contextAccessor.WorkContext.WorkingLanguage)); return RedirectToRoute($"{GetCurrentArea()}Index"); } diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index 0d1319c084..209eceef95 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -919,6 +919,7 @@ public virtual async Task> UpdateCart(Customer customer, string sh shoppingCartId, cart.WarehouseId, cart.Attributes, + _contextAccessor.WorkContext.WorkingLanguage, unitprice, cart.RentalStartDateUtc, cart.RentalEndDateUtc, diff --git a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs index ab9b76fbbf..5ca2a698be 100644 --- a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs @@ -1158,10 +1158,11 @@ public virtual async Task> AddProductToOrderDetails(AddProductToOr }; warnings.AddRange( - await _shoppingCartValidator.GetShoppingCartItemAttributeWarnings(customer, product, shoppingCartItem)); + await _shoppingCartValidator.GetShoppingCartItemAttributeWarnings(customer, product, shoppingCartItem, + _contextAccessor.WorkContext.WorkingLanguage)); warnings.AddRange( await _shoppingCartValidator.GetShoppingCartItemGiftVoucherWarnings(customer, product, - shoppingCartItem)); + shoppingCartItem, _contextAccessor.WorkContext.WorkingLanguage)); if (warnings.Count == 0) { //the description is stored on the order, so it is rendered for the order, not for the admin diff --git a/src/Web/Grand.Web/Controllers/AccountController.cs b/src/Web/Grand.Web/Controllers/AccountController.cs index 101fa48163..6314695ff5 100644 --- a/src/Web/Grand.Web/Controllers/AccountController.cs +++ b/src/Web/Grand.Web/Controllers/AccountController.cs @@ -358,7 +358,8 @@ private async Task SignInAction(Customer customer, bool createPer { //raise event await _mediator.Publish(new CustomerLoggedInEvent(customer, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + _contextAccessor.WorkContext.WorkingLanguage)); //sign in new customer await _authenticationService.SignIn(customer, createPersistentCookie); @@ -613,7 +614,8 @@ await _mediator.Send(new CustomerRegisteredCommand { await _authenticationService.SignIn(_contextAccessor.WorkContext.CurrentCustomer, true); //raise event - await _mediator.Publish(new CustomerRegisteredEvent(_contextAccessor.WorkContext.CurrentCustomer)); + await _mediator.Publish(new CustomerRegisteredEvent(_contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.WorkContext.WorkingLanguage)); IActionResult result; switch (registrationType) diff --git a/src/Web/Grand.Web/Controllers/ActionCartController.cs b/src/Web/Grand.Web/Controllers/ActionCartController.cs index 6bb4f3b5ea..e9dfc94a6b 100644 --- a/src/Web/Grand.Web/Controllers/ActionCartController.cs +++ b/src/Web/Grand.Web/Controllers/ActionCartController.cs @@ -201,7 +201,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, Quantity = quantityToValidate }, product, new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false - }); + }, _contextAccessor.WorkContext.WorkingLanguage); if (addToCartWarnings.Any()) //cannot be added to the cart @@ -216,6 +216,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, product.Id, model.ShoppingCartTypeId, _contextAccessor.StoreContext.CurrentStore.Id, + _contextAccessor.WorkContext.WorkingLanguage, warehouseId, quantity: model.Quantity, validator: new ShoppingCartValidatorOptions { @@ -402,7 +403,8 @@ public virtual async Task> AddProductDetails(Pr //add to the cart var (warnings, shoppingCartItem) = await _shoppingCartService.AddToCart(_contextAccessor.WorkContext.CurrentCustomer, - product.Id, model.ShoppingCartTypeId, _contextAccessor.StoreContext.CurrentStore.Id, warehouseId, + product.Id, model.ShoppingCartTypeId, _contextAccessor.StoreContext.CurrentStore.Id, + _contextAccessor.WorkContext.WorkingLanguage, warehouseId, attributes, customerEnteredPriceConverted, rentalStartDate, rentalEndDate, model.EnteredQuantity, true, model.Reservation, parameter, duration, new ShoppingCartValidatorOptions { @@ -642,8 +644,8 @@ await _currencyService.ConvertToPrimaryStoreCurrency(customerEnteredPrice, //update existing item addToCartWarnings.AddRange(await _shoppingCartService.UpdateShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, - cart.Id, warehouseId, attributes, customerEnteredPriceConverted, - rentalStartDate, rentalEndDate, model.EnteredQuantity)); + cart.Id, warehouseId, attributes, _contextAccessor.WorkContext.WorkingLanguage, + customerEnteredPriceConverted, rentalStartDate, rentalEndDate, model.EnteredQuantity)); if (addToCartWarnings.Any()) //cannot be updated the cart/wishlist @@ -756,8 +758,10 @@ public virtual async Task> AddBid(AddBidModel model, Quantity = 1 }; - var warnings = (await _shoppingCartValidator.GetStandardWarnings(customer, product, shoppingCartItem)).ToList(); - warnings.AddRange(await _shoppingCartValidator.GetAuctionProductWarning(bid, product, customer)); + var language = _contextAccessor.WorkContext.WorkingLanguage; + var warnings = (await _shoppingCartValidator.GetStandardWarnings(customer, product, shoppingCartItem, language)) + .ToList(); + warnings.AddRange(await _shoppingCartValidator.GetAuctionProductWarning(bid, product, customer, language)); if (warnings.Any()) { diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 72d3d3cca3..3494ce8927 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -103,7 +103,7 @@ public virtual async Task Index() //validation (cart) var checkoutAttributes = customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); var scWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, - _contextAccessor.CalculationContext(), true, true); + _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, true, true); if (scWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); @@ -113,7 +113,7 @@ public virtual async Task Index() var product = await _productService.GetProductById(sci.ProductId); var sciWarnings = await _shoppingCartValidator.GetShoppingCartItemWarnings(customer, sci, product, - new ShoppingCartValidatorOptions()); + new ShoppingCartValidatorOptions(), _contextAccessor.WorkContext.WorkingLanguage); if (sciWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); } @@ -144,7 +144,7 @@ public virtual async Task Start() .GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, - _contextAccessor.CalculationContext(), true, true); + _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, true, true); if (cartWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); @@ -153,7 +153,7 @@ public virtual async Task Start() { var product = await _productService.GetProductById(sci.ProductId); var sciWarnings = await _shoppingCartValidator.GetShoppingCartItemWarnings(_contextAccessor.WorkContext.CurrentCustomer, - sci, product, new ShoppingCartValidatorOptions()); + sci, product, new ShoppingCartValidatorOptions(), _contextAccessor.WorkContext.WorkingLanguage); if (sciWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); } diff --git a/src/Web/Grand.Web/Controllers/OrderController.cs b/src/Web/Grand.Web/Controllers/OrderController.cs index 9e4cf9c131..22410d0eb5 100644 --- a/src/Web/Grand.Web/Controllers/OrderController.cs +++ b/src/Web/Grand.Web/Controllers/OrderController.cs @@ -164,7 +164,10 @@ public virtual async Task ReOrder(string orderId) if (!await order.Access(_contextAccessor.WorkContext.CurrentCustomer, _groupService)) return Challenge(); - var warnings = await _mediator.Send(new ReOrderCommand { Order = order }); + var warnings = await _mediator.Send(new ReOrderCommand { + Order = order, + Language = _contextAccessor.WorkContext.WorkingLanguage + }); if (warnings.Any()) Notification(NotifyType.Error, string.Join(",", warnings), true); diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index 2a2e609fc4..c4e4af1492 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -323,7 +323,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode if (cart != null) { var currSciWarnings = await _shoppingCartService.UpdateShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, - cart.Id, cart.WarehouseId, cart.Attributes, cart.EnteredPrice, + cart.Id, cart.WarehouseId, cart.Attributes, _contextAccessor.WorkContext.WorkingLanguage, cart.EnteredPrice, cart.RentalStartDateUtc, cart.RentalEndDateUtc, model.Quantity); warnings.AddRange(currSciWarnings); @@ -473,7 +473,7 @@ public virtual async Task StartCheckout(CheckoutAttributeSelected var checkoutAttributeWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, - _contextAccessor.CalculationContext(), true, true); + _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, true, true); if (checkoutAttributeWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); //everything is OK diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 458f171a6f..8fc3ed011d 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -124,7 +124,8 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode { var currSciWarnings = await _shoppingCartService.UpdateShoppingCartItem( _contextAccessor.WorkContext.CurrentCustomer, - cart.Id, cart.WarehouseId, cart.Attributes, cart.EnteredPrice, + cart.Id, cart.WarehouseId, cart.Attributes, _contextAccessor.WorkContext.WorkingLanguage, + cart.EnteredPrice, cart.RentalStartDateUtc, cart.RentalEndDateUtc, model.Quantity); warnings.AddRange(currSciWarnings); @@ -170,7 +171,8 @@ public virtual async Task AddItemToCartFromWishlist(AddCartFromWi var warnings = (await _shoppingCartService.AddToCart(_contextAccessor.WorkContext.CurrentCustomer, itemCart.ProductId, ShoppingCartType.ShoppingCart, - _contextAccessor.StoreContext.CurrentStore.Id, itemCart.WarehouseId, + _contextAccessor.StoreContext.CurrentStore.Id, _contextAccessor.WorkContext.WorkingLanguage, + itemCart.WarehouseId, itemCart.Attributes, itemCart.EnteredPrice, itemCart.RentalStartDateUtc, itemCart.RentalEndDateUtc, itemCart.Quantity, validator: new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false })).warnings; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 3d549b7822..e275700406 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -206,7 +206,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu //cart warnings var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, - request.CalculationContext(), + request.CalculationContext(), request.Language, request.ValidateCheckoutAttributes, true); foreach (var warning in cartWarnings) model.Warnings.Add(warning); @@ -511,7 +511,7 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req //item warnings var itemWarnings = await _shoppingCartValidator.GetShoppingCartItemWarnings(request.Customer, sci, product, - new ShoppingCartValidatorOptions()); + new ShoppingCartValidatorOptions(), request.Language); foreach (var warning in itemWarnings) cartItemModel.Warnings.Add(warning); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index 139b2d6859..2b2c069a6b 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -94,7 +94,7 @@ public async Task Handle(GetWishlist request, CancellationToken c //cart warnings var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, - request.CalculationContext(), false, false); + request.CalculationContext(), request.Language, false, false); foreach (var warning in cartWarnings) model.Warnings.Add(warning); @@ -188,7 +188,7 @@ public async Task Handle(GetWishlist request, CancellationToken c //item warnings var itemWarnings = await _shoppingCartValidator.GetShoppingCartItemWarnings(request.Customer, sci, product, - new ShoppingCartValidatorOptions()); + new ShoppingCartValidatorOptions(), request.Language); foreach (var warning in itemWarnings) cartItemModel.Warnings.Add(warning); From c42472c6c237ffb4b61fb5ab41d4cb825f68d057 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 15:47:26 +0200 Subject: [PATCH 3/6] Fix the encoding of a Polish string in a cart validator test --- .../Validators/ShoppingCartValidatorsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs index c68f23a03a..fdfc497d3b 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs @@ -665,7 +665,7 @@ public async Task ShoppingCartStandardValidator_WritesWarningsInTheGivenLanguage _aclServiceMock.Setup(x => x.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(x => x.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _translationServiceMock.Setup(x => x.GetResource("ShoppingCart.ProductUnpublished", "lang-pl", It.IsAny(), - It.IsAny())).Returns("Produkt niedostÄ™pny"); + It.IsAny())).Returns("Produkt niedostępny"); //Act var result = await shoppingCartStandardValidator.ValidateAsync(new ShoppingCartStandardValidatorRecord( @@ -675,7 +675,7 @@ public async Task ShoppingCartStandardValidator_WritesWarningsInTheGivenLanguage new Language { Id = "lang-pl" })); //Assert - Assert.AreEqual("Produkt niedostÄ™pny", result.Errors.Single().ErrorMessage); + Assert.AreEqual("Produkt niedostępny", result.Errors.Single().ErrorMessage); } [TestMethod] From 4bc3b50e61e32310c048ede556527248603c00a3 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 16:05:31 +0200 Subject: [PATCH 4/6] Fall back to an existing language for loyalty point messages An order without CustomerLanguageId, a deleted order or store language, or a store without a published language made GetResource throw on a null key or write the raw resource key. GetLanguageOrStoreDefault picks the given language while it exists, then the store default, then any store language. --- .../AwardLoyaltyPointsCommandHandler.cs | 21 ++++++++++-- .../ReduceLoyaltyPointsCommandHandler.cs | 21 ++++++++++-- ...BackRedeemedLoyaltyPointsCommandHandler.cs | 23 ++++++++++--- .../CustomerRegisteredEventHandler.cs | 19 +++++------ .../Extensions/LanguageServiceExtensions.cs | 33 ++++++++++++++++++ .../AwardLoyaltyPointsCommandHandlerTests.cs | 9 ++++- .../ReduceLoyaltyPointsCommandHandlerTests.cs | 9 ++++- ...edeemedLoyaltyPointsCommandHandlerTests.cs | 34 ++++++++++++++++++- .../CustomerRegisteredEventHandlerTests.cs | 34 +++++++++++++++++++ 9 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 src/Business/Grand.Business.Core/Extensions/LanguageServiceExtensions.cs diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs index 02d84c74ce..0f30e2ddc7 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandler.cs @@ -1,8 +1,11 @@ using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Orders; using Grand.Mediator; namespace Grand.Business.Checkout.Commands.Handlers.Orders; @@ -14,6 +17,8 @@ public class AwardLoyaltyPointsCommandHandler : IRequestHandler GetMessage(string resourceKey, Order order) + { + var store = await _storeService.GetStoreById(order.StoreId); + var language = await _languageService.GetLanguageOrStoreDefault(order.CustomerLanguageId, store); + return language != null ? _translationService.GetResource(resourceKey, language.Id) : ""; + } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs index 50af1c671b..c0586a4469 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandler.cs @@ -1,8 +1,11 @@ using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Orders; using Grand.Mediator; namespace Grand.Business.Checkout.Commands.Handlers.Orders; @@ -14,6 +17,8 @@ public class ReduceLoyaltyPointsCommandHandler : IRequestHandler GetMessage(string resourceKey, Order order) + { + var store = await _storeService.GetStoreById(order.StoreId); + var language = await _languageService.GetLanguageOrStoreDefault(order.CustomerLanguageId, store); + return language != null ? _translationService.GetResource(resourceKey, language.Id) : ""; + } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs index 0b04eb2954..4d995c33d2 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandler.cs @@ -1,6 +1,9 @@ using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Orders; using Grand.Mediator; namespace Grand.Business.Checkout.Commands.Handlers.Orders; @@ -9,14 +12,20 @@ public class ReturnBackRedeemedLoyaltyPointsCommandHandler : IRequestHandler { private readonly ILoyaltyPointsService _loyaltyPointsService; + private readonly ILanguageService _languageService; + private readonly IStoreService _storeService; private readonly ITranslationService _translationService; public ReturnBackRedeemedLoyaltyPointsCommandHandler( ILoyaltyPointsService loyaltyPointsService, - ITranslationService translationService) + ITranslationService translationService, + ILanguageService languageService, + IStoreService storeService) { _loyaltyPointsService = loyaltyPointsService; _translationService = translationService; + _languageService = languageService; + _storeService = storeService; } public async Task Handle(ReturnBackRedeemedLoyaltyPointsCommand request, CancellationToken cancellationToken) @@ -30,11 +39,17 @@ public async Task Handle(ReturnBackRedeemedLoyaltyPointsCommand request, C //return back await _loyaltyPointsService.AddLoyaltyPointsHistory(request.Order.CustomerId, -request.Order.RedeemedLoyaltyPoints, request.Order.StoreId, - string.Format( - _translationService.GetResource("LoyaltyPoints.Message.ReturnedForOrder", - request.Order.CustomerLanguageId), + string.Format(await GetMessage("LoyaltyPoints.Message.ReturnedForOrder", request.Order), request.Order.OrderNumber)); return true; } + + //the message is written in the language the order was placed in, or in the store's language when that is gone + private async Task GetMessage(string resourceKey, Order order) + { + var store = await _storeService.GetStoreById(order.StoreId); + var language = await _languageService.GetLanguageOrStoreDefault(order.CustomerLanguageId, store); + return language != null ? _translationService.GetResource(resourceKey, language.Id) : ""; + } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs b/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs index dda999ff99..602ba90f6e 100644 --- a/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs +++ b/src/Business/Grand.Business.Checkout/Events/Customers/CustomerRegisteredEventHandler.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Events.Customers; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Stores; @@ -36,19 +37,15 @@ public async Task Handle(CustomerRegisteredEvent notification, CancellationToken if (!_loyaltyPointsSettings.Enabled || _loyaltyPointsSettings.PointsForRegistration <= 0) return; - var languageId = notification.Language?.Id ?? await GetStoreLanguageId(notification.Customer.StoreId); + //the request language; registration without a request (access-token sign-up) writes in the store's language + var language = notification.Language ?? + await _languageService.GetLanguageOrStoreDefault(null, + await _storeService.GetStoreById(notification.Customer.StoreId)); await _loyaltyPointsService.AddLoyaltyPointsHistory(notification.Customer.Id, _loyaltyPointsSettings.PointsForRegistration, notification.Customer.StoreId, - _translationService.GetResource("LoyaltyPoints.Message.EarnedForRegistration", languageId)); - } - - private async Task GetStoreLanguageId(string storeId) - { - var store = await _storeService.GetStoreById(storeId); - if (!string.IsNullOrEmpty(store?.DefaultLanguageId)) - return store.DefaultLanguageId; - - return (await _languageService.GetAllLanguages(storeId: storeId)).FirstOrDefault()?.Id; + language != null + ? _translationService.GetResource("LoyaltyPoints.Message.EarnedForRegistration", language.Id) + : ""); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Extensions/LanguageServiceExtensions.cs b/src/Business/Grand.Business.Core/Extensions/LanguageServiceExtensions.cs new file mode 100644 index 0000000000..d1cb4631f9 --- /dev/null +++ b/src/Business/Grand.Business.Core/Extensions/LanguageServiceExtensions.cs @@ -0,0 +1,33 @@ +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Localization; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Extensions; + +public static class LanguageServiceExtensions +{ + /// + /// Gets the language to write in when there is no request: the given language while it still exists, + /// otherwise the store's default language when the store offers it, otherwise the store's first published + /// language, otherwise any language + /// + /// Language service + /// Preferred language identifier; may be empty or point to a deleted language + /// Store whose languages are the fallback; null for all stores + /// The language, or null when no language exists at all + public static async Task GetLanguageOrStoreDefault(this ILanguageService languageService, + string languageId, Store store) + { + if (!string.IsNullOrEmpty(languageId)) + { + var language = await languageService.GetLanguageById(languageId); + if (language != null) + return language; + } + + var storeLanguages = await languageService.GetAllLanguages(storeId: store?.Id ?? ""); + return storeLanguages.FirstOrDefault(l => l.Id == store?.DefaultLanguageId) + ?? storeLanguages.FirstOrDefault() + ?? (await languageService.GetAllLanguages(true)).FirstOrDefault(); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs index 84fffbc195..cd38ade64c 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AwardLoyaltyPointsCommandHandlerTests.cs @@ -3,9 +3,11 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Customers; using Grand.Domain.Directory; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,6 +24,8 @@ public class AwardLoyaltyPointsCommandHandlerTests private Mock _loyaltyPointsServiceMock; private Mock _mediatorMock; private Mock _orderServiceMock; + private Mock _languageServiceMock; + private Mock _storeServiceMock; private Mock _translationServiceMock; @@ -34,10 +38,13 @@ public void Init() _currencyServiceMock = new Mock(); _orderServiceMock = new Mock(); _translationServiceMock = new Mock(); + _languageServiceMock = new Mock(); + _languageServiceMock.Setup(c => c.GetLanguageById("lang1")).ReturnsAsync(new Language { Id = "lang1" }); + _storeServiceMock = new Mock(); _handler = new AwardLoyaltyPointsCommandHandler(_customerServiceMock.Object, _loyaltyPointsServiceMock.Object, _mediatorMock.Object, _orderServiceMock.Object, _translationServiceMock.Object, - _currencyServiceMock.Object); + _languageServiceMock.Object, _storeServiceMock.Object, _currencyServiceMock.Object); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs index c8e419f6e3..fdc6636b85 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReduceLoyaltyPointsCommandHandlerTests.cs @@ -3,9 +3,11 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Customers; using Grand.Domain.Directory; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,6 +24,8 @@ public class ReduceLoyaltyPointsCommandHandlerTests private Mock _loyaltyPointsServiceMock; private Mock _mediatorMock; private Mock _orderServiceMock; + private Mock _languageServiceMock; + private Mock _storeServiceMock; private Mock _translationServiceMock; @@ -34,10 +38,13 @@ public void Init() _currencyServiceMock = new Mock(); _orderServiceMock = new Mock(); _translationServiceMock = new Mock(); + _languageServiceMock = new Mock(); + _languageServiceMock.Setup(c => c.GetLanguageById("lang1")).ReturnsAsync(new Language { Id = "lang1" }); + _storeServiceMock = new Mock(); _handler = new ReduceLoyaltyPointsCommandHandler(_customerServiceMock.Object, _loyaltyPointsServiceMock.Object, _mediatorMock.Object, _orderServiceMock.Object, _translationServiceMock.Object, - _currencyServiceMock.Object); + _languageServiceMock.Object, _storeServiceMock.Object, _currencyServiceMock.Object); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs index 3c92e6403c..dd178831ae 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ReturnBackRedeemedLoyaltyPointsCommandHandlerTests.cs @@ -2,7 +2,10 @@ using Grand.Business.Core.Commands.Checkout.Orders; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Localization; using Grand.Domain.Orders; +using Grand.Domain.Stores; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -13,6 +16,8 @@ public class ReturnBackRedeemedLoyaltyPointsCommandHandlerTests { private ReturnBackRedeemedLoyaltyPointsCommandHandler _handler; private Mock _loyaltyPointsServiceMock; + private Mock _languageServiceMock; + private Mock _storeServiceMock; private Mock _translationServiceMock; [TestInitialize] @@ -20,8 +25,12 @@ public void Init() { _loyaltyPointsServiceMock = new Mock(); _translationServiceMock = new Mock(); + _languageServiceMock = new Mock(); + _languageServiceMock.Setup(c => c.GetLanguageById("lang1")).ReturnsAsync(new Language { Id = "lang1" }); + _storeServiceMock = new Mock(); _handler = new ReturnBackRedeemedLoyaltyPointsCommandHandler(_loyaltyPointsServiceMock.Object, - _translationServiceMock.Object); + _translationServiceMock.Object, _languageServiceMock.Object, + _storeServiceMock.Object); } [TestMethod] @@ -41,4 +50,27 @@ public async Task HandleTest() c => c.AddLoyaltyPointsHistory(It.IsAny(), It.Is(g => g < 0), It.IsAny(), "Name", It.IsAny(), It.IsAny()), Times.Once); } + + [TestMethod] + public async Task Handle_OrderWithoutLanguage_WritesTheMessageInTheStoreDefaultLanguage() + { + //Arrange + var command = new ReturnBackRedeemedLoyaltyPointsCommand { + Order = new Order { RedeemedLoyaltyPoints = 10, StoreId = "store1", CustomerLanguageId = null } + }; + _storeServiceMock.Setup(c => c.GetStoreById("store1")) + .ReturnsAsync(new Store { Id = "store1", DefaultLanguageId = "store-lang" }); + _languageServiceMock.Setup(c => c.GetAllLanguages(false, "store1")) + .ReturnsAsync(new List { new() { Id = "first-lang" }, new() { Id = "store-lang" } }); + _translationServiceMock.Setup(x => x.GetResource(It.IsAny(), "store-lang", It.IsAny(), + It.IsAny())).Returns("Store"); + //Act + await _handler.Handle(command, CancellationToken.None); + + _loyaltyPointsServiceMock.Verify( + c => c.AddLoyaltyPointsHistory(It.IsAny(), It.Is(g => g < 0), "store1", + "Store", It.IsAny(), It.IsAny()), Times.Once); + _translationServiceMock.Verify(x => x.GetResource(It.IsAny(), null, It.IsAny(), + It.IsAny()), Times.Never); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs index 559991c8b2..3fdbe3fb9a 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Events/Customers/CustomerRegisteredEventHandlerTests.cs @@ -82,6 +82,8 @@ public async Task Handle_WithoutLanguage_WritesTheMessageInTheStoreDefaultLangua { _storeServiceMock.Setup(c => c.GetStoreById("store1")) .ReturnsAsync(new Store { Id = "store1", DefaultLanguageId = "store-lang" }); + _languageServiceMock.Setup(c => c.GetAllLanguages(false, "store1")) + .ReturnsAsync(new List { new() { Id = "first-lang" }, new() { Id = "store-lang" } }); //Act await _customerRegisteredEventHandler.Handle( new CustomerRegisteredEvent(new Customer { StoreId = "store1" }, null), CancellationToken.None); @@ -107,4 +109,36 @@ await _customerRegisteredEventHandler.Handle( "LoyaltyPoints.Message.EarnedForRegistration@first-lang", It.IsAny(), It.IsAny()), Times.Once); } + + [TestMethod] + public async Task Handle_WithoutLanguageAndDeletedStoreDefault_WritesTheMessageInTheFirstStoreLanguage() + { + _storeServiceMock.Setup(c => c.GetStoreById("store1")) + .ReturnsAsync(new Store { Id = "store1", DefaultLanguageId = "deleted-lang" }); + _languageServiceMock.Setup(c => c.GetAllLanguages(false, "store1")) + .ReturnsAsync(new List { new() { Id = "first-lang" } }); + //Act + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer { StoreId = "store1" }, null), CancellationToken.None); + //Assert + _loyaltyPointsServiceMock.Verify( + c => c.AddLoyaltyPointsHistory(It.IsAny(), 10, "store1", + "LoyaltyPoints.Message.EarnedForRegistration@first-lang", It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Handle_WithoutAnyLanguage_AwardsThePointsWithAnEmptyMessage() + { + _storeServiceMock.Setup(c => c.GetStoreById("store1")).ReturnsAsync(new Store { Id = "store1" }); + _languageServiceMock.Setup(c => c.GetAllLanguages(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + //Act + await _customerRegisteredEventHandler.Handle( + new CustomerRegisteredEvent(new Customer { StoreId = "store1" }, null), CancellationToken.None); + //Assert + _loyaltyPointsServiceMock.Verify( + c => c.AddLoyaltyPointsHistory(It.IsAny(), 10, "store1", "", It.IsAny(), + It.IsAny()), Times.Once); + } } \ No newline at end of file From df4f1866f07c6d2753311fe8820abbf763df4368 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 16:05:31 +0200 Subject: [PATCH 5/6] Guard the language of item warnings and pass it to the warning events Plugins that add cart warnings now get the language the core warnings are written in. --- .../Services/Orders/ShoppingCartValidator.cs | 6 ++- .../ShoppingCartItemWarningsEvent.cs | 10 +++- .../ShoppingCart/ShoppingCartWarningsEvent.cs | 9 +++- .../ShoppingCartEventPublisherExtensions.cs | 10 ++-- .../Orders/ShoppingCartValidatorTests.cs | 50 ++++++++++++++++++- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs index b3127762e5..24660dded2 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs @@ -167,7 +167,8 @@ public virtual async Task> GetShoppingCartWarnings(IList> GetShoppingCartItemWarnings(Customer cu Product product, ShoppingCartValidatorOptions options, Language language) { ArgumentNullException.ThrowIfNull(product); + ArgumentNullException.ThrowIfNull(language); var warnings = new List(); @@ -226,7 +228,7 @@ public virtual async Task> GetShoppingCartItemWarnings(Customer cu warnings.AddRange(await GetReservationProductWarnings(customer, product, shoppingCartItem, language)); //event notification - await _mediator.ShoppingCartItemWarningsAdded(warnings, customer, shoppingCartItem, product); + await _mediator.ShoppingCartItemWarningsAdded(warnings, customer, shoppingCartItem, product, language); return warnings; } diff --git a/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartItemWarningsEvent.cs b/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartItemWarningsEvent.cs index a422b61d49..8078d37b48 100644 --- a/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartItemWarningsEvent.cs +++ b/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartItemWarningsEvent.cs @@ -1,5 +1,6 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Mediator; @@ -8,12 +9,14 @@ namespace Grand.Business.Core.Events.Checkout.ShoppingCart; public class ShoppingCartItemWarningsEvent : INotification where C : Customer where S : ShoppingCartItem where P : Product { - public ShoppingCartItemWarningsEvent(IList warnings, C customer, S shoppingCartItem, P product) + public ShoppingCartItemWarningsEvent(IList warnings, C customer, S shoppingCartItem, P product, + Language language) { Warnings = warnings; Customer = customer; ShoppingCartItem = shoppingCartItem; Product = product; + Language = language; } public IList Warnings { get; } @@ -23,4 +26,9 @@ public ShoppingCartItemWarningsEvent(IList warnings, C customer, S shopp public S ShoppingCartItem { get; } public P Product { get; } + + /// + /// Language the warnings are written in + /// + public Language Language { get; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartWarningsEvent.cs b/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartWarningsEvent.cs index 638dbf5b83..0ee0b55e8a 100644 --- a/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartWarningsEvent.cs +++ b/src/Business/Grand.Business.Core/Events/Checkout/ShoppingCart/ShoppingCartWarningsEvent.cs @@ -1,4 +1,5 @@ using Grand.Domain.Common; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Mediator; @@ -7,12 +8,13 @@ namespace Grand.Business.Core.Events.Checkout.ShoppingCart; public class ShoppingCartWarningsEvent : INotification where U : ShoppingCartItem { public ShoppingCartWarningsEvent(IList warnings, IList shoppingCartItems, - IList checkoutAttributes, bool validateCheckoutAttributes) + IList checkoutAttributes, bool validateCheckoutAttributes, Language language) { Warnings = warnings; ShoppingCartItems = shoppingCartItems; CheckoutAttributes = checkoutAttributes; ValidateCheckoutAttributes = validateCheckoutAttributes; + Language = language; } public IList Warnings { get; } @@ -22,4 +24,9 @@ public ShoppingCartWarningsEvent(IList warnings, IList shoppingCartItems, public IList CheckoutAttributes { get; } public bool ValidateCheckoutAttributes { get; } + + /// + /// Language the warnings are written in + /// + public Language Language { get; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Extensions/ShoppingCartEventPublisherExtensions.cs b/src/Business/Grand.Business.Core/Extensions/ShoppingCartEventPublisherExtensions.cs index 7b5625eaa4..e311ef9a85 100644 --- a/src/Business/Grand.Business.Core/Extensions/ShoppingCartEventPublisherExtensions.cs +++ b/src/Business/Grand.Business.Core/Extensions/ShoppingCartEventPublisherExtensions.cs @@ -2,6 +2,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Mediator; @@ -10,18 +11,19 @@ namespace Grand.Business.Core.Extensions; public static class ShoppingCartEventPublisherExtensions { public static async Task ShoppingCartWarningsAdd(this IMediator eventPublisher, IList warnings, - IList shoppingCartItems, IList checkoutAttributes, bool validateCheckoutAttributes) + IList shoppingCartItems, IList checkoutAttributes, bool validateCheckoutAttributes, + Language language) where U : ShoppingCartItem { await eventPublisher.Publish(new ShoppingCartWarningsEvent(warnings, shoppingCartItems, - checkoutAttributes, validateCheckoutAttributes)); + checkoutAttributes, validateCheckoutAttributes, language)); } public static async Task ShoppingCartItemWarningsAdded(this IMediator eventPublisher, - IList warnings, C customer, S shoppingCartItem, P product) + IList warnings, C customer, S shoppingCartItem, P product, Language language) where C : Customer where S : ShoppingCartItem where P : Product { await eventPublisher.Publish( - new ShoppingCartItemWarningsEvent(warnings, customer, shoppingCartItem, product)); + new ShoppingCartItemWarningsEvent(warnings, customer, shoppingCartItem, product, language)); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs index eb3848b7d0..d9f2d0ac75 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs @@ -2,6 +2,7 @@ using FluentValidation.Results; using Grand.Business.Checkout.Services.Orders; using Grand.Business.Checkout.Validators; +using Grand.Business.Core.Events.Checkout.ShoppingCart; using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -24,6 +25,7 @@ public class ShoppingCartValidatorTests { private static readonly Language Language = new() { Id = "lang1" }; private readonly List _records = []; + private Mock _mediatorMock; private ShoppingCartValidator _validator; private Mock _validatorFactoryMock; @@ -35,7 +37,8 @@ public void Init() Capture(); Capture(); Capture(); - _validator = new ShoppingCartValidator(new Mock().Object, _validatorFactoryMock.Object); + _mediatorMock = new Mock(); + _validator = new ShoppingCartValidator(_mediatorMock.Object, _validatorFactoryMock.Object); } private void Capture() @@ -88,4 +91,47 @@ public async Task GetRequiredProductWarnings_UsesTheGivenCustomerAndStore() Assert.AreEqual("store-b", record.StoreId); Assert.AreSame(Language, record.Language); } -} + + [TestMethod] + public async Task GetShoppingCartWarnings_PublishesTheWarningsWithTheLanguage() + { + //Arrange + var context = new CalculationContext(new Customer(), new Store { Id = "store" }, + new Currency { CurrencyCode = "EUR" }, TaxDisplayType.IncludingTax); + + //Act + await _validator.GetShoppingCartWarnings(new List(), null, context, Language, false, false); + + //Assert + _mediatorMock.Verify(x => x.Publish( + It.Is>(e => e.Language == Language), + It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task GetShoppingCartItemWarnings_PublishesTheWarningsWithTheLanguage() + { + //Act + await _validator.GetShoppingCartItemWarnings(new Customer(), new ShoppingCartItem(), new Product(), + NoChecks(), Language); + + //Assert + _mediatorMock.Verify(x => x.Publish( + It.Is>(e => e.Language == Language), + It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task GetShoppingCartItemWarnings_WithoutLanguage_Throws() + { + //Act & Assert + await Assert.ThrowsAsync(() => + _validator.GetShoppingCartItemWarnings(new Customer(), new ShoppingCartItem(), new Product(), NoChecks(), + null)); + } + + private static ShoppingCartValidatorOptions NoChecks() => new() { + GetStandardWarnings = false, GetInventoryWarnings = false, GetAttributesWarnings = false, + GetGiftVoucherWarnings = false, GetRequiredProductWarnings = false, GetReservationWarnings = false + }; +} \ No newline at end of file From 5769fe5aefef2810b2d5b247d0eecb9a6675189c Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 16:05:32 +0200 Subject: [PATCH 6/6] Put the language right after the customer in AddToCart and UpdateShoppingCartItem A required parameter inserted before the optional ones let an old positional null bind to the language and fail at runtime; now an old call does not compile. EndAuctionsTask skips a bid instead of throwing when no language exists at all. --- .../AddRequiredProductsCommandHandler.cs | 3 +- .../Handlers/Orders/ReOrderCommandHandler.cs | 5 +- .../Services/Orders/ShoppingCartService.cs | 10 ++-- .../Checkout/Orders/IShoppingCartService.cs | 14 ++--- .../BackgroundServices/EndAuctionsTask.cs | 17 ++++-- .../AddRequiredProductsCommandHandlerTests.cs | 4 +- .../Orders/ShoppingCartServiceTests.cs | 14 ++--- .../BackgroundService/EndAuctionsTaskTests.cs | 57 +++++++++++++++++-- .../Services/CustomerViewModelService.cs | 3 +- .../Controllers/ActionCartController.cs | 10 ++-- .../Controllers/ShoppingCartController.cs | 3 +- .../Controllers/WishlistController.cs | 7 ++- 12 files changed, 101 insertions(+), 46 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs index 9736e8399b..64a2d31be2 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/AddRequiredProductsCommandHandler.cs @@ -51,11 +51,10 @@ public async Task Handle(AddRequiredProductsCommand request, CancellationT //not in the cart if (alreadyInTheCart) continue; if (!product.AutoAddRequiredProducts) continue; - var addToCart = await _shoppingCartService.AddToCart(customer, + var addToCart = await _shoppingCartService.AddToCart(customer, request.Language, rp.Id, shoppingCartType, storeId, - request.Language, automaticallyAddRequiredProductsIfEnabled: false, validator: new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false }); diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs index 598645678c..668fcd8a80 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ReOrderCommandHandler.cs @@ -43,8 +43,9 @@ public async Task> Handle(ReOrderCommand request, CancellationToke if (product != null) { if (product.ProductTypeId == ProductType.SimpleProduct) - warnings.AddRange((await _shoppingCartService.AddToCart(customer, orderItem.ProductId, - ShoppingCartType.ShoppingCart, request.Order.StoreId, request.Language, orderItem.WarehouseId, + warnings.AddRange((await _shoppingCartService.AddToCart(customer, + request.Language, orderItem.ProductId, + ShoppingCartType.ShoppingCart, request.Order.StoreId, orderItem.WarehouseId, orderItem.Attributes, product.EnteredPrice ? _taxSettings.PricesIncludeTax ? orderItem.UnitPriceInclTax : orderItem.UnitPriceExclTax diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs index f9a3d22a1c..fff4930d34 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs @@ -185,8 +185,8 @@ public virtual async Task FindShoppingCartItem(IListShoppingCartValidatorOptions /// (warnings, shoppingCartItem) public virtual async Task<(IList warnings, ShoppingCartItem shoppingCartItem)> AddToCart(Customer customer, - string productId, - ShoppingCartType shoppingCartType, string storeId, Language language, + Language language, string productId, + ShoppingCartType shoppingCartType, string storeId, string warehouseId = null, IList attributes = null, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, @@ -315,8 +315,8 @@ await _mediator.Send(new AddRequiredProductsCommand { /// /// /// Warnings - public virtual async Task> UpdateShoppingCartItem(Customer customer, - string shoppingCartItemId, string warehouseId, IList attributes, Language language, + public virtual async Task> UpdateShoppingCartItem(Customer customer, Language language, + string shoppingCartItemId, string warehouseId, IList attributes, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, int quantity = 1, bool resetCheckoutData = true, string reservationId = "", string sciId = "") @@ -426,7 +426,7 @@ public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer to language)).Any()) continue; - await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, language, sci.WarehouseId, + await AddToCart(toCustomer, language, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, sci.WarehouseId, sci.Attributes, sci.EnteredPrice, sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false, sci.ReservationId, sci.Parameter, sci.Duration, diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs index 1840710129..79732af023 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Utilities.Checkout; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Localization; @@ -46,10 +46,10 @@ Task FindShoppingCartItem(IList shoppingCart /// Add a product to shopping cart /// /// Customer + /// Language the warnings are written in /// Product id /// ShoppingCartType /// Store id - /// Language the warnings are written in /// Warehouse id /// Attributes /// EnteredPrice @@ -62,8 +62,8 @@ Task FindShoppingCartItem(IList shoppingCart /// Duration for reservation /// ShoppingCartValidatorOptions /// (warnings, shoppingCartItem) - Task<(IList warnings, ShoppingCartItem shoppingCartItem)> AddToCart(Customer customer, string productId, - ShoppingCartType shoppingCartType, string storeId, Language language, string warehouseId = null, + Task<(IList warnings, ShoppingCartItem shoppingCartItem)> AddToCart(Customer customer, Language language, + string productId, ShoppingCartType shoppingCartType, string storeId, string warehouseId = null, IList attributes = null, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, @@ -76,10 +76,10 @@ Task FindShoppingCartItem(IList shoppingCart /// Updates the shopping cart item /// /// Customer + /// Language the warnings are written in /// Shopping cart item identifier /// Warehouse ident /// Attributes - /// Language the warnings are written in /// New customer entered price /// Rental start date /// Rental end date @@ -88,8 +88,8 @@ Task FindShoppingCartItem(IList shoppingCart /// Reservation ident /// /// Warnings - Task> UpdateShoppingCartItem(Customer customer, - string shoppingCartItemId, string warehouseId, IList attributes, Language language, + Task> UpdateShoppingCartItem(Customer customer, Language language, + string shoppingCartItemId, string warehouseId, IList attributes, double? customerEnteredPrice = null, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null, int quantity = 1, bool resetCheckoutData = true, string reservationId = "", string sciId = ""); diff --git a/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs b/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs index 1d9d479700..a94f66d73b 100644 --- a/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs +++ b/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs @@ -1,4 +1,5 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Customers; @@ -47,8 +48,8 @@ public async Task Execute() if (!auctionsToEnd.Any()) return; //cart warnings only reach the log, so they are written in the admin language - var language = await _languageService.GetLanguageById(_languageSettings.DefaultAdminLanguageId) ?? - (await _languageService.GetAllLanguages()).FirstOrDefault(); + var language = + await _languageService.GetLanguageOrStoreDefault(_languageSettings.DefaultAdminLanguageId, null); foreach (var auctionToEnd in auctionsToEnd) { var bid = (await _auctionService.GetBidsByProductId(auctionToEnd.Id)).MaxBy(x => x.Amount); @@ -60,9 +61,17 @@ await _messageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, continue; } + if (language == null) + { + _logger.LogWarning("EndAuctionTask - Product {Name} - no language exists to validate the bid", + auctionToEnd.Name); + continue; + } + var warnings = (await _shoppingCartService.AddToCart(await _customerService.GetCustomerById(bid.CustomerId), + language, bid.ProductId, ShoppingCartType.Auctions, - bid.StoreId, language, bid.WarehouseId, customerEnteredPrice: bid.Amount, + bid.StoreId, bid.WarehouseId, customerEnteredPrice: bid.Amount, validator: new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false })).warnings; if (!warnings.Any()) diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs index b709a679ac..6610c123d8 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/AddRequiredProductsCommandHandlerTests.cs @@ -55,8 +55,8 @@ public async Task HandleTest() //Assert _shoppingCartServiceMock.Verify( - c => c.AddToCart(It.IsAny(), It.IsAny(), ShoppingCartType.ShoppingCart, "", - command.Language, null, null, + c => c.AddToCart(It.IsAny(), + command.Language, It.IsAny(), ShoppingCartType.ShoppingCart, "", null, null, null, null, null, 1, false, "", "", "", It.IsAny()), Times.AtLeastOnce); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs index d6568b3068..fd042f560b 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs @@ -110,7 +110,7 @@ public async Task AddToCartTest_Success() .Returns(() => Task.FromResult((IList)new List())); //Act - var result = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); + var result = await _shoppingCartService.AddToCart(customer, Language, "2", ShoppingCartType.ShoppingCart, ""); //Assert Assert.IsNotNull(result.shoppingCartItem); @@ -135,7 +135,7 @@ public async Task AddToCartTest_Warning() .Returns(() => Task.FromResult((IList)new List { "Error" })); //Act - var result = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); + var result = await _shoppingCartService.AddToCart(customer, Language, "2", ShoppingCartType.ShoppingCart, ""); //Assert Assert.HasCount(1, result.warnings); @@ -159,10 +159,10 @@ public async Task UpdateShoppingCartItemTest() It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); - var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); + var item = await _shoppingCartService.AddToCart(customer, Language, "2", ShoppingCartType.ShoppingCart, ""); //Act - var result = await _shoppingCartService.UpdateShoppingCartItem(customer, item.shoppingCartItem.Id, "", - new List(), Language, null); + var result = await _shoppingCartService.UpdateShoppingCartItem(customer, Language, item.shoppingCartItem.Id, "", + new List(), null); //Assert _customerServiceMock.Verify(x => x.UpdateShoppingCartItem(It.IsAny(), It.IsAny()), Times.Once); @@ -186,7 +186,7 @@ public async Task DeleteShoppingCartItemTest() It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); - var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); + var item = await _shoppingCartService.AddToCart(customer, Language, "2", ShoppingCartType.ShoppingCart, ""); //Act await _shoppingCartService.DeleteShoppingCartItem(customer, item.shoppingCartItem); //Assert @@ -215,7 +215,7 @@ public async Task MigrateShoppingCartTest() It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); - var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, "", Language); + var item = await _shoppingCartService.AddToCart(customer, Language, "2", ShoppingCartType.ShoppingCart, ""); var customer2 = new Customer(); //Act await _shoppingCartService.MigrateShoppingCart(customer, customer2, "", Language, false); diff --git a/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs b/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs index 24e1063208..86c3111623 100644 --- a/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs +++ b/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs @@ -67,8 +67,9 @@ public async Task Execute_HasWarnings_InvokeExpectedMethos() _auctionMock.Setup(c => c.GetAuctionsToEnd()).ReturnsAsync(new List { new() { Id = "id" } }); _auctionMock.Setup(c => c.GetBidsByProductId(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => new PagedList { new() }); - _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), + _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -77,8 +78,9 @@ public async Task Execute_HasWarnings_InvokeExpectedMethos() await _task.Execute(); //the warnings only reach the log, so they are written in the admin language - _shoppingCartMock.Verify(c => c.AddToCart(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.Is(l => l.Id == "admin-lang"), + _shoppingCartMock.Verify(c => c.AddToCart(It.IsAny(), + It.Is(l => l.Id == "admin-lang"), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); @@ -96,8 +98,9 @@ public async Task Execute_Valid_InvokeExpectedMethos() _auctionMock.Setup(c => c.GetAuctionsToEnd()).ReturnsAsync(new List { new() { Id = "id" } }); _auctionMock.Setup(c => c.GetBidsByProductId(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => new PagedList { new() }); - _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), + _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -123,4 +126,46 @@ public async Task Execute_Valid_InvokeExpectedMethos() c => c.SendAuctionEndedLostCustomerMessage(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } + + [TestMethod] + public async Task Execute_DeletedAdminLanguage_UsesAPublishedLanguage() + { + _languageServiceMock.Setup(c => c.GetLanguageById("admin-lang")).ReturnsAsync((Language)null); + _languageServiceMock.Setup(c => c.GetAllLanguages(false, "")) + .ReturnsAsync(new List { new() { Id = "published-lang" } }); + _auctionMock.Setup(c => c.GetAuctionsToEnd()).ReturnsAsync(new List { new() { Id = "id" } }); + _auctionMock.Setup(c => c.GetBidsByProductId(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(() => new PagedList { new() }); + _shoppingCartMock.Setup(c => c.AddToCart(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((new List(), null)); + + await _task.Execute(); + + _shoppingCartMock.Verify(c => c.AddToCart(It.IsAny(), + It.Is(l => l.Id == "published-lang"), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task Execute_NoLanguageAtAll_SkipsTheBidWithoutThrowing() + { + _languageServiceMock.Setup(c => c.GetLanguageById("admin-lang")).ReturnsAsync((Language)null); + _languageServiceMock.Setup(c => c.GetAllLanguages(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + _auctionMock.Setup(c => c.GetAuctionsToEnd()).ReturnsAsync(new List { new() { Id = "id" } }); + _auctionMock.Setup(c => c.GetBidsByProductId(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(() => new PagedList { new() }); + + await _task.Execute(); + + _shoppingCartMock.VerifyNoOtherCalls(); + _auctionMock.Verify(c => c.UpdateAuctionEnded(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index 209eceef95..d3b2154665 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -915,11 +915,10 @@ public virtual async Task> UpdateCart(Customer customer, string sh if (cart != null) return await _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService() .UpdateShoppingCartItem( - customer, + customer, _contextAccessor.WorkContext.WorkingLanguage, shoppingCartId, cart.WarehouseId, cart.Attributes, - _contextAccessor.WorkContext.WorkingLanguage, unitprice, cart.RentalStartDateUtc, cart.RentalEndDateUtc, diff --git a/src/Web/Grand.Web/Controllers/ActionCartController.cs b/src/Web/Grand.Web/Controllers/ActionCartController.cs index e9dfc94a6b..24b7566ae0 100644 --- a/src/Web/Grand.Web/Controllers/ActionCartController.cs +++ b/src/Web/Grand.Web/Controllers/ActionCartController.cs @@ -212,11 +212,10 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, } //try adding product to the cart - var addToCart = await _shoppingCartService.AddToCart(customer, + var addToCart = await _shoppingCartService.AddToCart(customer, _contextAccessor.WorkContext.WorkingLanguage, product.Id, model.ShoppingCartTypeId, _contextAccessor.StoreContext.CurrentStore.Id, - _contextAccessor.WorkContext.WorkingLanguage, warehouseId, quantity: model.Quantity, validator: new ShoppingCartValidatorOptions { @@ -403,8 +402,8 @@ public virtual async Task> AddProductDetails(Pr //add to the cart var (warnings, shoppingCartItem) = await _shoppingCartService.AddToCart(_contextAccessor.WorkContext.CurrentCustomer, - product.Id, model.ShoppingCartTypeId, _contextAccessor.StoreContext.CurrentStore.Id, - _contextAccessor.WorkContext.WorkingLanguage, warehouseId, + _contextAccessor.WorkContext.WorkingLanguage, + product.Id, model.ShoppingCartTypeId, _contextAccessor.StoreContext.CurrentStore.Id, warehouseId, attributes, customerEnteredPriceConverted, rentalStartDate, rentalEndDate, model.EnteredQuantity, true, model.Reservation, parameter, duration, new ShoppingCartValidatorOptions { @@ -644,7 +643,8 @@ await _currencyService.ConvertToPrimaryStoreCurrency(customerEnteredPrice, //update existing item addToCartWarnings.AddRange(await _shoppingCartService.UpdateShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, - cart.Id, warehouseId, attributes, _contextAccessor.WorkContext.WorkingLanguage, + _contextAccessor.WorkContext.WorkingLanguage, + cart.Id, warehouseId, attributes, customerEnteredPriceConverted, rentalStartDate, rentalEndDate, model.EnteredQuantity)); if (addToCartWarnings.Any()) diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index c4e4af1492..3cab9ba40f 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -323,7 +323,8 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode if (cart != null) { var currSciWarnings = await _shoppingCartService.UpdateShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, - cart.Id, cart.WarehouseId, cart.Attributes, _contextAccessor.WorkContext.WorkingLanguage, cart.EnteredPrice, + _contextAccessor.WorkContext.WorkingLanguage, + cart.Id, cart.WarehouseId, cart.Attributes, cart.EnteredPrice, cart.RentalStartDateUtc, cart.RentalEndDateUtc, model.Quantity); warnings.AddRange(currSciWarnings); diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 8fc3ed011d..4456f6b92a 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -123,8 +123,8 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode if (cart != null) { var currSciWarnings = await _shoppingCartService.UpdateShoppingCartItem( - _contextAccessor.WorkContext.CurrentCustomer, - cart.Id, cart.WarehouseId, cart.Attributes, _contextAccessor.WorkContext.WorkingLanguage, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.WorkContext.WorkingLanguage, + cart.Id, cart.WarehouseId, cart.Attributes, cart.EnteredPrice, cart.RentalStartDateUtc, cart.RentalEndDateUtc, model.Quantity); @@ -170,8 +170,9 @@ public virtual async Task AddItemToCartFromWishlist(AddCartFromWi return Json(new { success = false, message = "Shopping cart ident not found" }); var warnings = (await _shoppingCartService.AddToCart(_contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.WorkContext.WorkingLanguage, itemCart.ProductId, ShoppingCartType.ShoppingCart, - _contextAccessor.StoreContext.CurrentStore.Id, _contextAccessor.WorkContext.WorkingLanguage, + _contextAccessor.StoreContext.CurrentStore.Id, itemCart.WarehouseId, itemCart.Attributes, itemCart.EnteredPrice, itemCart.RentalStartDateUtc, itemCart.RentalEndDateUtc, itemCart.Quantity,