From fc9591b5f608f4b1d93ba087bfc7212df8bde0b7 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 17:16:15 +0200 Subject: [PATCH 1/4] Link message e-mails to the host of their store MessageProviderService took the link host from the current request, so a message sent from the admin panel for another store, or from a background task, linked to the wrong place. It now uses the primary domain of the message's store and no longer reads IContextAccessor. --- .../Services/MessageProviderService.cs | 97 ++++++++++--------- .../Services/MessageProviderServiceTest.cs | 48 +++++++-- 2 files changed, 93 insertions(+), 52 deletions(-) diff --git a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs index 4778a0c1ec..c94c8395d4 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs @@ -18,7 +18,6 @@ using Grand.Domain.Shipping; using Grand.Domain.Stores; using Grand.Domain.Vendors; -using Grand.Infrastructure; using Grand.SharedKernel.Extensions; using Grand.Mediator; using System.Net; @@ -30,7 +29,6 @@ public class MessageProviderService : IMessageProviderService #region Ctor public MessageProviderService( - IContextAccessor contextAccessor, IMessageTemplateService messageTemplateService, IQueuedEmailService queuedEmailService, ILanguageService languageService, @@ -41,7 +39,6 @@ public MessageProviderService( EmailAccountSettings emailAccountSettings, CommonSettings commonSettings) { - _contextAccessor = contextAccessor; _messageTemplateService = messageTemplateService; _queuedEmailService = queuedEmailService; _languageService = languageService; @@ -56,7 +53,6 @@ public MessageProviderService( #endregion #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IMessageTemplateService _messageTemplateService; private readonly IQueuedEmailService _queuedEmailService; private readonly ILanguageService _languageService; @@ -68,12 +64,23 @@ public MessageProviderService( private readonly EmailAccountSettings _emailAccountSettings; private readonly CommonSettings _commonSettings; - private DomainHost CurrentHost => _contextAccessor.StoreContext.CurrentHost; - #endregion #region Utilities + /// + /// Gets the host the links of a message point to: the store's primary domain, so a message links to the + /// store it is about whoever sends it, in a request to any host or in a background task + /// + protected static DomainHost StoreHost(Store store) + { + return store.Domains.FirstOrDefault(x => x.Primary) ?? store.Domains.FirstOrDefault() ?? new DomainHost { + Id = int.MinValue.ToString(), + Url = store.Url, + HostName = "temporary-store" + }; + } + protected virtual async Task GetStore(string storeId) { return await _storeService.GetStoreById(storeId) ?? (await _storeService.GetAllStores()).FirstOrDefault(); @@ -123,7 +130,7 @@ private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer cu { if (customer != null) { - builder.AddCustomerTokens(customer, store, CurrentHost, language); + builder.AddCustomerTokens(customer, store, StoreHost(store), language); } } @@ -158,7 +165,7 @@ protected virtual async Task SendCustomerMessage(Customer customer, Store s var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language, customerNote); + .AddCustomerTokens(customer, store, StoreHost(store), language, customerNote); var liquidObject = await builder.BuildAsync(); //event notification @@ -324,7 +331,7 @@ private async Task SendOrderStoreOwnerMessage(string template, Order order, var liquidBuilder = new LiquidObjectBuilder(_mediator); liquidBuilder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, CurrentHost); + .AddOrderTokens(order, customer, store, StoreHost(store)); AddCustomerTokensIfNotNull(liquidBuilder, customer, store, language); var liquidObject = await liquidBuilder.BuildAsync(); @@ -455,7 +462,7 @@ private async Task SendOrderCustomerMessage(string message, Order order, Cu var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, CurrentHost); + .AddOrderTokens(order, customer, store, StoreHost(store)); AddCustomerTokensIfNotNull(builder, customer, store, language); var liquidObject = await builder.BuildAsync(); @@ -536,7 +543,7 @@ private async Task SendOrderVendorMessage(string message, Order order, Vend var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, CurrentHost, vendor: vendor); + .AddOrderTokens(order, customer, store, StoreHost(store), vendor: vendor); AddCustomerTokensIfNotNull(builder, customer, store, language); @@ -601,8 +608,8 @@ private async Task SendShipmentCustomerMessage(string message, Shipment shi var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddShipmentTokens(shipment, order, store, CurrentHost, language) - .AddOrderTokens(order, customer, store, CurrentHost); + .AddShipmentTokens(shipment, order, store, StoreHost(store), language) + .AddOrderTokens(order, customer, store, StoreHost(store)); AddCustomerTokensIfNotNull(builder, customer, store, language); @@ -644,7 +651,7 @@ public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, CurrentHost, orderNote); + .AddOrderTokens(order, customer, store, StoreHost(store), orderNote); AddCustomerTokensIfNotNull(builder, customer, store, language); @@ -711,7 +718,7 @@ private async Task SendNewsLetterSubscriptionMessage(string message, NewsLe var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddNewsLetterSubscriptionTokens(subscription, store, CurrentHost); + .AddNewsLetterSubscriptionTokens(subscription, store, StoreHost(store)); var liquidObject = await builder.BuildAsync(); //event notification @@ -757,8 +764,8 @@ public virtual async Task SendProductEmailAFriendMessage(Customer customer, var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail) - .AddCustomerTokens(customer, store, CurrentHost, language) - .AddProductTokens(product, language, store, CurrentHost); + .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddProductTokens(product, language, store, StoreHost(store)); var liquidObject = await builder.BuildAsync(); //event notification @@ -799,7 +806,7 @@ public virtual async Task SendWishlistEmailAFriendMessage(Customer customer var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language) + .AddCustomerTokens(customer, store, StoreHost(store), language) .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail); var liquidObject = await builder.BuildAsync(); @@ -837,8 +844,8 @@ public virtual async Task SendProductQuestionMessage(Customer customer, Sto var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language) - .AddProductTokens(product, language, store, CurrentHost); + .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddProductTokens(product, language, store, StoreHost(store)); var liquidObject = await builder.BuildAsync(); liquidObject.AskQuestion = new LiquidAskQuestion(message, customerEmail, fullName, phone); @@ -919,7 +926,7 @@ public virtual async Task SendNewMerchandiseReturnStoreOwnerMessage(Merchan AddCustomerTokensIfNotNull(builder, customer, store, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, CurrentHost, order, language); + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language); var liquidObject = await builder.BuildAsync(); @@ -976,7 +983,7 @@ public virtual async Task SendMerchandiseReturnStatusChangedCustomerMessage AddCustomerTokensIfNotNull(builder, customer, store, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, CurrentHost, order, language); + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1017,7 +1024,7 @@ public virtual async Task SendNewMerchandiseReturnCustomerMessage(Merchandi builder.AddStoreTokens(store, language, emailAccount); AddCustomerTokensIfNotNull(builder, customer, store, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, CurrentHost, order, language); + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1059,7 +1066,7 @@ public virtual async Task SendNewMerchandiseReturnNoteAddedCustomerMessage( builder.AddStoreTokens(store, language, emailAccount); AddCustomerTokensIfNotNull(builder, customer, store, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, CurrentHost, order, language, + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language, merchandiseReturnNote); var liquidObject = await builder.BuildAsync(); //event notification @@ -1100,7 +1107,7 @@ public virtual async Task SendNewVendorAccountApplyStoreOwnerMessage(Custom var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var builder = new LiquidObjectBuilder(_mediator).AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language) + .AddCustomerTokens(customer, store, StoreHost(store), language) .AddVendorTokens(vendor, language); var liquidObject = await builder.BuildAsync(); @@ -1290,7 +1297,7 @@ public virtual async Task SendQuantityBelowStoreOwnerMessage(Product produc var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddProductTokens(product, language, store, CurrentHost); + .AddProductTokens(product, language, store, StoreHost(store)); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1327,8 +1334,8 @@ public virtual async Task SendQuantityBelowStoreOwnerMessage(Product produc var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddProductTokens(product, language, store, CurrentHost) - .AddAttributeCombinationTokens(product, combination, language, CurrentHost); + .AddProductTokens(product, language, store, StoreHost(store)) + .AddAttributeCombinationTokens(product, combination, language, StoreHost(store)); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1362,7 +1369,7 @@ public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer cust var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language); + .AddCustomerTokens(customer, store, StoreHost(store), language); var liquidObject = await builder.BuildAsync(); @@ -1393,11 +1400,11 @@ public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogCom var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddBlogCommentTokens(blogPost, blogComment, store, CurrentHost, language); + .AddBlogCommentTokens(blogPost, blogComment, store, StoreHost(store), language); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = blogComment.CustomerId }); if (customer != null && await _groupService.IsRegistered(customer)) - builder.AddCustomerTokens(customer, store, CurrentHost, language); + builder.AddCustomerTokens(customer, store, StoreHost(store), language); var liquidObject = await builder.BuildAsync(); //event notification @@ -1434,11 +1441,11 @@ public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle ar var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddArticleCommentTokens(article, articleComment, store, CurrentHost, language); + .AddArticleCommentTokens(article, articleComment, store, StoreHost(store), language); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = articleComment.CustomerId }); if (customer != null && await _groupService.IsRegistered(customer)) - builder.AddCustomerTokens(customer, store, CurrentHost, language); + builder.AddCustomerTokens(customer, store, StoreHost(store), language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1472,7 +1479,7 @@ public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsCom var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddNewsCommentTokens(newsItem, newsComment, store, CurrentHost, language); + .AddNewsCommentTokens(newsItem, newsComment, store, StoreHost(store), language); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = newsComment.CustomerId }); AddCustomerTokensIfNotNull(builder, customer, store, language); @@ -1513,7 +1520,7 @@ public virtual async Task SendBackInStockMessage(Customer customer, Product builder.AddStoreTokens(store, language, emailAccount); AddCustomerTokensIfNotNull(builder, customer, store, language); - builder.AddOutOfStockTokens(product, subscription, store, CurrentHost, language); + builder.AddOutOfStockTokens(product, subscription, store, StoreHost(store), language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1570,7 +1577,7 @@ public virtual async Task SendContactUsMessage(Customer customer, Store sto var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language); + .AddCustomerTokens(customer, store, StoreHost(store), language); var liquidObject = await builder.BuildAsync(); liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, attrInfo); @@ -1649,7 +1656,7 @@ public virtual async Task SendContactVendorMessage(Customer customer, Store var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, CurrentHost, language); + .AddCustomerTokens(customer, store, StoreHost(store), language); var liquidObject = await builder.BuildAsync(); liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, ""); //event notification @@ -1703,8 +1710,8 @@ public virtual async Task SendAuctionWinEndedCustomerMessage(Product produc var builder = new LiquidObjectBuilder(_mediator); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, CurrentHost, language) - .AddProductTokens(product, language, store, CurrentHost) + .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddProductTokens(product, language, store, StoreHost(store)) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); @@ -1738,7 +1745,7 @@ public virtual async Task SendAuctionEndedLostCustomerMessage(Product produ var builder = new LiquidObjectBuilder(_mediator); builder.AddAuctionTokens(product, bid) - .AddProductTokens(product, language, store, CurrentHost) + .AddProductTokens(product, language, store, StoreHost(store)) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); @@ -1750,7 +1757,7 @@ public virtual async Task SendAuctionEndedLostCustomerMessage(Product produ var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); if (string.IsNullOrEmpty(languageId)) languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - builder2.AddCustomerTokens(customer, store, CurrentHost, language); + builder2.AddCustomerTokens(customer, store, StoreHost(store), language); var liquidObject2 = await builder2.BuildAsync(); //event notification @@ -1782,7 +1789,7 @@ public virtual async Task SendAuctionEndedBinCustomerMessage(Product produc var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var builder = new LiquidObjectBuilder(_mediator); - builder.AddProductTokens(product, language, store, CurrentHost) + builder.AddProductTokens(product, language, store, StoreHost(store)) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); @@ -1838,7 +1845,7 @@ public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, CurrentHost, language) + .AddCustomerTokens(customer, store, StoreHost(store), language) .AddStoreTokens(store, language, emailAccount); } else @@ -1850,7 +1857,7 @@ public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product return 0; emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - builder.AddProductTokens(product, language, store, CurrentHost); + builder.AddProductTokens(product, language, store, StoreHost(store)); } var liquidObject = await builder.BuildAsync(); @@ -1892,7 +1899,7 @@ public virtual async Task SendOutBidCustomerMessage(Product product, string var builder = new LiquidObjectBuilder(_mediator); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, CurrentHost, language) + .AddCustomerTokens(customer, store, StoreHost(store), language) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); diff --git a/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs b/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs index 087d816510..165fa4b4f4 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs @@ -1,8 +1,10 @@ -using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Events.Messages; +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.Messages; using Grand.Business.Core.Queries.Messages; +using Grand.Business.Core.Utilities.Messages.DotLiquidDrops; using Grand.Business.Messages.Services; using Grand.Domain; using Grand.Domain.Catalog; @@ -12,7 +14,6 @@ using Grand.Domain.Messages; using Grand.Domain.Stores; using Grand.Domain.Vendors; -using Grand.Infrastructure; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -33,7 +34,6 @@ public class MessageProviderServiceTest private Mock _messageTokenProviderMock; private Product _product; private Mock _queuedEmailServiceMock; - private Mock _contextAccessorMock; private Mock _storeServiceMock; [TestInitialize] @@ -63,8 +63,6 @@ public void Init() .Returns(Task.FromResult(new Store { Url = "https://localhost:44350/" })); _storeServiceMock.Setup(x => x.GetAllStores()).Returns(Task.FromResult(new List() as IList)); - _contextAccessorMock = new Mock(); - _contextAccessorMock.Setup(x => x.StoreContext.CurrentHost).Returns(new DomainHost { Url = "https://localhost:44350/" }); _mediatorMock = new Mock(); _mediatorMock.Setup(x => x.Send(It.IsAny(), default)) .Returns(Task.FromResult(new Customer { Email = "sdfsdf@mail.com" })); @@ -73,7 +71,6 @@ public void Init() _commonSetting = new Mock(); _messageService = new MessageProviderService( - _contextAccessorMock.Object, _messageTemplateServiceMock.Object, _queuedEmailServiceMock.Object, _languageServiceMock.Object, @@ -120,4 +117,41 @@ public async Task SendNotificationMethodToEmailIsNull() Assert.AreEqual(0, result); } -} \ No newline at end of file + + [TestMethod] + public async Task SendCustomerMessage_LinksPointToThePrimaryDomainOfTheStore() + { + var store = new Store { + Url = "https://store.example/", + Domains = { + new DomainHost { HostName = "second.example", Url = "https://second.example/" }, + new DomainHost { HostName = "primary.example", Url = "https://primary.example/", Primary = true } + } + }; + + var url = await PasswordRecoveryUrl(store); + + StringAssert.StartsWith(url, "https://primary.example/"); + } + + [TestMethod] + public async Task SendCustomerMessage_StoreWithoutDomains_LinksPointToTheStoreUrl() + { + var url = await PasswordRecoveryUrl(new Store { Url = "https://store.example/" }); + + StringAssert.StartsWith(url, "https://store.example/"); + } + + private async Task PasswordRecoveryUrl(Store store) + { + LiquidCustomer liquidCustomer = null; + _mediatorMock.Setup(x => x.Publish(It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((e, _) => + liquidCustomer = (LiquidCustomer)e.LiquidDrop); + + await _messageService.SendCustomerPasswordRecoveryMessage(new Customer { Email = "a@b.c" }, store, ""); + + Assert.IsNotNull(liquidCustomer); + return liquidCustomer.PasswordRecoveryURL; + } +} From da1a2cf4878e237192ad0194e7b3177d34cbccf8 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 17:16:16 +0200 Subject: [PATCH 2/4] Pass an explicit context to contact attributes and coordinates GetAllContactAttributes takes a CatalogAccess and keys its cache by the customer's groups, so one visitor's attributes are no longer served to everyone. The contact parser takes the access and the host for file links; the parameterless coordinate overloads are gone, and a customer without coordinates no longer throws. --- .../Contacts/IContactAttributeParser.cs | 11 ++- .../Contacts/IContactAttributeService.cs | 6 +- .../Customers/ICustomerCoordinatesService.cs | 2 - .../Contacts/ContactAttributeParser.cs | 21 +++--- .../Contacts/ContactAttributeService.cs | 31 ++++----- .../Customers/CustomerCoordinatesService.cs | 23 ++----- .../Caching/Constants/MessagesCacheKey.cs | 5 +- .../Contacts/ContactAttributeParserTests.cs | 51 ++++++++++++-- .../Contacts/ContactAttributeServiceTests.cs | 68 ++++++++++++++++--- .../CustomerCoordinatesServiceTests.cs | 32 ++------- .../ContactAttributeViewModelService.cs | 5 +- .../ContactAttributeChangeCommandHandler.cs | 5 +- .../Contact/ContactUsCommandHandler.cs | 3 +- .../Contact/ContactUsSendCommandHandler.cs | 21 +++--- .../Models/Contact/ContactUsSendCommand.cs | 9 ++- .../Controllers/ContactController.cs | 4 ++ .../Validators/Contact/ContactUsValidator.cs | 8 ++- 17 files changed, 184 insertions(+), 121 deletions(-) diff --git a/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs b/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs index a5dc53b7ab..741e1969d3 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs @@ -1,7 +1,9 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Localization; using Grand.Domain.Messages; +using Grand.Domain.Stores; namespace Grand.Business.Core.Interfaces.Marketing.Contacts; @@ -56,6 +58,7 @@ IList AddContactAttribute(IList customAttribut /// Language /// Attributes /// Customer + /// Store host the file upload links point to; without one no link is rendered /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to HTML hyperlink tags could be rendered (if required) @@ -64,6 +67,7 @@ Task FormatAttributes( Language language, IList customAttributes, Customer customer, + DomainHost host, string separator = "
", bool htmlEncode = true, bool allowHyperlinks = true); @@ -71,7 +75,8 @@ Task FormatAttributes( /// /// Get parse contact attributes /// - /// - /// - Task> GetParseContactAttributes(IList model); + /// Posted attributes + /// Customer whose groups limit the attributes and the store + /// Selected attributes whose conditions are met + Task> GetParseContactAttributes(IList model, CatalogAccess access); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeService.cs index 42f9a4aa35..66a0b8c26c 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeService.cs @@ -1,3 +1,4 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Messages; namespace Grand.Business.Core.Interfaces.Marketing.Contacts; @@ -18,10 +19,9 @@ public interface IContactAttributeService /// /// Gets all Contact attributes /// - /// Store identifier - /// Ignore acl + /// Customer whose groups limit the result and the store, or every attribute of the store /// Contact attributes - Task> GetAllContactAttributes(string storeId = "", bool ignoreAcl = false); + Task> GetAllContactAttributes(CatalogAccess access); /// /// Gets a Contact attribute diff --git a/src/Business/Grand.Business.Core/Interfaces/Marketing/Customers/ICustomerCoordinatesService.cs b/src/Business/Grand.Business.Core/Interfaces/Marketing/Customers/ICustomerCoordinatesService.cs index 008e35bf6e..65f506715a 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Marketing/Customers/ICustomerCoordinatesService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Marketing/Customers/ICustomerCoordinatesService.cs @@ -4,8 +4,6 @@ namespace Grand.Business.Core.Interfaces.Marketing.Customers; public interface ICustomerCoordinatesService { - Task<(double longitude, double latitude)> GetGeoCoordinate(); Task<(double longitude, double latitude)> GetGeoCoordinate(Customer customer); - Task SaveGeoCoordinate(double longitude, double latitude); Task SaveGeoCoordinate(Customer customer, double longitude, double latitude); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs index 5d9c109a18..454ec972ae 100644 --- a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs +++ b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs @@ -1,11 +1,12 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Localization; using Grand.Domain.Messages; -using Grand.Infrastructure; +using Grand.Domain.Stores; using Grand.SharedKernel.Extensions; using System.Net; @@ -17,15 +18,10 @@ namespace Grand.Business.Marketing.Services.Contacts; public class ContactAttributeParser : IContactAttributeParser { private readonly IContactAttributeService _contactAttributeService; - private readonly IContextAccessor _contextAccessor; - public ContactAttributeParser( - IContactAttributeService contactAttributeService, - IContextAccessor contextAccessor - ) + public ContactAttributeParser(IContactAttributeService contactAttributeService) { _contactAttributeService = contactAttributeService; - _contextAccessor = contextAccessor; } /// @@ -162,6 +158,7 @@ public virtual IList RemoveContactAttribute(IListLanguage /// Attributes /// Customer + /// Store host the file upload links point to; without one no link is rendered /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to HTML hyperlink tags could be rendered (if required) @@ -170,6 +167,7 @@ public virtual async Task FormatAttributes( Language language, IList customAttributes, Customer customer, + DomainHost host, string separator = "
", bool htmlEncode = true, bool allowHyperlinks = true) @@ -210,10 +208,10 @@ public virtual async Task FormatAttributes( { var attributeText = string.Empty; var attributeName = attribute.GetTranslation(a => a.Name, language.Id); - if (allowHyperlinks) + if (allowHyperlinks && host != null) { var downloadLink = - $"{_contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}"; + $"{host.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}"; attributeText = $"{attribute.GetTranslation(a => a.TextPrompt, language.Id)}"; } @@ -256,10 +254,11 @@ public virtual async Task FormatAttributes( return result.ToString(); } - public async Task> GetParseContactAttributes(IList model) + public async Task> GetParseContactAttributes(IList model, + CatalogAccess access) { var customAttributes = new List(); - var contactAttributes = await _contactAttributeService.GetAllContactAttributes(); + var contactAttributes = await _contactAttributeService.GetAllContactAttributes(access); foreach (var attribute in contactAttributes) switch (attribute.AttributeControlType) { diff --git a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeService.cs b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeService.cs index 6e8ede082c..0e73733050 100644 --- a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeService.cs @@ -1,8 +1,8 @@ using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Customers; using Grand.Domain.Messages; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -24,12 +24,11 @@ public class ContactAttributeService : IContactAttributeService public ContactAttributeService(ICacheBase cacheBase, IRepository contactAttributeRepository, IMediator mediator, - IContextAccessor contextAccessor, AccessControlConfig accessControlConfig) + AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _contactAttributeRepository = contactAttributeRepository; _mediator = mediator; - _contextAccessor = contextAccessor; _accessControlConfig = accessControlConfig; } @@ -40,7 +39,6 @@ public ContactAttributeService(ICacheBase cacheBase, private readonly IRepository _contactAttributeRepository; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; - private readonly IContextAccessor _contextAccessor; private readonly AccessControlConfig _accessControlConfig; #endregion @@ -69,13 +67,16 @@ public virtual async Task DeleteContactAttribute(ContactAttribute contactAttribu /// /// Gets all contact attributes /// - /// Store identifier - /// + /// Customer whose groups limit the result and the store, or every attribute of the store /// Contact attributes - public virtual async Task> GetAllContactAttributes(string storeId = "", - bool ignoreAcl = false) + public virtual async Task> GetAllContactAttributes(CatalogAccess access) { - var key = string.Format(CacheKey.CONTACTATTRIBUTES_ALL_KEY, storeId, ignoreAcl); + ArgumentNullException.ThrowIfNull(access); + + var applyAcl = !access.ShowHidden && !_accessControlConfig.IgnoreAcl; + var allowedCustomerGroupsIds = applyAcl ? access.Customer.GetCustomerGroupIds() : []; + var key = string.Format(CacheKey.CONTACTATTRIBUTES_ALL_KEY, access.StoreId, access.ShowHidden, + string.Join(",", allowedCustomerGroupsIds)); return await _cacheBase.GetAsync(key, async () => { var query = from p in _contactAttributeRepository.Table @@ -83,21 +84,15 @@ public virtual async Task> GetAllContactAttributes(strin query = query.OrderBy(c => c.DisplayOrder); - if ((string.IsNullOrEmpty(storeId) || _accessControlConfig.IgnoreStoreLimitations) && - (ignoreAcl || _accessControlConfig.IgnoreAcl)) - return await _contactAttributeRepository.ToListAsync(query); - if (!ignoreAcl && !_accessControlConfig.IgnoreAcl) - { - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); + if (applyAcl) query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; - } //Store acl - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) select p; return await _contactAttributeRepository.ToListAsync(query); }); diff --git a/src/Business/Grand.Business.Marketing/Services/Customers/CustomerCoordinatesService.cs b/src/Business/Grand.Business.Marketing/Services/Customers/CustomerCoordinatesService.cs index b4e4bd615b..fd348f86ab 100644 --- a/src/Business/Grand.Business.Marketing/Services/Customers/CustomerCoordinatesService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Customers/CustomerCoordinatesService.cs @@ -3,7 +3,6 @@ using Grand.Data; using Grand.Domain.Common; using Grand.Domain.Customers; -using Grand.Infrastructure; using Grand.Mediator; namespace Grand.Business.Marketing.Services.Customers; @@ -12,36 +11,22 @@ public class CustomerCoordinatesService : ICustomerCoordinatesService { private readonly IRepository _customerRepository; private readonly IMediator _mediator; - private readonly IContextAccessor _contextAccessor; public CustomerCoordinatesService( IRepository customerRepository, - IContextAccessor contextAccessor, IMediator mediator) { _customerRepository = customerRepository; - _contextAccessor = contextAccessor; _mediator = mediator; } - public Task<(double longitude, double latitude)> GetGeoCoordinate() - { - return GetGeoCoordinate(_contextAccessor.WorkContext.CurrentCustomer); - } - - public async Task<(double longitude, double latitude)> GetGeoCoordinate(Customer customer) + public Task<(double longitude, double latitude)> GetGeoCoordinate(Customer customer) { ArgumentNullException.ThrowIfNull(customer); - if (customer.Coordinates == null) - await Task.FromResult((0, 0)); - - return await Task.FromResult((customer.Coordinates!.X, customer.Coordinates!.Y)); - } - - public async Task SaveGeoCoordinate(double longitude, double latitude) - { - await SaveGeoCoordinate(_contextAccessor.WorkContext.CurrentCustomer, longitude, latitude); + return customer.Coordinates == null + ? Task.FromResult((0d, 0d)) + : Task.FromResult((customer.Coordinates.X, customer.Coordinates.Y)); } public async Task SaveGeoCoordinate(Customer customer, double longitude, double latitude) diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/MessagesCacheKey.cs b/src/Core/Grand.Infrastructure/Caching/Constants/MessagesCacheKey.cs index 6339f6c409..29568bd759 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/MessagesCacheKey.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/MessagesCacheKey.cs @@ -9,9 +9,10 @@ public static partial class CacheKey ///
/// /// {0} : store ID - /// {1} : ignore ACL? + /// {1} : show hidden? + /// {2} : customer group IDs, empty when hidden attributes are shown /// - public static string CONTACTATTRIBUTES_ALL_KEY => "Grand.contactattribute.all-{0}-{1}"; + public static string CONTACTATTRIBUTES_ALL_KEY => "Grand.contactattribute.all-{0}-{1}-{2}"; /// /// Key for caching diff --git a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs index 2a15794706..377fca0eb7 100644 --- a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs +++ b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs @@ -1,12 +1,13 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Marketing.Services.Contacts; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Localization; using Grand.Domain.Messages; -using Grand.Infrastructure; +using Grand.Domain.Stores; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -18,20 +19,18 @@ public class ContactAttributeParserTests private List _contactAtr; private Mock _contactAttributeServiceMock; private ContactAttributeParser _parser; - private Mock _workContextMock; private List customAtr; [TestInitialize] public void Init() { _contactAttributeServiceMock = new Mock(); - _workContextMock = new Mock(); var _translationServiceMock = new Mock(); { _translationServiceMock.Setup(x => x.GetResource(It.IsAny())).Returns("Warning{0}"); } - _parser = new ContactAttributeParser(_contactAttributeServiceMock.Object, _workContextMock.Object); + _parser = new ContactAttributeParser(_contactAttributeServiceMock.Object); customAtr = [ new CustomAttribute { Key = "key1", Value = "value1" }, new CustomAttribute { Key = "key2", Value = "value2" }, @@ -120,14 +119,52 @@ public void RemoveContactAttributeTest() [TestMethod] public async Task FormatAttributesTest() { - _contactAttributeServiceMock.Setup(c => c.GetAllContactAttributes("", false)) - .Returns(() => Task.FromResult((IList)_contactAtr)); _contactAttributeServiceMock.Setup(c => c.GetContactAttributeById(It.IsAny())).Returns((string w) => Task.FromResult(_contactAtr.FirstOrDefault(a => a.Id.Equals(w)))); var result = await _parser.FormatAttributes(new Language(), new List { new() { Key = "key1", Value = "value1" } - }, new Customer()); + }, new Customer(), null); Assert.AreEqual("name1: value1", result); } + + [TestMethod] + public async Task FormatAttributes_FileUpload_LinksToTheGivenHost() + { + var result = await FormatFileUpload(new DomainHost { Url = "https://shop.example/" }); + + StringAssert.Contains(result, "href=\"https://shop.example/download/getfileupload/?downloadId="); + } + + [TestMethod] + public async Task FormatAttributes_FileUploadWithoutHost_RendersNoLink() + { + var result = await FormatFileUpload(null); + + Assert.AreEqual("file: ", result); + } + + [TestMethod] + public async Task GetParseContactAttributes_ReadsTheAttributesOfTheGivenAccess() + { + var access = CatalogAccess.For(new Customer(), "store1"); + _contactAttributeServiceMock.Setup(c => c.GetAllContactAttributes(access)) + .ReturnsAsync(_contactAtr.Take(1).ToList()); + + var result = await _parser.GetParseContactAttributes( + [new CustomAttribute { Key = "key1", Value = " text " }], access); + + Assert.HasCount(1, result); + Assert.AreEqual("text", result[0].Value); + } + + private async Task FormatFileUpload(DomainHost host) + { + var attribute = new ContactAttribute { + Id = "file", Name = "file", TextPrompt = "download", AttributeControlType = AttributeControlType.FileUpload + }; + _contactAttributeServiceMock.Setup(c => c.GetContactAttributeById("file")).ReturnsAsync(attribute); + return await _parser.FormatAttributes(new Language(), + [new CustomAttribute { Key = "file", Value = Guid.NewGuid().ToString() }], new Customer(), host); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeServiceTests.cs b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeServiceTests.cs index 62c5404d2e..ae43ce5309 100644 --- a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeServiceTests.cs @@ -1,10 +1,9 @@ -using Grand.Business.Marketing.Services.Contacts; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Business.Marketing.Services.Contacts; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; using Grand.Domain.Messages; -using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Tests.Caching; @@ -21,21 +20,17 @@ public class ContactAttributeServiceTests private ContactAttributeService _contactAttributeService; private Mock _mediatorMock; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void Init() { _repository = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); var accessControlConfig = new AccessControlConfig(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); _contactAttributeService = new ContactAttributeService(_cacheBase, _repository, _mediatorMock.Object, - _workContextMock.Object, accessControlConfig); + accessControlConfig); } [TestMethod] @@ -64,12 +59,67 @@ public async Task GetAllContactAttributesTest() await _contactAttributeService.InsertContactAttribute(new ContactAttribute()); //Act - var result = await _contactAttributeService.GetAllContactAttributes(); + var result = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(3, result); } + [TestMethod] + public async Task GetAllContactAttributes_CustomersInDifferentGroups_SeeTheirOwnAttributes() + { + //Arrange + await _contactAttributeService.InsertContactAttribute(new ContactAttribute { Name = "public" }); + await _contactAttributeService.InsertContactAttribute(new ContactAttribute { + Name = "vip", LimitedToGroups = true, CustomerGroups = ["vip"] + }); + var vip = new Customer(); + vip.Groups.Add("vip"); + var guest = new Customer(); + guest.Groups.Add("guests"); + + //Act + var forVip = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.For(vip, "")); + var forGuest = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.For(guest, "")); + + //Assert + Assert.HasCount(2, forVip); + Assert.HasCount(1, forGuest); + Assert.AreEqual("public", forGuest[0].Name); + } + + [TestMethod] + public async Task GetAllContactAttributes_IncludingHidden_ReturnsGroupLimitedAttributes() + { + //Arrange + await _contactAttributeService.InsertContactAttribute(new ContactAttribute { + Name = "vip", LimitedToGroups = true, CustomerGroups = ["vip"] + }); + + //Act + var result = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.IncludingHidden()); + + //Assert + Assert.HasCount(1, result); + } + + [TestMethod] + public async Task GetAllContactAttributes_Store_LeavesOutAttributesOfOtherStores() + { + //Arrange + await _contactAttributeService.InsertContactAttribute(new ContactAttribute { + Name = "other", LimitedToStores = true, Stores = ["store2"] + }); + await _contactAttributeService.InsertContactAttribute(new ContactAttribute { Name = "all" }); + + //Act + var result = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.IncludingHidden("store1")); + + //Assert + Assert.HasCount(1, result); + Assert.AreEqual("all", result[0].Name); + } + [TestMethod] public async Task GetContactAttributeByIdTest() { diff --git a/src/Tests/Grand.Business.Marketing.Tests/Services/Customers/CustomerCoordinatesServiceTests.cs b/src/Tests/Grand.Business.Marketing.Tests/Services/Customers/CustomerCoordinatesServiceTests.cs index 5cfb417523..4d45a63fd9 100644 --- a/src/Tests/Grand.Business.Marketing.Tests/Services/Customers/CustomerCoordinatesServiceTests.cs +++ b/src/Tests/Grand.Business.Marketing.Tests/Services/Customers/CustomerCoordinatesServiceTests.cs @@ -3,8 +3,6 @@ using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; -using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -14,36 +12,27 @@ namespace Grand.Business.Marketing.Tests.Services.Customers; [TestClass] public class CustomerCoordinatesServiceTests { - private Customer _customer; private CustomerCoordinatesService _customerCoordinatesService; private Mock _mediatorMock; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void Init() { _repository = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _customer = new Customer(); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => _customer); - _customerCoordinatesService = - new CustomerCoordinatesService(_repository, _workContextMock.Object, _mediatorMock.Object); + _customerCoordinatesService = new CustomerCoordinatesService(_repository, _mediatorMock.Object); } [TestMethod] - public async Task GetGeoCoordinateTest_WorkContext() + public async Task GetGeoCoordinateTest_CustomerWithoutCoordinates_ReturnsZero() { - //Arrange - await _customerCoordinatesService.SaveGeoCoordinate(1, 2); //Act - var result = await _customerCoordinatesService.GetGeoCoordinate(); + var result = await _customerCoordinatesService.GetGeoCoordinate(new Customer()); //Assert - Assert.AreEqual(1, result.longitude); - Assert.AreEqual(2, result.latitude); + Assert.AreEqual(0, result.longitude); + Assert.AreEqual(0, result.latitude); } [TestMethod] @@ -73,15 +62,4 @@ public async Task SaveGeoCoordinateTest_Customer() Assert.AreEqual(2, _repository.Table.FirstOrDefault(x => x.Id == customer.Id).Coordinates.Y); _mediatorMock.Verify(c => c.Publish(It.IsAny(), default), Times.Once); } - - [TestMethod] - public async Task SaveGeoCoordinateTest_WorkContext() - { - //Act - await _customerCoordinatesService.SaveGeoCoordinate(1, 2); - //Assert - Assert.AreEqual(1, _workContextMock.Object.WorkContext.CurrentCustomer.Coordinates.X); - Assert.AreEqual(2, _workContextMock.Object.WorkContext.CurrentCustomer.Coordinates.Y); - _mediatorMock.Verify(c => c.Publish(It.IsAny(), default), Times.Once); - } } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Services/ContactAttributeViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ContactAttributeViewModelService.cs index fbdea8e259..5c091efeae 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ContactAttributeViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ContactAttributeViewModelService.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Messages; @@ -21,7 +22,7 @@ public class ContactAttributeViewModelService( public virtual async Task> PrepareContactAttributeListModel() { var contactAttributes = - await contactAttributeService.GetAllContactAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, true); + await contactAttributeService.GetAllContactAttributes(CatalogAccess.IncludingHidden(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)); return contactAttributes.Select(x => { var attributeModel = x.ToModel(); @@ -49,7 +50,7 @@ public virtual async Task PrepareConditionAttributes(ContactAttributeModel model EnableCondition = contactAttribute.ConditionAttribute.Any(), SelectedAttributeId = selectedAttribute != null ? selectedAttribute.Id : "", ConditionAttributes = - (await contactAttributeService.GetAllContactAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, true)) + (await contactAttributeService.GetAllContactAttributes(CatalogAccess.IncludingHidden(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId))) //ignore this attribute and non-combinable attributes .Where(x => x.Id != contactAttribute.Id && x.CanBeUsedAsCondition()) .Select(x => diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs index 81f66e0af4..b3359e6f37 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Web.Commands.Models.Contact; @@ -26,7 +27,7 @@ public ContactAttributeChangeCommandHandler(IContactAttributeService contactAttr var enabledAttributeIds = new List(); var disabledAttributeIds = new List(); - var attributes = await _contactAttributeService.GetAllContactAttributes(request.Store.Id); + var attributes = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.For(request.Customer, request.Store.Id)); foreach (var attribute in attributes) { var conditionMet = await _contactAttributeParser.IsConditionMet(attribute, customAttributes); @@ -43,7 +44,7 @@ public ContactAttributeChangeCommandHandler(IContactAttributeService contactAttr private async Task> ParseContactAttributes(ContactAttributeChangeCommand request) { var customAttributes = new List(); - var contactAttributes = await _contactAttributeService.GetAllContactAttributes(request.Store.Id); + var contactAttributes = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.For(request.Customer, request.Store.Id)); foreach (var attribute in contactAttributes) switch (attribute.AttributeControlType) { diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsCommandHandler.cs index f8f95eef60..9ac1531bdb 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsCommandHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -47,7 +48,7 @@ public async Task Handle(ContactUsCommand request, CancellationT { var model = new List(); - var contactAttributes = await _contactAttributeService.GetAllContactAttributes(request.Store.Id); + var contactAttributes = await _contactAttributeService.GetAllContactAttributes(CatalogAccess.For(request.Customer, request.Store.Id)); foreach (var attribute in contactAttributes) { var attributeModel = new ContactUsModel.ContactAttributeModel { diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs index f710d92e06..6d6bca8ee4 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs @@ -1,8 +1,8 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Marketing.Contacts; using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Common; -using Grand.Infrastructure; using Grand.SharedKernel.Extensions; using Grand.Web.Commands.Models.Contact; using Grand.Web.Models.Contact; @@ -16,15 +16,13 @@ public class ContactUsSendCommandHandler : IRequestHandler Handle(ContactUsSendCommand request, CancellationToken cancellationToken) { var attributes = await _contactAttributeParser.GetParseContactAttributes(request.Model.Attributes - .Select(x => new CustomAttribute { Key = x.Key, Value = x.Value }).ToList()); + .Select(x => new CustomAttribute { Key = x.Key, Value = x.Value }).ToList(), + CatalogAccess.For(request.Customer, request.Store.Id)); request.Model.ContactAttribute = attributes; request.Model.ContactAttributeInfo = - await _contactAttributeParser.FormatAttributes(_contextAccessor.WorkContext.WorkingLanguage, attributes, - _contextAccessor.WorkContext.CurrentCustomer); - request.Model = await SendContactUs(request, _contextAccessor.StoreContext.CurrentStore); + await _contactAttributeParser.FormatAttributes(request.Language, attributes, request.Customer, + request.Host); + request.Model = await SendContactUs(request); return request.Model; } - private async Task SendContactUs(ContactUsSendCommand request, Domain.Stores.Store store) + private async Task SendContactUs(ContactUsSendCommand request) { var subject = _commonSettings.SubjectFieldOnContactUsForm ? request.Model.Subject : null; var body = FormatText.ConvertText(request.Model.Enquiry); await _messageProviderService.SendContactUsMessage - (_contextAccessor.WorkContext.CurrentCustomer, store, _contextAccessor.WorkContext.WorkingLanguage.Id, request.Model.Email.Trim(), + (request.Customer, request.Store, request.Language.Id, request.Model.Email.Trim(), request.Model.FullName, subject, body, request.Model.ContactAttributeInfo, request.Model.ContactAttribute, request.IpAddress); request.Model.SuccessfullySent = true; - request.Model.Result = _translationService.GetResource("ContactUs.YourEnquiryHasBeenSent"); + request.Model.Result = _translationService.GetResource("ContactUs.YourEnquiryHasBeenSent", request.Language.Id); return request.Model; } diff --git a/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs b/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs index 57e33054e1..1332f6f8a1 100644 --- a/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs @@ -1,10 +1,17 @@ -using Grand.Web.Models.Contact; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; +using Grand.Web.Models.Contact; using Grand.Mediator; namespace Grand.Web.Commands.Models.Contact; public class ContactUsSendCommand : IRequest { + public Customer Customer { get; set; } + public Domain.Stores.Store Store { get; set; } + public Language Language { get; set; } + public DomainHost Host { get; set; } public ContactUsModel Model { get; set; } public string IpAddress { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Controllers/ContactController.cs b/src/Web/Grand.Web/Controllers/ContactController.cs index 4a3a5bc810..8886a0e323 100644 --- a/src/Web/Grand.Web/Controllers/ContactController.cs +++ b/src/Web/Grand.Web/Controllers/ContactController.cs @@ -76,6 +76,10 @@ public virtual async Task> Index( if (ModelState.IsValid) { var result = await _mediator.Send(new ContactUsSendCommand { + Customer = _contextAccessor.WorkContext.CurrentCustomer, + Store = _contextAccessor.StoreContext.CurrentStore, + Language = _contextAccessor.WorkContext.WorkingLanguage, + Host = _contextAccessor.StoreContext.CurrentHost, Model = model, IpAddress = HttpContext?.Connection?.RemoteIpAddress?.ToString() }); diff --git a/src/Web/Grand.Web/Validators/Contact/ContactUsValidator.cs b/src/Web/Grand.Web/Validators/Contact/ContactUsValidator.cs index e9a54cb810..5428a23b22 100644 --- a/src/Web/Grand.Web/Validators/Contact/ContactUsValidator.cs +++ b/src/Web/Grand.Web/Validators/Contact/ContactUsValidator.cs @@ -2,12 +2,14 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Infrastructure; using Grand.Infrastructure.Models; using Grand.Infrastructure.Validators; using Grand.SharedKernel.Captcha; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Security.Captcha; using Grand.Web.Common.Validators; using Grand.Web.Models.Contact; @@ -60,7 +62,7 @@ public ContactUsValidator( { var contactAttributeWarnings = await GetContactAttributesWarnings( x.Attributes.Select(z => new CustomAttribute { Key = z.Key, Value = z.Value }).ToList(), - contextAccessor.StoreContext.CurrentStore.Id); + contextAccessor.CatalogAccess()); if (contactAttributeWarnings.Any()) foreach (var item in contactAttributeWarnings) context.AddFailure(item); @@ -68,7 +70,7 @@ public ContactUsValidator( } private async Task> GetContactAttributesWarnings(IList customAttributes, - string storeId) + CatalogAccess access) { var warnings = new List(); @@ -76,7 +78,7 @@ private async Task> GetContactAttributesWarnings(IList Date: Sat, 17 Oct 2026 17:37:58 +0200 Subject: [PATCH 3/4] Resolve every message link host from the store's primary domain Store.PrimaryHost() replaces the private StoreHost helper in MessageProviderService and tolerates a store saved without domains. The contact form and order placement now use it too, so file upload links in a contact e-mail and in saved order attribute descriptions point to the same host as the rest of the message. Without a host, a contact file upload is named with its download id instead of being dropped. MessageProviderService resolves the host once per message, and IContactAttributeParser.FormatAttributes no longer takes the unused customer. --- .../Contacts/IContactAttributeParser.cs | 4 +- .../Contacts/ContactAttributeParser.cs | 23 ++- .../Services/MessageProviderService.cs | 152 ++++++++++-------- .../Grand.Domain/Stores/StoreExtensions.cs | 15 ++ .../Contacts/ContactAttributeParserTests.cs | 14 +- .../Stores/StoreExtensionsTests.cs | 22 +++ .../Contact/ContactUsSendCommandHandler.cs | 4 +- .../Models/Contact/ContactUsSendCommand.cs | 1 - .../Controllers/CheckoutController.cs | 3 +- .../Controllers/ContactController.cs | 1 - 10 files changed, 150 insertions(+), 89 deletions(-) diff --git a/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs b/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs index 741e1969d3..a654691ba8 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Marketing/Contacts/IContactAttributeParser.cs @@ -57,8 +57,7 @@ IList AddContactAttribute(IList customAttribut /// /// Language /// Attributes - /// Customer - /// Store host the file upload links point to; without one no link is rendered + /// Store host the file upload links point to; without one the file is named but not linked /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to HTML hyperlink tags could be rendered (if required) @@ -66,7 +65,6 @@ IList AddContactAttribute(IList customAttribut Task FormatAttributes( Language language, IList customAttributes, - Customer customer, DomainHost host, string separator = "
", bool htmlEncode = true, diff --git a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs index 454ec972ae..b98d8880ca 100644 --- a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs +++ b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs @@ -157,8 +157,7 @@ public virtual IList RemoveContactAttribute(IList /// Language /// Attributes - /// Customer - /// Store host the file upload links point to; without one no link is rendered + /// Store host the file upload links point to; without one the file is named but not linked /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to HTML hyperlink tags could be rendered (if required) @@ -166,7 +165,6 @@ public virtual IList RemoveContactAttribute(IList FormatAttributes( Language language, IList customAttributes, - Customer customer, DomainHost host, string separator = "
", bool htmlEncode = true, @@ -208,12 +206,21 @@ public virtual async Task FormatAttributes( { var attributeText = string.Empty; var attributeName = attribute.GetTranslation(a => a.Name, language.Id); - if (allowHyperlinks && host != null) + if (allowHyperlinks) { - var downloadLink = - $"{host.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}"; - attributeText = - $"{attribute.GetTranslation(a => a.TextPrompt, language.Id)}"; + var textPrompt = attribute.GetTranslation(a => a.TextPrompt, language.Id); + if (host != null) + { + var downloadLink = + $"{host.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}"; + attributeText = + $"{textPrompt}"; + } + else + { + //without a host the file cannot be linked, so name it instead of dropping it + attributeText = $"{textPrompt} ({downloadGuid})"; + } } formattedAttribute = $"{attributeName}: {attributeText}"; diff --git a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs index c94c8395d4..ab0cb3c3ef 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs @@ -68,19 +68,6 @@ public MessageProviderService( #region Utilities - /// - /// Gets the host the links of a message point to: the store's primary domain, so a message links to the - /// store it is about whoever sends it, in a request to any host or in a background task - /// - protected static DomainHost StoreHost(Store store) - { - return store.Domains.FirstOrDefault(x => x.Primary) ?? store.Domains.FirstOrDefault() ?? new DomainHost { - Id = int.MinValue.ToString(), - Url = store.Url, - HostName = "temporary-store" - }; - } - protected virtual async Task GetStore(string storeId) { return await _storeService.GetStoreById(storeId) ?? (await _storeService.GetAllStores()).FirstOrDefault(); @@ -126,11 +113,12 @@ protected virtual async Task EnsureLanguageIsActive(string languageId, return language; } - private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer customer, Store store, Language language) + private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer customer, Store store, DomainHost host, + Language language) { if (customer != null) { - builder.AddCustomerTokens(customer, store, StoreHost(store), language); + builder.AddCustomerTokens(customer, store, host, language); } } @@ -163,9 +151,10 @@ protected virtual async Task SendCustomerMessage(Customer customer, Store s //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language, customerNote); + .AddCustomerTokens(customer, store, host, language, customerNote); var liquidObject = await builder.BuildAsync(); //event notification @@ -329,10 +318,11 @@ private async Task SendOrderStoreOwnerMessage(string template, Order order, //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var liquidBuilder = new LiquidObjectBuilder(_mediator); liquidBuilder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, StoreHost(store)); - AddCustomerTokensIfNotNull(liquidBuilder, customer, store, language); + .AddOrderTokens(order, customer, store, host); + AddCustomerTokensIfNotNull(liquidBuilder, customer, store, host, language); var liquidObject = await liquidBuilder.BuildAsync(); //event notification @@ -460,10 +450,11 @@ private async Task SendOrderCustomerMessage(string message, Order order, Cu //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, StoreHost(store)); - AddCustomerTokensIfNotNull(builder, customer, store, language); + .AddOrderTokens(order, customer, store, host); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -541,11 +532,12 @@ private async Task SendOrderVendorMessage(string message, Order order, Vend var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, StoreHost(store), vendor: vendor); + .AddOrderTokens(order, customer, store, host, vendor: vendor); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -606,12 +598,13 @@ private async Task SendShipmentCustomerMessage(string message, Shipment shi var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddShipmentTokens(shipment, order, store, StoreHost(store), language) - .AddOrderTokens(order, customer, store, StoreHost(store)); + .AddShipmentTokens(shipment, order, store, host, language) + .AddOrderTokens(order, customer, store, host); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -649,11 +642,12 @@ public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, StoreHost(store), orderNote); + .AddOrderTokens(order, customer, store, host, orderNote); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -716,9 +710,10 @@ private async Task SendNewsLetterSubscriptionMessage(string message, NewsLe //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddNewsLetterSubscriptionTokens(subscription, store, StoreHost(store)); + .AddNewsLetterSubscriptionTokens(subscription, store, host); var liquidObject = await builder.BuildAsync(); //event notification @@ -761,11 +756,12 @@ public virtual async Task SendProductEmailAFriendMessage(Customer customer, //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail) - .AddCustomerTokens(customer, store, StoreHost(store), language) - .AddProductTokens(product, language, store, StoreHost(store)); + .AddCustomerTokens(customer, store, host, language) + .AddProductTokens(product, language, store, host); var liquidObject = await builder.BuildAsync(); //event notification @@ -804,9 +800,10 @@ public virtual async Task SendWishlistEmailAFriendMessage(Customer customer var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddCustomerTokens(customer, store, host, language) .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail); var liquidObject = await builder.BuildAsync(); @@ -842,10 +839,11 @@ public virtual async Task SendProductQuestionMessage(Customer customer, Sto //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language) - .AddProductTokens(product, language, store, StoreHost(store)); + .AddCustomerTokens(customer, store, host, language) + .AddProductTokens(product, language, store, host); var liquidObject = await builder.BuildAsync(); liquidObject.AskQuestion = new LiquidAskQuestion(message, customerEmail, fullName, phone); @@ -921,12 +919,13 @@ public virtual async Task SendNewMerchandiseReturnStoreOwnerMessage(Merchan var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language); + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); var liquidObject = await builder.BuildAsync(); @@ -978,12 +977,13 @@ public virtual async Task SendMerchandiseReturnStatusChangedCustomerMessage var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language); + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1020,11 +1020,12 @@ public virtual async Task SendNewMerchandiseReturnCustomerMessage(Merchandi var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language); + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1062,11 +1063,12 @@ public virtual async Task SendNewMerchandiseReturnNoteAddedCustomerMessage( var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, StoreHost(store), order, language, + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language, merchandiseReturnNote); var liquidObject = await builder.BuildAsync(); //event notification @@ -1106,8 +1108,9 @@ public virtual async Task SendNewVendorAccountApplyStoreOwnerMessage(Custom //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator).AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddCustomerTokens(customer, store, host, language) .AddVendorTokens(vendor, language); var liquidObject = await builder.BuildAsync(); @@ -1215,10 +1218,11 @@ public virtual async Task SendProductReviewMessage(Product product, Product var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = productReview.CustomerId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddProductReviewTokens(product, productReview); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -1257,10 +1261,11 @@ public virtual async Task SendVendorReviewMessage(VendorReview vendorReview //vendor var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = vendorReview.VendorId }); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddVendorReviewTokens(vendor, vendorReview); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); builder.AddVendorTokens(vendor, language); var liquidObject = await builder.BuildAsync(); @@ -1295,9 +1300,10 @@ public virtual async Task SendQuantityBelowStoreOwnerMessage(Product produc //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddProductTokens(product, language, store, StoreHost(store)); + .AddProductTokens(product, language, store, host); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1332,10 +1338,11 @@ public virtual async Task SendQuantityBelowStoreOwnerMessage(Product produc //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddProductTokens(product, language, store, StoreHost(store)) - .AddAttributeCombinationTokens(product, combination, language, StoreHost(store)); + .AddProductTokens(product, language, store, host) + .AddAttributeCombinationTokens(product, combination, language, host); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1367,9 +1374,10 @@ public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer cust //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language); + .AddCustomerTokens(customer, store, host, language); var liquidObject = await builder.BuildAsync(); @@ -1398,13 +1406,14 @@ public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogCom //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddBlogCommentTokens(blogPost, blogComment, store, StoreHost(store), language); + .AddBlogCommentTokens(blogPost, blogComment, store, host, language); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = blogComment.CustomerId }); if (customer != null && await _groupService.IsRegistered(customer)) - builder.AddCustomerTokens(customer, store, StoreHost(store), language); + builder.AddCustomerTokens(customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -1439,13 +1448,14 @@ public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle ar //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddArticleCommentTokens(article, articleComment, store, StoreHost(store), language); + .AddArticleCommentTokens(article, articleComment, store, host, language); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = articleComment.CustomerId }); if (customer != null && await _groupService.IsRegistered(customer)) - builder.AddCustomerTokens(customer, store, StoreHost(store), language); + builder.AddCustomerTokens(customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1477,11 +1487,12 @@ public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsCom //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddNewsCommentTokens(newsItem, newsComment, store, StoreHost(store), language); + .AddNewsCommentTokens(newsItem, newsComment, store, host, language); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = newsComment.CustomerId }); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification @@ -1516,11 +1527,12 @@ public virtual async Task SendBackInStockMessage(Customer customer, Product //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, language); + AddCustomerTokensIfNotNull(builder, customer, store, host, language); - builder.AddOutOfStockTokens(product, subscription, store, StoreHost(store), language); + builder.AddOutOfStockTokens(product, subscription, store, host, language); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); @@ -1575,9 +1587,10 @@ public virtual async Task SendContactUsMessage(Customer customer, Store sto fromName = senderName; } + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language); + .AddCustomerTokens(customer, store, host, language); var liquidObject = await builder.BuildAsync(); liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, attrInfo); @@ -1654,9 +1667,10 @@ public virtual async Task SendContactVendorMessage(Customer customer, Store fromName = senderName; } + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, StoreHost(store), language); + .AddCustomerTokens(customer, store, host, language); var liquidObject = await builder.BuildAsync(); liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, ""); //event notification @@ -1708,10 +1722,11 @@ public virtual async Task SendAuctionWinEndedCustomerMessage(Product produc var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, StoreHost(store), language) - .AddProductTokens(product, language, store, StoreHost(store)) + .AddCustomerTokens(customer, store, host, language) + .AddProductTokens(product, language, store, host) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); @@ -1743,9 +1758,10 @@ public virtual async Task SendAuctionEndedLostCustomerMessage(Product produ var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddAuctionTokens(product, bid) - .AddProductTokens(product, language, store, StoreHost(store)) + .AddProductTokens(product, language, store, host) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); @@ -1757,7 +1773,7 @@ public virtual async Task SendAuctionEndedLostCustomerMessage(Product produ var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); if (string.IsNullOrEmpty(languageId)) languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - builder2.AddCustomerTokens(customer, store, StoreHost(store), language); + builder2.AddCustomerTokens(customer, store, host, language); var liquidObject2 = await builder2.BuildAsync(); //event notification @@ -1788,8 +1804,9 @@ public virtual async Task SendAuctionEndedBinCustomerMessage(Product produc var language = await EnsureLanguageIsActive(languageId, store.Id); var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); - builder.AddProductTokens(product, language, store, StoreHost(store)) + builder.AddProductTokens(product, language, store, host) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); @@ -1804,7 +1821,7 @@ public virtual async Task SendAuctionEndedBinCustomerMessage(Product produc if (string.IsNullOrEmpty(languageId)) languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - AddCustomerTokensIfNotNull(builder2, customer, store, language); + AddCustomerTokensIfNotNull(builder2, customer, store, host, language); var liquidObject2 = await builder2.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject2); @@ -1845,7 +1862,7 @@ public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddCustomerTokens(customer, store, store.PrimaryHost(), language) .AddStoreTokens(store, language, emailAccount); } else @@ -1857,7 +1874,7 @@ public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product return 0; emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - builder.AddProductTokens(product, language, store, StoreHost(store)); + builder.AddProductTokens(product, language, store, store.PrimaryHost()); } var liquidObject = await builder.BuildAsync(); @@ -1897,9 +1914,10 @@ public virtual async Task SendOutBidCustomerMessage(Product product, string var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + var host = store.PrimaryHost(); var builder = new LiquidObjectBuilder(_mediator); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, StoreHost(store), language) + .AddCustomerTokens(customer, store, host, language) .AddStoreTokens(store, language, emailAccount); var liquidObject = await builder.BuildAsync(); diff --git a/src/Core/Grand.Domain/Stores/StoreExtensions.cs b/src/Core/Grand.Domain/Stores/StoreExtensions.cs index cf5f7d08d2..27cc2c99e3 100644 --- a/src/Core/Grand.Domain/Stores/StoreExtensions.cs +++ b/src/Core/Grand.Domain/Stores/StoreExtensions.cs @@ -33,4 +33,19 @@ public static DomainHost HostValue(this Store store, string host) ? null : store.Domains.FirstOrDefault(x => x.HostName.Equals(host, StringComparison.OrdinalIgnoreCase)); } + + /// + /// Return the host links to a store point to: its primary domain, else its first domain, else its url + /// + /// Store + /// DomainHost + public static DomainHost PrimaryHost(this Store store) + { + ArgumentNullException.ThrowIfNull(store); + return store.Domains?.FirstOrDefault(x => x.Primary) ?? store.Domains?.FirstOrDefault() ?? new DomainHost { + Id = int.MinValue.ToString(), + Url = store.Url, + HostName = "temporary-store" + }; + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs index 377fca0eb7..14064ad672 100644 --- a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs +++ b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs @@ -124,7 +124,7 @@ public async Task FormatAttributesTest() var result = await _parser.FormatAttributes(new Language(), new List { new() { Key = "key1", Value = "value1" } - }, new Customer(), null); + }, null); Assert.AreEqual("name1: value1", result); } @@ -137,11 +137,13 @@ public async Task FormatAttributes_FileUpload_LinksToTheGivenHost() } [TestMethod] - public async Task FormatAttributes_FileUploadWithoutHost_RendersNoLink() + public async Task FormatAttributes_FileUploadWithoutHost_NamesTheFileWithoutALink() { - var result = await FormatFileUpload(null); + var downloadGuid = Guid.NewGuid(); - Assert.AreEqual("file: ", result); + var result = await FormatFileUpload(null, downloadGuid); + + Assert.AreEqual($"file: download ({downloadGuid})", result); } [TestMethod] @@ -158,13 +160,13 @@ public async Task GetParseContactAttributes_ReadsTheAttributesOfTheGivenAccess() Assert.AreEqual("text", result[0].Value); } - private async Task FormatFileUpload(DomainHost host) + private async Task FormatFileUpload(DomainHost host, Guid? downloadGuid = null) { var attribute = new ContactAttribute { Id = "file", Name = "file", TextPrompt = "download", AttributeControlType = AttributeControlType.FileUpload }; _contactAttributeServiceMock.Setup(c => c.GetContactAttributeById("file")).ReturnsAsync(attribute); return await _parser.FormatAttributes(new Language(), - [new CustomAttribute { Key = "file", Value = Guid.NewGuid().ToString() }], new Customer(), host); + [new CustomAttribute { Key = "file", Value = (downloadGuid ?? Guid.NewGuid()).ToString() }], host); } } \ No newline at end of file diff --git a/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs b/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs index fc609aedf9..196e59f858 100644 --- a/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs +++ b/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs @@ -28,6 +28,28 @@ public void ContainsHostValueTest_False() Assert.IsFalse(store.ContainsHostValue("google")); } + [TestMethod] + public void PrimaryHost_ReturnsThePrimaryDomain() + { + store.Domains[1].Primary = true; + + Assert.AreEqual("yahoo.com", store.PrimaryHost().HostName); + } + + [TestMethod] + public void PrimaryHost_WithoutAPrimaryDomain_ReturnsTheFirstDomain() + { + Assert.AreEqual("google.com", store.PrimaryHost().HostName); + } + + [TestMethod] + public void PrimaryHost_WithoutDomains_FallsBackToTheStoreUrl() + { + var withoutDomains = new Store { Url = "https://shop.example/", Domains = null }; + + Assert.AreEqual("https://shop.example/", withoutDomains.PrimaryHost().Url); + } + [TestMethod] public void HostValueTest_NotNull() { diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs index 6d6bca8ee4..6458afb17b 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Common; +using Grand.Domain.Stores; using Grand.SharedKernel.Extensions; using Grand.Web.Commands.Models.Contact; using Grand.Web.Models.Contact; @@ -37,8 +38,7 @@ public async Task Handle(ContactUsSendCommand request, Cancellat request.Model.ContactAttribute = attributes; request.Model.ContactAttributeInfo = - await _contactAttributeParser.FormatAttributes(request.Language, attributes, request.Customer, - request.Host); + await _contactAttributeParser.FormatAttributes(request.Language, attributes, request.Store.PrimaryHost()); request.Model = await SendContactUs(request); return request.Model; diff --git a/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs b/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs index 1332f6f8a1..21d38303f8 100644 --- a/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs @@ -11,7 +11,6 @@ public class ContactUsSendCommand : IRequest public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } public Language Language { get; set; } - public DomainHost Host { get; set; } public ContactUsModel Model { get; set; } public string IpAddress { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 3494ce8927..f7b19ed172 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -14,6 +14,7 @@ using Grand.Domain.Orders; using Grand.Domain.Payments; using Grand.Domain.Shipping; +using Grand.Domain.Stores; using Grand.Infrastructure.Configuration; using Grand.Infrastructure; using Grand.Infrastructure.Extensions; @@ -723,7 +724,7 @@ public virtual async Task ConfirmOrder() var placeOrderResult = await _mediator.Send(new PlaceOrderCommand { Context = _contextAccessor.CalculationContext(), Language = _contextAccessor.WorkContext.WorkingLanguage, - Host = _contextAccessor.StoreContext.CurrentHost, + Host = _contextAccessor.StoreContext.CurrentStore.PrimaryHost(), OriginalCustomerIfImpersonated = _contextAccessor.WorkContext.OriginalCustomerIfImpersonated }); if (placeOrderResult.Success) diff --git a/src/Web/Grand.Web/Controllers/ContactController.cs b/src/Web/Grand.Web/Controllers/ContactController.cs index 8886a0e323..2ad13fd569 100644 --- a/src/Web/Grand.Web/Controllers/ContactController.cs +++ b/src/Web/Grand.Web/Controllers/ContactController.cs @@ -79,7 +79,6 @@ public virtual async Task> Index( Customer = _contextAccessor.WorkContext.CurrentCustomer, Store = _contextAccessor.StoreContext.CurrentStore, Language = _contextAccessor.WorkContext.WorkingLanguage, - Host = _contextAccessor.StoreContext.CurrentHost, Model = model, IpAddress = HttpContext?.Connection?.RemoteIpAddress?.ToString() }); From 31879050bf3aafccd1f057eafc48f85527e4604d Mon Sep 17 00:00:00 2001 From: support Date: Sat, 17 Oct 2026 18:43:21 +0200 Subject: [PATCH 4/4] Link storefront-triggered message e-mails to the request's host Messages sent from a storefront request now receive the current host, and their links use it when it is one of the store's domains; any other or missing host falls back to the store's primary domain. --- .../Orders/OrderNotificationCommandHandler.cs | 8 +- .../Orders/PlaceOrderCommandHandler.cs | 3 +- .../Common/OrderNotificationCommand.cs | 6 + .../Messages/IMessageProviderService.cs | 136 +++++++++-- .../Services/MessageProviderService.cs | 228 +++++++++++++----- .../Grand.Domain/Stores/StoreExtensions.cs | 16 ++ .../FacebookAuthenticationEventConsumer.cs | 7 +- .../GoogleAuthenticationEventConsumer.cs | 7 +- .../Stores/StoreExtensionsTests.cs | 17 ++ .../Blogs/InsertBlogCommentCommandHandler.cs | 2 +- .../Contact/ContactUsSendCommandHandler.cs | 6 +- .../CustomerRegisteredCommandHandler.cs | 2 +- .../Customers/DeleteAccountCommandHandler.cs | 2 +- .../PasswordRecoverySendCommandHandler.cs | 2 +- .../News/InsertNewsCommentCommandHandler.cs | 2 +- .../Newsletter/SubscribeNewsletterHandler.cs | 8 +- .../Orders/InsertOrderNoteCommandHandler.cs | 3 +- .../MerchandiseReturnSubmitCommandHandler.cs | 4 +- .../InsertProductReviewCommandHandler.cs | 2 +- ...ProductAskQuestionMessageCommandHandler.cs | 2 +- ...roductEmailAFriendMessageCommandHandler.cs | 2 +- .../Vendors/ContactVendorCommandHandler.cs | 3 +- .../InsertVendorReviewCommandHandler.cs | 2 +- .../Models/Contact/ContactUsSendCommand.cs | 1 + .../Customers/CustomerRegisteredCommand.cs | 1 + .../Models/Customers/DeleteAccountCommand.cs | 1 + .../Customers/PasswordRecoverySendCommand.cs | 1 + .../Models/Orders/InsertOrderNoteCommand.cs | 1 + .../Products/InsertProductReviewCommand.cs | 1 + .../SendProductAskQuestionMessageCommand.cs | 1 + .../SendProductEmailAFriendMessageCommand.cs | 1 + .../Controllers/AccountController.cs | 14 +- .../Controllers/CheckoutController.cs | 2 +- .../Controllers/ContactController.cs | 1 + .../Controllers/KnowledgebaseController.cs | 2 +- .../Grand.Web/Controllers/OrderController.cs | 6 +- .../Controllers/ProductController.cs | 3 + .../Grand.Web/Controllers/VendorController.cs | 3 +- .../Controllers/WishlistController.cs | 3 +- .../GetTwoFactorAuthenticationHandler.cs | 2 +- .../Customers/GetTwoFactorAuthentication.cs | 1 + 41 files changed, 392 insertions(+), 123 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs index 4013d2b751..dd5618da91 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs @@ -60,7 +60,7 @@ await _orderService.InsertOrderNote(new OrderNote { //send email notifications await _messageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.Customer, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, request.Host); string orderPlacedAttachmentFilePath = string.Empty, orderPlacedAttachmentFileName = string.Empty; var orderPlacedAttachments = new List(); @@ -87,14 +87,16 @@ await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguag _logger.LogError(ex, "Error - order placed attachment file {OrderOrderNumber}", request.Order.OrderNumber); } - await _messageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer, request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName, orderPlacedAttachments); + await _messageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer, + request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName, + orderPlacedAttachments, request.Host); if (request.Order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId))) { var vendors = await _mediator.Send(new GetVendorsInOrderQuery { Order = request.Order }); foreach (var vendor in vendors) await _messageProviderService.SendOrderPlacedVendorMessage(request.Order, request.Customer, vendor, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, request.Host); } } catch (Exception e) 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 01527bb29d..389098fd66 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -184,7 +184,8 @@ await UpdatePaymentTransaction(processPayment.paymentTransaction, result.PlacedO await _mediator.Send(new OrderNotificationCommand { Order = result.PlacedOrder, Customer = details.Customer, - OriginalCustomerIfImpersonated = command.OriginalCustomerIfImpersonated + OriginalCustomerIfImpersonated = command.OriginalCustomerIfImpersonated, + Host = command.Host }, cancellationToken); //check order status diff --git a/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs b/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs index e451bac685..85bdf36387 100644 --- a/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs @@ -1,5 +1,6 @@ using Grand.Domain.Customers; using Grand.Domain.Orders; +using Grand.Domain.Stores; using Grand.Mediator; namespace Grand.Business.Core.Commands.Messages.Common; @@ -17,4 +18,9 @@ public class OrderNotificationCommand : IRequest /// Staff member who placed the order while impersonating the customer, if any ///
public Customer OriginalCustomerIfImpersonated { get; set; } + + /// + /// Host of the storefront request that placed the order; the messages link to it + /// + public DomainHost Host { get; set; } } diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs index aa9ec68a40..f28f90038a 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs @@ -23,7 +23,10 @@ public interface IMessageProviderService /// Store /// Message language identifier /// Queued email identifier - Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, DomainHost host = null); /// /// Sends a welcome message to a customer @@ -32,7 +35,10 @@ public interface IMessageProviderService /// Store /// Message language identifier /// Queued email identifier - Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId, DomainHost host = null); /// /// Sends an email validation message to a customer @@ -41,7 +47,11 @@ public interface IMessageProviderService /// Store /// Message language identifier /// Queued email identifier - Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId, + DomainHost host = null); /// /// Sends password recovery message to a customer @@ -50,7 +60,11 @@ public interface IMessageProviderService /// Store /// Message language identifier /// Queued email identifier - Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, string languageId, + DomainHost host = null); /// /// Sends a new customer note added notification to a customer @@ -69,7 +83,11 @@ public interface IMessageProviderService /// Store instance /// Message language identifier /// Queued email identifier - Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, string languageId, + DomainHost host = null); #endregion @@ -83,7 +101,11 @@ public interface IMessageProviderService /// Vendor instance /// Message language identifier /// Queued email identifier - Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, string languageId, + DomainHost host = null); /// /// Sends an order placed notification to a store owner @@ -92,7 +114,11 @@ public interface IMessageProviderService /// Customer instance /// Message language identifier /// Queued email identifier - Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId, + DomainHost host = null); /// /// Sends an order paid notification to a store owner @@ -141,8 +167,12 @@ Task SendOrderPaidCustomerMessage(Order order, Customer customer, string la /// /// Attachments ident /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null); + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, + DomainHost host = null); /// /// Sends a shipment sent notification to a customer @@ -227,7 +257,10 @@ Task SendOrderCompletedCustomerMessage(Order order, Customer customer, stri /// Order /// Order note /// Queued email identifier - Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote, DomainHost host = null); #endregion @@ -239,8 +272,11 @@ Task SendOrderCompletedCustomerMessage(Order order, Customer customer, stri /// Newsletter subscription /// Language identifier /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - string languageId); + string languageId, DomainHost host = null); /// /// Sends a newsletter subscription deactivation message @@ -248,8 +284,11 @@ Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription sub /// Newsletter subscription /// Language identifier /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - string languageId); + string languageId, DomainHost host = null); #endregion @@ -266,8 +305,11 @@ Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription s /// Friend's email /// Personal message /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendProductEmailAFriendMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string friendsEmail, string personalMessage); + Product product, string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null); /// /// Sends wishlist "email a friend" message @@ -279,16 +321,23 @@ Task SendProductEmailAFriendMessage(Customer customer, Store store, string /// Friend's email /// Personal message /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendWishlistEmailAFriendMessage(Customer customer, Store store, string languageId, - string customerEmail, string friendsEmail, string personalMessage); + string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null); /// /// Sends "email a friend" message /// /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendProductQuestionMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string fullName, string phone, string message, string ipaddress); + Product product, string customerEmail, string fullName, string phone, string message, string ipaddress, + DomainHost host = null); #endregion @@ -301,8 +350,11 @@ Task SendProductQuestionMessage(Customer customer, Store store, string lang /// Order /// Message language identifier /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendNewMerchandiseReturnStoreOwnerMessage(MerchandiseReturn merchandiseReturn, Order order, - string languageId); + string languageId, DomainHost host = null); /// @@ -322,8 +374,11 @@ Task SendMerchandiseReturnStatusChangedCustomerMessage(MerchandiseReturn me /// /// /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendNewMerchandiseReturnCustomerMessage(MerchandiseReturn merchandiseReturn, Order order, - string languageId); + string languageId, DomainHost host = null); /// /// Sends a new merchandise return note added notification to a customer @@ -347,8 +402,11 @@ Task SendNewMerchandiseReturnNoteAddedCustomerMessage(MerchandiseReturn mer /// Store /// Message language identifier /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor vendor, Store store, - string languageId); + string languageId, DomainHost host = null); /// @@ -368,7 +426,11 @@ Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor v /// Store /// Message language identifier /// Queued email identifier - Task SendProductReviewMessage(Product product, ProductReview productReview, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendProductReviewMessage(Product product, ProductReview productReview, Store store, string languageId, + DomainHost host = null); /// /// Sends a vendor review notification message to a store owner @@ -377,7 +439,11 @@ Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor v /// Store /// Message language identifier /// Queued email identifier - Task SendVendorReviewMessage(VendorReview vendorReview, Store store, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendVendorReviewMessage(VendorReview vendorReview, Store store, string languageId, + DomainHost host = null); /// /// Sends a gift voucher notification @@ -413,13 +479,20 @@ Task SendQuantityBelowStoreOwnerMessage(Product product, ProductAttributeCo /// Customer /// Message language identifier /// Queued email identifier - Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId, DomainHost host = null); /// /// Sends a blog comment notification message to a store owner /// /// Queued email identifier - Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId, + DomainHost host = null); /// /// Sends an article comment notification message to a store owner @@ -428,8 +501,11 @@ Task SendQuantityBelowStoreOwnerMessage(Product product, ProductAttributeCo /// Article comment /// Message language identifier /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendArticleCommentMessage(KnowledgebaseArticle article, KnowledgebaseArticleComment articleComment, - string languageId); + string languageId, DomainHost host = null); /// /// Sends a news comment notification message to a store owner @@ -438,7 +514,11 @@ Task SendArticleCommentMessage(KnowledgebaseArticle article, KnowledgebaseA /// News comment /// Message language identifier /// Queued email identifier - Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId); + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId, + DomainHost host = null); /// /// Sends a 'Back in stock' notification message to a customer @@ -466,9 +546,12 @@ Task SendBackInStockMessage(Customer customer, Product product, OutOfStockS /// Custom Attributes /// ip address /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendContactUsMessage(Customer customer, Store store, string languageId, string senderEmail, string senderName, string subject, string body, string attrInfo, IList customAttributes, - string ipaddress); + string ipaddress, DomainHost host = null); /// /// Sends "contact vendor" message @@ -483,8 +566,11 @@ Task SendContactUsMessage(Customer customer, Store store, string languageId /// Email body /// Ip Address /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// Task SendContactVendorMessage(Customer customer, Store store, Vendor vendor, string languageId, - string senderEmail, string senderName, string subject, string body, string ipaddress); + string senderEmail, string senderName, string subject, string body, string ipaddress, DomainHost host = null); /// /// Sends auction ended notification to a customer (winner) diff --git a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs index ab0cb3c3ef..d73e19d7b9 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs @@ -113,8 +113,8 @@ protected virtual async Task EnsureLanguageIsActive(string languageId, return language; } - private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer customer, Store store, DomainHost host, - Language language) + private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer customer, Store store, + DomainHost host, Language language) { if (customer != null) { @@ -137,8 +137,11 @@ private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer cu /// Message template name /// Send email to email account /// Customer note + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// protected virtual async Task SendCustomerMessage(Customer customer, Store store, string languageId, - string templateName, bool toEmailAccount = false, CustomerNote customerNote = null) + string templateName, bool toEmailAccount = false, CustomerNote customerNote = null, DomainHost host = null) { ArgumentNullException.ThrowIfNull(customer); @@ -151,7 +154,7 @@ protected virtual async Task SendCustomerMessage(Customer customer, Store s //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language, customerNote); @@ -174,9 +177,14 @@ protected virtual async Task SendCustomerMessage(Customer customer, Store s /// Store identifier /// Message language identifier /// Queued email identifier - public virtual async Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, + DomainHost host = null) { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerRegistered, true); + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerRegistered, true, + host: host); } /// @@ -185,9 +193,13 @@ public virtual async Task SendCustomerRegisteredMessage(Customer customer, /// Customer instance /// Store /// Message language identifier - public virtual async Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId, + DomainHost host = null) { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerWelcome); + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerWelcome, host: host); } /// @@ -196,9 +208,14 @@ public virtual async Task SendCustomerWelcomeMessage(Customer customer, Sto /// Customer instance /// Store /// Message language identifier - public virtual async Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId, + DomainHost host = null) { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerEmailValidation); + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerEmailValidation, + host: host); } /// @@ -207,10 +224,14 @@ public virtual async Task SendCustomerEmailValidationMessage(Customer custo /// Customer instance /// Store /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, - string languageId) + string languageId, DomainHost host = null) { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerPasswordRecovery); + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerPasswordRecovery, + host: host); } /// @@ -234,11 +255,14 @@ public virtual async Task SendNewCustomerNoteMessage(CustomerNote customerN /// Customer instance /// Store instance /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, - string languageId) + string languageId, DomainHost host = null) { return await SendCustomerMessage(customer, store, languageId, - MessageTemplateNames.CustomerEmailTokenValidationMessage); + MessageTemplateNames.CustomerEmailTokenValidationMessage, host: host); } #endregion @@ -251,10 +275,14 @@ public virtual async Task SendCustomerEmailTokenValidationMessage(Customer /// Order instance /// Customer /// Message language identifier - public virtual async Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId, + DomainHost host = null) { return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderPlacedStoreOwnerMessage, order, customer, - languageId); + languageId, host: host); } /// @@ -303,8 +331,11 @@ public virtual async Task SendOrderRefundedStoreOwnerMessage(Order order, d /// Customer instance /// Message language identifier /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// private async Task SendOrderStoreOwnerMessage(string template, Order order, Customer customer, - string languageId, double refundedAmount = 0) + string languageId, double refundedAmount = 0, DomainHost host = null) { ArgumentNullException.ThrowIfNull(order); @@ -318,7 +349,7 @@ private async Task SendOrderStoreOwnerMessage(string template, Order order, //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var liquidBuilder = new LiquidObjectBuilder(_mediator); liquidBuilder.AddStoreTokens(store, language, emailAccount) .AddOrderTokens(order, customer, store, host); @@ -348,11 +379,15 @@ private async Task SendOrderStoreOwnerMessage(string template, Order order, /// Otherwise, "AttachmentFilePath" name will be used. /// /// Attachments + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null) + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, + DomainHost host = null) { return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderPlacedCustomerMessage, order, customer, - languageId, attachmentFilePath, attachmentFileName, attachments); + languageId, attachmentFilePath, attachmentFileName, attachments, host: host); } /// @@ -434,9 +469,12 @@ public virtual async Task SendOrderRefundedCustomerMessage(Order order, dou /// /// Attachments ident /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// private async Task SendOrderCustomerMessage(string message, Order order, Customer customer, string languageId, string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, - double refundedAmount = 0) + double refundedAmount = 0, DomainHost host = null) { ArgumentNullException.ThrowIfNull(order); @@ -450,7 +488,7 @@ private async Task SendOrderCustomerMessage(string message, Order order, Cu //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddOrderTokens(order, customer, store, host); @@ -478,11 +516,14 @@ private async Task SendOrderCustomerMessage(string message, Order order, Cu /// Customer instance /// Vendor instance /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, - string languageId) + string languageId, DomainHost host = null) { return await SendOrderVendorMessage(MessageTemplateNames.SendOrderPlacedVendorMessage, order, vendor, - languageId); + languageId, host: host); } /// @@ -515,7 +556,11 @@ public virtual async Task SendOrderCancelledVendorMessage(Order order, Vend /// Order instance /// Vendor instance /// Message language identifier - private async Task SendOrderVendorMessage(string message, Order order, Vendor vendor, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + private async Task SendOrderVendorMessage(string message, Order order, Vendor vendor, string languageId, + DomainHost host = null) { ArgumentNullException.ThrowIfNull(order); ArgumentNullException.ThrowIfNull(vendor); @@ -532,7 +577,7 @@ private async Task SendOrderVendorMessage(string message, Order order, Vend var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddOrderTokens(order, customer, store, host, vendor: vendor); @@ -624,7 +669,11 @@ private async Task SendShipmentCustomerMessage(string message, Shipment shi /// /// Order /// Order note - public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote, + DomainHost host = null) { ArgumentNullException.ThrowIfNull(orderNote); ArgumentNullException.ThrowIfNull(order); @@ -642,7 +691,7 @@ public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddOrderTokens(order, customer, store, host, orderNote); @@ -670,11 +719,14 @@ public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, /// /// Newsletter subscription /// Language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - string languageId) + string languageId, DomainHost host = null) { return await SendNewsLetterSubscriptionMessage(MessageTemplateNames.SendNewsLetterSubscriptionActivationMessage, - subscription, languageId); + subscription, languageId, host: host); } /// @@ -682,11 +734,14 @@ public virtual async Task SendNewsLetterSubscriptionActivationMessage(NewsL /// /// Newsletter subscription /// Language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - string languageId) + string languageId, DomainHost host = null) { return await SendNewsLetterSubscriptionMessage( - MessageTemplateNames.SendNewsLetterSubscriptionDeactivationMessage, subscription, languageId); + MessageTemplateNames.SendNewsLetterSubscriptionDeactivationMessage, subscription, languageId, host: host); } /// @@ -695,8 +750,11 @@ public virtual async Task SendNewsLetterSubscriptionDeactivationMessage(New /// Message template /// Newsletter subscription /// Language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// private async Task SendNewsLetterSubscriptionMessage(string message, NewsLetterSubscription subscription, - string languageId) + string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(subscription); @@ -710,7 +768,7 @@ private async Task SendNewsLetterSubscriptionMessage(string message, NewsLe //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddNewsLetterSubscriptionTokens(subscription, store, host); @@ -741,8 +799,11 @@ private async Task SendNewsLetterSubscriptionMessage(string message, NewsLe /// Friend's email /// Personal message /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendProductEmailAFriendMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string friendsEmail, string personalMessage) + Product product, string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null) { ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(product); @@ -756,7 +817,7 @@ public virtual async Task SendProductEmailAFriendMessage(Customer customer, //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail) @@ -785,8 +846,11 @@ public virtual async Task SendProductEmailAFriendMessage(Customer customer, /// Friend's email /// Personal message /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendWishlistEmailAFriendMessage(Customer customer, Store store, string languageId, - string customerEmail, string friendsEmail, string personalMessage) + string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null) { ArgumentNullException.ThrowIfNull(customer); @@ -800,7 +864,7 @@ public virtual async Task SendWishlistEmailAFriendMessage(Customer customer var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language) @@ -824,8 +888,12 @@ public virtual async Task SendWishlistEmailAFriendMessage(Customer customer /// Sends "email a friend" message /// /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendProductQuestionMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string fullName, string phone, string message, string ipaddress) + Product product, string customerEmail, string fullName, string phone, string message, string ipaddress, + DomainHost host = null) { ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(product); @@ -839,7 +907,7 @@ public virtual async Task SendProductQuestionMessage(Customer customer, Sto //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language) @@ -901,8 +969,11 @@ await _mediator.Send(new InsertContactUsCommand { /// Merchandise return /// Order /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendNewMerchandiseReturnStoreOwnerMessage(MerchandiseReturn merchandiseReturn, - Order order, string languageId) + Order order, string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(merchandiseReturn); @@ -919,7 +990,7 @@ public virtual async Task SendNewMerchandiseReturnStoreOwnerMessage(Merchan var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); @@ -1002,8 +1073,11 @@ public virtual async Task SendMerchandiseReturnStatusChangedCustomerMessage /// Merchandise return /// Order /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendNewMerchandiseReturnCustomerMessage(MerchandiseReturn merchandiseReturn, - Order order, string languageId) + Order order, string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(merchandiseReturn); @@ -1020,7 +1094,7 @@ public virtual async Task SendNewMerchandiseReturnCustomerMessage(Merchandi var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount); AddCustomerTokensIfNotNull(builder, customer, store, host, language); @@ -1093,8 +1167,11 @@ public virtual async Task SendNewMerchandiseReturnNoteAddedCustomerMessage( /// Vendor /// Store /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor vendor, - Store store, string languageId) + Store store, string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(vendor); @@ -1108,7 +1185,7 @@ public virtual async Task SendNewVendorAccountApplyStoreOwnerMessage(Custom //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator).AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language) .AddVendorTokens(vendor, language); @@ -1202,8 +1279,11 @@ public virtual async Task SendGiftVoucherMessage(GiftVoucher giftVoucher, O /// Product review /// Store /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendProductReviewMessage(Product product, ProductReview productReview, - Store store, string languageId) + Store store, string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(productReview); @@ -1218,7 +1298,7 @@ public virtual async Task SendProductReviewMessage(Product product, Product var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = productReview.CustomerId }); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddProductReviewTokens(product, productReview); @@ -1243,8 +1323,11 @@ public virtual async Task SendProductReviewMessage(Product product, Product /// Vendor review /// Store /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendVendorReviewMessage(VendorReview vendorReview, Store store, - string languageId) + string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(vendorReview); @@ -1261,7 +1344,7 @@ public virtual async Task SendVendorReviewMessage(VendorReview vendorReview //vendor var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = vendorReview.VendorId }); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddVendorReviewTokens(vendor, vendorReview); @@ -1360,7 +1443,11 @@ public virtual async Task SendQuantityBelowStoreOwnerMessage(Product produc /// /// Customer /// Message language identifier - public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId, + DomainHost host = null) { ArgumentNullException.ThrowIfNull(customer); @@ -1374,7 +1461,7 @@ public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer cust //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language); @@ -1392,7 +1479,11 @@ public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer cust /// /// Sends a blog comment notification message to a store owner /// - public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId, + DomainHost host = null) { ArgumentNullException.ThrowIfNull(blogComment); @@ -1406,7 +1497,7 @@ public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogCom //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddBlogCommentTokens(blogPost, blogComment, store, host, language); @@ -1433,8 +1524,11 @@ public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogCom /// /// Article comment /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle article, - KnowledgebaseArticleComment articleComment, string languageId) + KnowledgebaseArticleComment articleComment, string languageId, DomainHost host = null) { ArgumentNullException.ThrowIfNull(articleComment); @@ -1448,7 +1542,7 @@ public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle ar //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddArticleCommentTokens(article, articleComment, store, host, language); @@ -1473,7 +1567,11 @@ public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle ar /// News item /// News comment /// Message language identifier - public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId) + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId, + DomainHost host = null) { ArgumentNullException.ThrowIfNull(newsComment); @@ -1487,7 +1585,7 @@ public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsCom //email account var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddNewsCommentTokens(newsItem, newsComment, store, host, language); @@ -1558,10 +1656,13 @@ public virtual async Task SendBackInStockMessage(Customer customer, Product /// Attr info /// CustomAttributes /// Ip address + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendContactUsMessage(Customer customer, Store store, string languageId, string senderEmail, string senderName, string subject, string body, string attrInfo, IList customAttributes, - string ipaddress) + string ipaddress, DomainHost host = null) { var language = await EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = await GetMessageTemplate("Service.ContactUs", store.Id); @@ -1587,7 +1688,7 @@ public virtual async Task SendContactUsMessage(Customer customer, Store sto fromName = senderName; } - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language); @@ -1635,9 +1736,12 @@ await _mediator.Send(new InsertContactUsCommand { /// Email subject. Pass null if you want a message template subject to be used. /// Email body /// Ip address + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// public virtual async Task SendContactVendorMessage(Customer customer, Store store, Vendor vendor, string languageId, string senderEmail, - string senderName, string subject, string body, string ipaddress) + string senderName, string subject, string body, string ipaddress, DomainHost host = null) { ArgumentNullException.ThrowIfNull(vendor); @@ -1667,7 +1771,7 @@ public virtual async Task SendContactVendorMessage(Customer customer, Store fromName = senderName; } - var host = store.PrimaryHost(); + host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); builder.AddStoreTokens(store, language, emailAccount) .AddCustomerTokens(customer, store, host, language); diff --git a/src/Core/Grand.Domain/Stores/StoreExtensions.cs b/src/Core/Grand.Domain/Stores/StoreExtensions.cs index 27cc2c99e3..61cf90363c 100644 --- a/src/Core/Grand.Domain/Stores/StoreExtensions.cs +++ b/src/Core/Grand.Domain/Stores/StoreExtensions.cs @@ -48,4 +48,20 @@ public static DomainHost PrimaryHost(this Store store) HostName = "temporary-store" }; } + + /// + /// Return the host links to a store point to for a request to a given host: that host when it is one of the + /// store's domains, else the store's primary host + /// + /// Store + /// Host the request was made to; null when there is no request + /// DomainHost + public static DomainHost LinkHost(this Store store, DomainHost host) + { + ArgumentNullException.ThrowIfNull(store); + var domain = string.IsNullOrEmpty(host?.HostName) + ? null + : store.Domains?.FirstOrDefault(x => x.HostName.Equals(host.HostName, StringComparison.OrdinalIgnoreCase)); + return domain ?? store.PrimaryHost(); + } } \ No newline at end of file diff --git a/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs b/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs index 3bef435ae1..2d5c75209c 100644 --- a/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs +++ b/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs @@ -23,6 +23,7 @@ public class FacebookAuthenticationEventConsumer : INotificationHandler _contextAccessor.WorkContext.WorkingLanguage.Id; private Store CurrentStore => _contextAccessor.StoreContext.CurrentStore; + private DomainHost CurrentHost => _contextAccessor.StoreContext.CurrentHost; #endregion @@ -67,10 +68,12 @@ public async Task Handle(RegisteredByExternalMethod eventMessage, CancellationTo //notifications for admin if (_customerSettings.NotifyNewCustomerRegistration) - await _messageProviderService.SendCustomerRegisteredMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId); + await _messageProviderService.SendCustomerRegisteredMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId, + CurrentHost); //send welcome message - await _messageProviderService.SendCustomerWelcomeMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId); + await _messageProviderService.SendCustomerWelcomeMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId, + CurrentHost); } #endregion } \ No newline at end of file diff --git a/src/Plugins/Authentication.Google/Infrastructure/GoogleAuthenticationEventConsumer.cs b/src/Plugins/Authentication.Google/Infrastructure/GoogleAuthenticationEventConsumer.cs index bfb44a73ee..24c4d16695 100644 --- a/src/Plugins/Authentication.Google/Infrastructure/GoogleAuthenticationEventConsumer.cs +++ b/src/Plugins/Authentication.Google/Infrastructure/GoogleAuthenticationEventConsumer.cs @@ -22,6 +22,7 @@ public class GoogleAuthenticationEventConsumer : INotificationHandler _contextAccessor.WorkContext.WorkingLanguage.Id; private Store CurrentStore => _contextAccessor.StoreContext.CurrentStore; + private DomainHost CurrentHost => _contextAccessor.StoreContext.CurrentHost; #endregion #region Ctor @@ -66,10 +67,12 @@ public async Task Handle(RegisteredByExternalMethod eventMessage, CancellationTo //notifications for admin if (_customerSettings.NotifyNewCustomerRegistration) - await _messageProviderService.SendCustomerRegisteredMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId); + await _messageProviderService.SendCustomerRegisteredMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId, + CurrentHost); //send welcome message - await _messageProviderService.SendCustomerWelcomeMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId); + await _messageProviderService.SendCustomerWelcomeMessage(eventMessage.Customer, CurrentStore, WorkingLanguageId, + CurrentHost); } #endregion diff --git a/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs b/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs index 196e59f858..fc3d8ae33e 100644 --- a/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs +++ b/src/Tests/Grand.Domain.Tests/Stores/StoreExtensionsTests.cs @@ -50,6 +50,23 @@ public void PrimaryHost_WithoutDomains_FallsBackToTheStoreUrl() Assert.AreEqual("https://shop.example/", withoutDomains.PrimaryHost().Url); } + [TestMethod] + public void LinkHost_ReturnsTheRequestHostWhenItIsOneOfTheStoreDomains() + { + store.Domains[0].Primary = true; + + Assert.AreEqual("yahoo.com", store.LinkHost(new DomainHost { HostName = "Yahoo.com" }).HostName); + } + + [TestMethod] + public void LinkHost_ForeignOrMissingHost_ReturnsThePrimaryDomain() + { + store.Domains[1].Primary = true; + + Assert.AreEqual("yahoo.com", store.LinkHost(new DomainHost { HostName = "other.example" }).HostName); + Assert.AreEqual("yahoo.com", store.LinkHost(null).HostName); + } + [TestMethod] public void HostValueTest_NotNull() { diff --git a/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs index 33507ac323..b727efdaa8 100644 --- a/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs @@ -52,7 +52,7 @@ public async Task Handle(InsertBlogCommentCommand request, Cancella //notify a store owner if (_blogSettings.NotifyAboutNewBlogComments) await _messageProviderService.SendBlogCommentMessage(request.BlogPost, comment, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return comment; } diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs index 6458afb17b..e92788fa0e 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs @@ -38,7 +38,8 @@ public async Task Handle(ContactUsSendCommand request, Cancellat request.Model.ContactAttribute = attributes; request.Model.ContactAttributeInfo = - await _contactAttributeParser.FormatAttributes(request.Language, attributes, request.Store.PrimaryHost()); + await _contactAttributeParser.FormatAttributes(request.Language, attributes, + request.Store.LinkHost(request.Host)); request.Model = await SendContactUs(request); return request.Model; @@ -52,7 +53,8 @@ private async Task SendContactUs(ContactUsSendCommand request) await _messageProviderService.SendContactUsMessage (request.Customer, request.Store, request.Language.Id, request.Model.Email.Trim(), request.Model.FullName, subject, - body, request.Model.ContactAttributeInfo, request.Model.ContactAttribute, request.IpAddress); + body, request.Model.ContactAttributeInfo, request.Model.ContactAttribute, request.IpAddress, + host: request.Host); request.Model.SuccessfullySent = true; request.Model.Result = _translationService.GetResource("ContactUs.YourEnquiryHasBeenSent", request.Language.Id); diff --git a/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs index 89411e7232..f69a4aa29b 100644 --- a/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs @@ -184,7 +184,7 @@ await _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmailAndStoreId( //notifications if (_customerSettings.NotifyNewCustomerRegistration) await _messageProviderService.SendCustomerRegisteredMessage(request.Customer, request.Store, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: request.Host); //New customer has a free shipping for the first order if (_customerSettings.RegistrationFreeShipping) diff --git a/src/Web/Grand.Web/Commands/Handler/Customers/DeleteAccountCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Customers/DeleteAccountCommandHandler.cs index 1fb4e1aff5..f53d42701a 100644 --- a/src/Web/Grand.Web/Commands/Handler/Customers/DeleteAccountCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Customers/DeleteAccountCommandHandler.cs @@ -33,7 +33,7 @@ public async Task Handle(DeleteAccountCommand request, CancellationToken c { //send notification to customer await _messageProviderService.SendCustomerDeleteStoreOwnerMessage(request.Customer, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: request.Host); //delete emails await _queuedEmailService.DeleteCustomerEmail(request.Customer.Email); diff --git a/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs index 15a2a12e78..50a0be2f58 100644 --- a/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs @@ -42,7 +42,7 @@ await _customerService.UpdateUserField(request.Customer, SystemCustomerF //send email await _messageProviderService.SendCustomerPasswordRecoveryMessage(request.Customer, request.Store, - request.Language.Id); + request.Language.Id, host: request.Host); return true; } diff --git a/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs index 7f48ff0f84..5337d3112a 100644 --- a/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs @@ -53,7 +53,7 @@ public async Task Handle(InsertNewsCommentCommand request, Cancella //notify a store owner; if (_newsSettings.NotifyAboutNewNewsComments) await _messageProviderService.SendNewsCommentMessage(request.NewsItem, comment, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return comment; } diff --git a/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs b/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs index 0ac363c1da..07f42e4cbd 100644 --- a/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs @@ -55,14 +55,16 @@ await _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmailAndStoreId( { if (!subscription.Active) await _messageProviderService.SendNewsLetterSubscriptionActivationMessage(subscription, - _contextAccessor.WorkContext.WorkingLanguage.Id); + _contextAccessor.WorkContext.WorkingLanguage.Id, + host: _contextAccessor.StoreContext.CurrentHost); model.Result = _translationService.GetResource("Newsletter.SubscribeEmailSent"); } else { if (subscription.Active) await _messageProviderService.SendNewsLetterSubscriptionDeactivationMessage(subscription, - _contextAccessor.WorkContext.WorkingLanguage.Id); + _contextAccessor.WorkContext.WorkingLanguage.Id, + host: _contextAccessor.StoreContext.CurrentHost); model.Result = _translationService.GetResource("Newsletter.UnsubscribeEmailSent"); } } @@ -78,7 +80,7 @@ await _messageProviderService.SendNewsLetterSubscriptionDeactivationMessage(subs await _newsLetterSubscriptionService.InsertNewsLetterSubscription(subscription); await _messageProviderService.SendNewsLetterSubscriptionActivationMessage(subscription, - _contextAccessor.WorkContext.WorkingLanguage.Id); + _contextAccessor.WorkContext.WorkingLanguage.Id, host: _contextAccessor.StoreContext.CurrentHost); model.Result = _translationService.GetResource("Newsletter.SubscribeEmailSent"); var modelCategory = await PrepareNewsletterCategory(subscription.Id); diff --git a/src/Web/Grand.Web/Commands/Handler/Orders/InsertOrderNoteCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Orders/InsertOrderNoteCommandHandler.cs index d9874ab64c..e89823e69d 100644 --- a/src/Web/Grand.Web/Commands/Handler/Orders/InsertOrderNoteCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Orders/InsertOrderNoteCommandHandler.cs @@ -28,7 +28,8 @@ public async Task Handle(InsertOrderNoteCommand request, Cancellation await _orderService.InsertOrderNote(orderNote); //email - await _messageProviderService.SendNewOrderNoteAddedCustomerMessage(request.Order, orderNote); + await _messageProviderService.SendNewOrderNoteAddedCustomerMessage(request.Order, orderNote, + host: request.Host); return orderNote; } diff --git a/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs index a078dfe56d..d0ae78bf22 100644 --- a/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs @@ -86,10 +86,10 @@ public async Task Handle( //notify store owner here (email) await _messageProviderService.SendNewMerchandiseReturnStoreOwnerMessage(rr, request.Order, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); //notify customer await _messageProviderService.SendNewMerchandiseReturnCustomerMessage(rr, request.Order, - request.Order.CustomerLanguageId); + request.Order.CustomerLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return rr; } diff --git a/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs index a5c6d75f8a..03b6e7466d 100644 --- a/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs @@ -76,7 +76,7 @@ public async Task Handle(InsertProductReviewCommand request, Canc //notify store owner if (_catalogSettings.NotifyStoreOwnerAboutNewProductReviews) await _messageProviderService.SendProductReviewMessage(request.Product, productReview, request.Store, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: request.Host); return productReview; } diff --git a/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs index 83520d81ed..51f70bac44 100644 --- a/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs @@ -18,7 +18,7 @@ public async Task Handle(SendProductAskQuestionMessageCommand request, Can { await _messageProviderService.SendProductQuestionMessage(request.Customer, request.Store, request.Language.Id, request.Product, request.Model.Email, request.Model.FullName, request.Model.Phone, - FormatText.ConvertText(request.Model.Message), request.RemoteIpAddress); + FormatText.ConvertText(request.Model.Message), request.RemoteIpAddress, host: request.Host); return true; } diff --git a/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs index 1b07274228..859c35e073 100644 --- a/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs @@ -19,7 +19,7 @@ public async Task Handle(SendProductEmailAFriendMessageCommand request, Ca await _messageProviderService.SendProductEmailAFriendMessage(request.Customer, request.Store, request.Language.Id, request.Product, request.Model.YourEmailAddress, request.Model.FriendEmail, - FormatText.ConvertText(request.Model.PersonalMessage)); + FormatText.ConvertText(request.Model.PersonalMessage), host: request.Host); return true; } diff --git a/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs index 146dc56909..12bc5db696 100644 --- a/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs @@ -32,7 +32,8 @@ public async Task Handle(ContactVendorSendCommand request, C await _messageProviderService.SendContactVendorMessage(_contextAccessor.WorkContext.CurrentCustomer, request.Store, request.Vendor, _contextAccessor.WorkContext.WorkingLanguage.Id, - request.Model.Email.Trim(), request.Model.FullName, subject, body, request.IpAddress); + request.Model.Email.Trim(), request.Model.FullName, subject, body, request.IpAddress, + host: _contextAccessor.StoreContext.CurrentHost); request.Model.SuccessfullySent = true; request.Model.Result = _translationService.GetResource("ContactVendor.YourEnquiryHasBeenSent"); diff --git a/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs index 5a211190f4..09437083fc 100644 --- a/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs @@ -60,7 +60,7 @@ public async Task Handle(InsertVendorReviewCommand request, Cancel //notify store owner if (_vendorSettings.NotifyVendorAboutNewVendorReviews) await _messageProviderService.SendVendorReviewMessage(vendorReview, request.Store, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return vendorReview; } diff --git a/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs b/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs index 21d38303f8..a8e144e9af 100644 --- a/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Contact/ContactUsSendCommand.cs @@ -10,6 +10,7 @@ public class ContactUsSendCommand : IRequest { public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public Language Language { get; set; } public ContactUsModel Model { get; set; } public string IpAddress { get; set; } diff --git a/src/Web/Grand.Web/Commands/Models/Customers/CustomerRegisteredCommand.cs b/src/Web/Grand.Web/Commands/Models/Customers/CustomerRegisteredCommand.cs index c47d5ac0aa..46a2dec659 100644 --- a/src/Web/Grand.Web/Commands/Models/Customers/CustomerRegisteredCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Customers/CustomerRegisteredCommand.cs @@ -9,6 +9,7 @@ public class CustomerRegisteredCommand : IRequest { public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public RegisterModel Model { get; set; } public IList CustomerAttributes { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Commands/Models/Customers/DeleteAccountCommand.cs b/src/Web/Grand.Web/Commands/Models/Customers/DeleteAccountCommand.cs index 7eae751cc8..1203be1cd9 100644 --- a/src/Web/Grand.Web/Commands/Models/Customers/DeleteAccountCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Customers/DeleteAccountCommand.cs @@ -7,5 +7,6 @@ public class DeleteAccountCommand : IRequest { public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public string IpAddress { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Commands/Models/Customers/PasswordRecoverySendCommand.cs b/src/Web/Grand.Web/Commands/Models/Customers/PasswordRecoverySendCommand.cs index e53ffd43cd..6d0d6a46e7 100644 --- a/src/Web/Grand.Web/Commands/Models/Customers/PasswordRecoverySendCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Customers/PasswordRecoverySendCommand.cs @@ -10,5 +10,6 @@ public class PasswordRecoverySendCommand : IRequest public PasswordRecoveryModel Model { get; set; } public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public Language Language { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Commands/Models/Orders/InsertOrderNoteCommand.cs b/src/Web/Grand.Web/Commands/Models/Orders/InsertOrderNoteCommand.cs index 423c0477fe..6f43689ae9 100644 --- a/src/Web/Grand.Web/Commands/Models/Orders/InsertOrderNoteCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Orders/InsertOrderNoteCommand.cs @@ -10,4 +10,5 @@ public class InsertOrderNoteCommand : IRequest public Order Order { get; set; } public Language Language { get; set; } public AddOrderNoteModel OrderNote { get; set; } + public Domain.Stores.DomainHost Host { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Commands/Models/Products/InsertProductReviewCommand.cs b/src/Web/Grand.Web/Commands/Models/Products/InsertProductReviewCommand.cs index 02c723b8cc..7587ae8a5c 100644 --- a/src/Web/Grand.Web/Commands/Models/Products/InsertProductReviewCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Products/InsertProductReviewCommand.cs @@ -8,6 +8,7 @@ namespace Grand.Web.Commands.Models.Products; public class InsertProductReviewCommand : IRequest { public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public Customer Customer { get; set; } public Product Product { get; set; } public ProductReviewsModel Model { get; set; } diff --git a/src/Web/Grand.Web/Commands/Models/Products/SendProductAskQuestionMessageCommand.cs b/src/Web/Grand.Web/Commands/Models/Products/SendProductAskQuestionMessageCommand.cs index 723807d455..0ed29717ac 100644 --- a/src/Web/Grand.Web/Commands/Models/Products/SendProductAskQuestionMessageCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Products/SendProductAskQuestionMessageCommand.cs @@ -10,6 +10,7 @@ public class SendProductAskQuestionMessageCommand : IRequest { public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public Language Language { get; set; } public Product Product { get; set; } public ProductAskQuestionModel Model { get; set; } diff --git a/src/Web/Grand.Web/Commands/Models/Products/SendProductEmailAFriendMessageCommand.cs b/src/Web/Grand.Web/Commands/Models/Products/SendProductEmailAFriendMessageCommand.cs index 2716b12521..b454816e00 100644 --- a/src/Web/Grand.Web/Commands/Models/Products/SendProductEmailAFriendMessageCommand.cs +++ b/src/Web/Grand.Web/Commands/Models/Products/SendProductEmailAFriendMessageCommand.cs @@ -10,6 +10,7 @@ public class SendProductEmailAFriendMessageCommand : IRequest { public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } + public Domain.Stores.DomainHost Host { get; set; } public Language Language { get; set; } public Product Product { get; set; } public ProductEmailAFriendModel Model { get; set; } diff --git a/src/Web/Grand.Web/Controllers/AccountController.cs b/src/Web/Grand.Web/Controllers/AccountController.cs index 6314695ff5..74fdbb1c61 100644 --- a/src/Web/Grand.Web/Controllers/AccountController.cs +++ b/src/Web/Grand.Web/Controllers/AccountController.cs @@ -297,6 +297,7 @@ public async Task TwoFactorAuthorization() if (_customerSettings.TwoFactorAuthenticationType != TwoFactorAuthenticationType.AppVerification) await _mediator.Send(new GetTwoFactorAuthentication { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = customer, Language = _contextAccessor.WorkContext.WorkingLanguage, Store = _contextAccessor.StoreContext.CurrentStore @@ -432,6 +433,7 @@ public virtual async Task> PasswordRecovery( var customer = await _customerService.GetCustomerByEmail(model.Email, CustomerStoreId); await _mediator.Send(new PasswordRecoverySendCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = customer, Store = _contextAccessor.StoreContext.CurrentStore, Language = _contextAccessor.WorkContext.WorkingLanguage, @@ -603,6 +605,7 @@ public virtual async Task Register(RegisterModel model, string re { SelectedAttributes = model.SelectedAttributes }); await _mediator.Send(new CustomerRegisteredCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, CustomerAttributes = customerAttributes, Model = model, @@ -627,7 +630,8 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto SystemCustomerFieldNames.AccountActivationToken, Guid.NewGuid().ToString()); await _messageProviderService.SendCustomerEmailValidationMessage( _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingLanguage.Id); + _contextAccessor.WorkContext.WorkingLanguage.Id, + host: _contextAccessor.StoreContext.CurrentHost); //result result = RedirectToRoute("RegisterResult", @@ -644,7 +648,8 @@ await _messageProviderService.SendCustomerEmailValidationMessage( { //send customer welcome message await _messageProviderService.SendCustomerWelcomeMessage(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id); + _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id, + host: _contextAccessor.StoreContext.CurrentHost); var redirectUrl = Url.RouteUrl("RegisterResult", new { resultId = (int)UserRegistrationType.Standard }, HttpContext.Request.Scheme); @@ -791,7 +796,7 @@ public virtual async Task AccountActivation(string token, string //send welcome message await _messageProviderService.SendCustomerWelcomeMessage(customer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingLanguage.Id); + _contextAccessor.WorkContext.WorkingLanguage.Id, host: _contextAccessor.StoreContext.CurrentHost); var model = new AccountActivationModel { Result = _translationService.GetResource("Account.AccountActivation.Activated") @@ -1152,6 +1157,7 @@ public virtual async Task DeleteAccount(DeleteAccountModel model, //delete account await _mediator.Send(new DeleteAccountCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Store = _contextAccessor.StoreContext.CurrentStore, IpAddress = HttpContext.Connection?.RemoteIpAddress?.ToString() @@ -1182,6 +1188,7 @@ public async Task EnableTwoFactorAuthenticator([FromServices] Oid return RedirectToRoute("CustomerInfo"); var model = await _mediator.Send(new GetTwoFactorAuthentication { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Language = _contextAccessor.WorkContext.WorkingLanguage, Store = _contextAccessor.StoreContext.CurrentStore @@ -1242,6 +1249,7 @@ public async Task DisableTwoFactorAuthenticator([FromServices] Oi return RedirectToRoute("CustomerInfo"); _ = await _mediator.Send(new GetTwoFactorAuthentication { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Language = _contextAccessor.WorkContext.WorkingLanguage, Store = _contextAccessor.StoreContext.CurrentStore diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index f7b19ed172..cdeaabeb80 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -724,7 +724,7 @@ public virtual async Task ConfirmOrder() var placeOrderResult = await _mediator.Send(new PlaceOrderCommand { Context = _contextAccessor.CalculationContext(), Language = _contextAccessor.WorkContext.WorkingLanguage, - Host = _contextAccessor.StoreContext.CurrentStore.PrimaryHost(), + Host = _contextAccessor.StoreContext.CurrentStore.LinkHost(_contextAccessor.StoreContext.CurrentHost), OriginalCustomerIfImpersonated = _contextAccessor.WorkContext.OriginalCustomerIfImpersonated }); if (placeOrderResult.Success) diff --git a/src/Web/Grand.Web/Controllers/ContactController.cs b/src/Web/Grand.Web/Controllers/ContactController.cs index 2ad13fd569..74d8372332 100644 --- a/src/Web/Grand.Web/Controllers/ContactController.cs +++ b/src/Web/Grand.Web/Controllers/ContactController.cs @@ -76,6 +76,7 @@ public virtual async Task> Index( if (ModelState.IsValid) { var result = await _mediator.Send(new ContactUsSendCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Store = _contextAccessor.StoreContext.CurrentStore, Language = _contextAccessor.WorkContext.WorkingLanguage, diff --git a/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs b/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs index d098c7381b..6090b1324e 100644 --- a/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs +++ b/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs @@ -315,7 +315,7 @@ public virtual async Task ArticleCommentAdd(KnowledgebaseArticleM //notify a store owner if (_knowledgebaseSettings.NotifyAboutNewArticleComments) await _messageProviderService.SendArticleCommentMessage(article, comment, - _languageSettings.DefaultAdminLanguageId); + _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); //The text boxes should be cleared after a comment has been posted //That' why we reload the page diff --git a/src/Web/Grand.Web/Controllers/OrderController.cs b/src/Web/Grand.Web/Controllers/OrderController.cs index 22410d0eb5..e373bd2959 100644 --- a/src/Web/Grand.Web/Controllers/OrderController.cs +++ b/src/Web/Grand.Web/Controllers/OrderController.cs @@ -146,8 +146,10 @@ public virtual async Task> AddOrderNote(AddOrder if (!await order.Access(_contextAccessor.WorkContext.CurrentCustomer, _groupService)) return Challenge(); - await _mediator.Send(new InsertOrderNoteCommand - { Order = order, OrderNote = model, Language = _contextAccessor.WorkContext.WorkingLanguage }); + await _mediator.Send(new InsertOrderNoteCommand { + Order = order, OrderNote = model, Language = _contextAccessor.WorkContext.WorkingLanguage, + Host = _contextAccessor.StoreContext.CurrentHost + }); //notification await _mediator.Publish(new OrderNoteEvent(order, model)); diff --git a/src/Web/Grand.Web/Controllers/ProductController.cs b/src/Web/Grand.Web/Controllers/ProductController.cs index 85791be6e7..cbcc224e52 100644 --- a/src/Web/Grand.Web/Controllers/ProductController.cs +++ b/src/Web/Grand.Web/Controllers/ProductController.cs @@ -151,6 +151,7 @@ public virtual async Task ProductEmailAFriend(ProductEmailAFriend if (ModelState.IsValid) { await _mediator.Send(new SendProductEmailAFriendMessageCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Product = product, Language = _contextAccessor.WorkContext.WorkingLanguage, @@ -210,6 +211,7 @@ public virtual async Task AskQuestionOnProduct(ProductAskQuestion // email await _mediator.Send(new SendProductAskQuestionMessageCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Language = _contextAccessor.WorkContext.WorkingLanguage, Store = _contextAccessor.StoreContext.CurrentStore, @@ -573,6 +575,7 @@ public virtual async Task ProductReviews( if (ModelState.IsValid) { var productReview = await _mediator.Send(new InsertProductReviewCommand { + Host = _contextAccessor.StoreContext.CurrentHost, Customer = _contextAccessor.WorkContext.CurrentCustomer, Store = _contextAccessor.StoreContext.CurrentStore, Model = model, diff --git a/src/Web/Grand.Web/Controllers/VendorController.cs b/src/Web/Grand.Web/Controllers/VendorController.cs index 929b92e4c3..55f0bafd62 100644 --- a/src/Web/Grand.Web/Controllers/VendorController.cs +++ b/src/Web/Grand.Web/Controllers/VendorController.cs @@ -168,7 +168,8 @@ await _customerService.UpdateCustomerField(_contextAccessor.WorkContext.CurrentC //notify store owner here (email) await _messageProviderService.SendNewVendorAccountApplyStoreOwnerMessage(_contextAccessor.WorkContext.CurrentCustomer, - vendor, _contextAccessor.StoreContext.CurrentStore, _languageSettings.DefaultAdminLanguageId); + vendor, _contextAccessor.StoreContext.CurrentStore, _languageSettings.DefaultAdminLanguageId, + host: _contextAccessor.StoreContext.CurrentHost); model.DisableFormInput = true; model.Result = _translationService.GetResource("Vendors.ApplyAccount.Submitted"); diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 4456f6b92a..71a0e029d8 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -228,7 +228,8 @@ public virtual async Task EmailWishlist(WishlistEmailAFriendModel await messageProviderService.SendWishlistEmailAFriendMessage(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id, model.YourEmailAddress, - model.FriendEmail, FormatText.ConvertText(model.PersonalMessage)); + model.FriendEmail, FormatText.ConvertText(model.PersonalMessage), + host: _contextAccessor.StoreContext.CurrentHost); model.SuccessfullySent = true; model.Result = _translationService.GetResource("Wishlist.EmailAFriend.SuccessfullySent"); diff --git a/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs b/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs index 3b520265af..204c5751a0 100644 --- a/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs @@ -37,7 +37,7 @@ public GetTwoFactorAuthenticationHandler( }; if (_customerSetting.TwoFactorAuthenticationType == TwoFactorAuthenticationType.EmailVerification) await _messageProviderService.SendCustomerEmailTokenValidationMessage(request.Customer, request.Store, - request.Customer.Id); + request.Customer.Id, host: request.Host); return model; } diff --git a/src/Web/Grand.Web/Features/Models/Customers/GetTwoFactorAuthentication.cs b/src/Web/Grand.Web/Features/Models/Customers/GetTwoFactorAuthentication.cs index 8643c1e407..67de80d633 100644 --- a/src/Web/Grand.Web/Features/Models/Customers/GetTwoFactorAuthentication.cs +++ b/src/Web/Grand.Web/Features/Models/Customers/GetTwoFactorAuthentication.cs @@ -11,4 +11,5 @@ public class GetTwoFactorAuthentication : IRequest