diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs index 3aa63362ab..ec13d9c5d0 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs @@ -1,13 +1,13 @@ -using Grand.Business.Core.Extensions; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Prices; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; -using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; -using Grand.Domain.Tax; -using Grand.Infrastructure; +using Grand.Domain.Stores; using Grand.SharedKernel.Extensions; using System.Net; @@ -23,16 +23,13 @@ public class ProductAttributeFormatter : IProductAttributeFormatter private readonly IProductAttributeService _productAttributeService; private readonly IProductService _productService; private readonly ITaxService _taxService; - private readonly IContextAccessor _contextAccessor; - public ProductAttributeFormatter(IContextAccessor contextAccessor, - IProductAttributeService productAttributeService, + public ProductAttributeFormatter(IProductAttributeService productAttributeService, ITaxService taxService, IPriceFormatter priceFormatter, IPricingService priceCalculationService, IProductService productService) { - _contextAccessor = contextAccessor; _productAttributeService = productAttributeService; _taxService = taxService; _priceFormatter = priceFormatter; @@ -45,19 +42,9 @@ public ProductAttributeFormatter(IContextAccessor contextAccessor, /// /// Product /// Attributes - /// Attributes - public virtual Task FormatAttributes(Product product, IList customAttributes) - { - var customer = _contextAccessor.WorkContext.CurrentCustomer; - return FormatAttributes(product, customAttributes, customer); - } - - /// - /// Formats attributes - /// - /// Product - /// Attributes - /// Customer + /// Customer, store, currency and tax display the prices are rendered for (required when renderPrices is true) + /// Language of the attribute names + /// Store address used for file download links (read only when allowHyperlinks is true; without it the link is not rendered) /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to render prices @@ -67,7 +54,8 @@ public virtual Task FormatAttributes(Product product, IListShow in admin /// Attributes public virtual async Task FormatAttributes(Product product, IList customAttributes, - Customer customer, string separator = "
", bool htmlEncode = true, bool renderPrices = true, + CalculationContext context, Language language, DomainHost host, + string separator = "
", bool htmlEncode = true, bool renderPrices = true, bool renderProductAttributes = true, bool renderGiftVoucherAttributes = true, bool allowHyperlinks = true, bool showInAdmin = false) { @@ -76,17 +64,15 @@ public virtual async Task FormatAttributes(Product product, IList(SystemCustomerFieldNames.LanguageId); + if (renderPrices) + ArgumentNullException.ThrowIfNull(context); - if (string.IsNullOrEmpty(langId)) - langId = ""; + var langId = language?.Id ?? ""; //attributes if (renderProductAttributes) { - result = await PrepareFormattedAttribute(product, customAttributes, customer, langId, separator, + result = await PrepareFormattedAttribute(product, customAttributes, context, langId, host, separator, htmlEncode, renderPrices, allowHyperlinks, showInAdmin); if (product.ProductTypeId == ProductType.BundledProduct) @@ -106,8 +92,8 @@ public virtual async Task FormatAttributes(Product product, IList x.Name, langId)) : p1.GetTranslation(x => x.Name, langId))}"); - var formattedAttribute = await PrepareFormattedAttribute(p1, customAttributes, customer, langId, - separator, htmlEncode, + var formattedAttribute = await PrepareFormattedAttribute(p1, customAttributes, context, langId, + host, separator, htmlEncode, renderPrices, allowHyperlinks, showInAdmin); if (formattedAttribute.Length > 0) { @@ -151,11 +137,10 @@ public virtual async Task FormatAttributes(Product product, IList PrepareFormattedAttribute(Product product, - IList customAttributes, Customer customer, string langId, - string separator, bool htmlEncode, bool renderPrices, + IList customAttributes, CalculationContext context, string langId, + DomainHost host, string separator, bool htmlEncode, bool renderPrices, bool allowHyperlinks, bool showInAdmin) { - var priceCustomer = customer ?? _contextAccessor.WorkContext.CurrentCustomer; var result = new StringBuilder(); var attributes = product.ParseProductAttributeMappings(customAttributes); for (var i = 0; i < attributes.Count; i++) @@ -194,10 +179,10 @@ private async Task PrepareFormattedAttribute(Product product, { var attributeText = string.Empty; var attributeName = productAttribute.GetTranslation(a => a.Name, langId); - 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, langId)}"; } @@ -236,25 +221,23 @@ private async Task PrepareFormattedAttribute(Product product, { var attributeValuePriceAdjustment = await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue, - priceCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + context.Customer, context.Store, context.Currency); var (priceAdjustmentBase, _) = await _taxService.GetProductPrice(product, attributeValuePriceAdjustment, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - priceCustomer, _contextAccessor.StoreContext.CurrentStore); + context.IncludingTax, context.Customer, context.Store); switch (priceAdjustmentBase) { case > 0: { var priceAdjustmentStr = _priceFormatter.FormatPrice(priceAdjustmentBase, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); formattedAttribute += $" [+{priceAdjustmentStr}]"; break; } case < 0: { var priceAdjustmentStr = _priceFormatter.FormatPrice(-priceAdjustmentBase, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); formattedAttribute += $" [-{priceAdjustmentStr}]"; break; } 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 6633308e16..f8e8280201 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -386,7 +386,9 @@ protected virtual async Task PreparePlaceOrderDetails(Place var details = new PlaceOrderContainer { Context = command.Context, Customer = command.Context.Customer, - Store = command.Context.Store + Store = command.Context.Store, + WorkingLanguage = command.Language, + Host = command.Host }; if (details.Customer == null) throw new ArgumentException("Customer is not set"); @@ -442,7 +444,7 @@ protected virtual async Task PreparePlaceOrderDetails(Place details.Store.Id); details.CheckoutAttributeDescription = await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, details.Context, - command.Language, command.Host); + details.WorkingLanguage, details.Host); //load and validate customer shopping cart details.Cart = details.Customer.ShoppingCartItems @@ -651,7 +653,8 @@ protected virtual async Task PrepareOrderItem(ShoppingCartItem sc, Pr details.AppliedDiscounts.All(x => x.DiscountId != disc.DiscountId))) details.AppliedDiscounts.Add(disc); var attributeDescription = - await _productAttributeFormatter.FormatAttributes(product, sc.Attributes, details.Customer); + await _productAttributeFormatter.FormatAttributes(product, sc.Attributes, details.Context, + details.WorkingLanguage, details.Host); var itemWeight = await GetShoppingCartItemWeight(sc); var warehouseId = !string.IsNullOrEmpty(sc.WarehouseId) ? sc.WarehouseId diff --git a/src/Business/Grand.Business.Core/Commands/Messages/Tokens/GetAttributeCombinationTokensCommand.cs b/src/Business/Grand.Business.Core/Commands/Messages/Tokens/GetAttributeCombinationTokensCommand.cs index 9aa1fdebe6..af8caf5d4c 100644 --- a/src/Business/Grand.Business.Core/Commands/Messages/Tokens/GetAttributeCombinationTokensCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Messages/Tokens/GetAttributeCombinationTokensCommand.cs @@ -1,5 +1,7 @@ using Grand.Business.Core.Utilities.Messages.DotLiquidDrops; using Grand.Domain.Catalog; +using Grand.Domain.Localization; +using Grand.Domain.Stores; using Grand.Mediator; namespace Grand.Business.Core.Commands.Messages.Tokens; @@ -8,4 +10,6 @@ public class GetAttributeCombinationTokensCommand : IRequest { - public Customer Customer { get; set; } - public Store Store { get; set; } + public CalculationContext Context { get; set; } + public DomainHost Host { get; set; } public Language Language { get; set; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeFormatter.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeFormatter.cs index 300d1255a5..4fc6842b84 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeFormatter.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeFormatter.cs @@ -1,6 +1,8 @@ +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; -using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; namespace Grand.Business.Core.Interfaces.Catalog.Products; @@ -14,15 +16,9 @@ public interface IProductAttributeFormatter /// /// Product /// Attributes - /// Attributes - Task FormatAttributes(Product product, IList customAttributes); - - /// - /// Formats attributes - /// - /// Product - /// Attributes - /// Customer + /// Customer, store, currency and tax display the prices are rendered for (required when renderPrices is true) + /// Language of the attribute names + /// Store address used for file download links (read only when allowHyperlinks is true; without it the link is not rendered) /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to render prices @@ -32,7 +28,8 @@ public interface IProductAttributeFormatter /// Show in admin /// Attributes Task FormatAttributes(Product product, IList customAttributes, - Customer customer, string separator = "
", bool htmlEncode = true, bool renderPrices = true, + CalculationContext context, Language language, DomainHost host, + string separator = "
", bool htmlEncode = true, bool renderPrices = true, bool renderProductAttributes = true, bool renderGiftVoucherAttributes = true, bool allowHyperlinks = true, bool showInAdmin = false); -} \ No newline at end of file +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Marketing/Campaigns/ICampaignService.cs b/src/Business/Grand.Business.Core/Interfaces/Marketing/Campaigns/ICampaignService.cs index c9ff1a9c9d..2cb3d104b8 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Marketing/Campaigns/ICampaignService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Marketing/Campaigns/ICampaignService.cs @@ -1,5 +1,8 @@ using Grand.Domain; +using Grand.Domain.Directory; using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Domain.Tax; namespace Grand.Business.Core.Interfaces.Marketing.Campaigns; @@ -65,9 +68,13 @@ Task> CustomerSubscriptions(Campaign campaign /// Campaign /// Email account /// Subscriptions + /// Currency the shopping cart prices are rendered in + /// Whether the shopping cart prices include tax + /// Store address used for the links in the email /// Total emails sent Task SendCampaign(Campaign campaign, EmailAccount emailAccount, - IEnumerable subscriptions); + IEnumerable subscriptions, Currency currency, TaxDisplayType taxDisplayType, + DomainHost host); /// /// Sends a campaign to specified email @@ -75,5 +82,9 @@ Task SendCampaign(Campaign campaign, EmailAccount emailAccount, /// Campaign /// Email account /// Email - Task SendCampaign(Campaign campaign, EmailAccount emailAccount, string email); + /// Currency the shopping cart prices are rendered in + /// Whether the shopping cart prices include tax + /// Store address used for the links in the email + Task SendCampaign(Campaign campaign, EmailAccount emailAccount, string email, Currency currency, + TaxDisplayType taxDisplayType, DomainHost host); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs b/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs index eb6bff6aa8..11d7624ce8 100644 --- a/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs +++ b/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs @@ -21,6 +21,17 @@ public class PlaceOrderContainer public Customer Customer { get; set; } public Store Store { get; set; } public Language Language { get; set; } + + /// + /// Language of the request placing the order, used for the attribute descriptions + /// + public Language WorkingLanguage { get; set; } + + /// + /// Store address used for file download links in the attribute descriptions + /// + public DomainHost Host { get; set; } + public Currency Currency { get; set; } public string AffiliateId { get; set; } public TaxDisplayType TaxDisplayType { get; set; } diff --git a/src/Business/Grand.Business.Core/Utilities/Messages/DotLiquidDrops/LiquidObjectBuilder.cs b/src/Business/Grand.Business.Core/Utilities/Messages/DotLiquidDrops/LiquidObjectBuilder.cs index 86bd80f41e..92ed2f3cac 100644 --- a/src/Business/Grand.Business.Core/Utilities/Messages/DotLiquidDrops/LiquidObjectBuilder.cs +++ b/src/Business/Grand.Business.Core/Utilities/Messages/DotLiquidDrops/LiquidObjectBuilder.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Commands.Messages.Tokens; using Grand.Business.Core.Extensions; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Blogs; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -122,18 +123,18 @@ public LiquidObjectBuilder AddCustomerTokens(Customer customer, Store store, Dom return this; } - public LiquidObjectBuilder AddShoppingCartTokens(Customer customer, Store store, Language language) + public LiquidObjectBuilder AddShoppingCartTokens(CalculationContext context, DomainHost host, Language language) { _chain.Add(async liquidObject => { var liquidShoppingCart = await _mediator.Send(new GetShoppingCartTokensCommand { - Customer = customer, - Language = language, - Store = store + Context = context, + Host = host, + Language = language }); liquidObject.ShoppingCart = liquidShoppingCart; - await _mediator.EntityTokensAdded(customer, liquidShoppingCart, liquidObject); + await _mediator.EntityTokensAdded(context.Customer, liquidShoppingCart, liquidObject); }); return this; } @@ -231,12 +232,13 @@ public LiquidObjectBuilder AddProductTokens(Product product, Language language, return this; } - public LiquidObjectBuilder AddAttributeCombinationTokens(Product product, ProductAttributeCombination combination) + public LiquidObjectBuilder AddAttributeCombinationTokens(Product product, ProductAttributeCombination combination, + Language language, DomainHost host) { _chain.Add(async liquidObject => { var liquidAttributeCombination = await _mediator.Send(new GetAttributeCombinationTokensCommand - { Product = product, Combination = combination }); + { Product = product, Combination = combination, Language = language, Host = host }); liquidObject.AttributeCombination = liquidAttributeCombination; await _mediator.EntityTokensAdded(combination, liquidAttributeCombination, liquidObject); }); diff --git a/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs b/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs index 2ec54e947c..cbaf6c2efc 100644 --- a/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs @@ -2,13 +2,16 @@ using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Marketing.Campaigns; using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Utilities.Checkout; using Grand.Business.Core.Utilities.Messages.DotLiquidDrops; using Grand.Data; using Grand.Domain; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Directory; using Grand.Domain.Messages; -using Grand.Infrastructure; +using Grand.Domain.Stores; +using Grand.Domain.Tax; using Grand.Infrastructure.Extensions; using Grand.Mediator; @@ -23,8 +26,7 @@ public class CampaignService( IQueuedEmailService queuedEmailService, IStoreService storeService, IMediator mediator, - ILanguageService languageService, - IContextAccessor contextAccessor) + ILanguageService languageService) : ICampaignService { /// @@ -224,9 +226,13 @@ from customers in joined /// Campaign /// Email account /// Subscriptions + /// Currency the shopping cart prices are rendered in + /// Whether the shopping cart prices include tax + /// Store address used for the links in the email /// Total emails sent public virtual async Task SendCampaign(Campaign campaign, EmailAccount emailAccount, - IEnumerable subscriptions) + IEnumerable subscriptions, Currency currency, TaxDisplayType taxDisplayType, + DomainHost host) { ArgumentNullException.ThrowIfNull(campaign); ArgumentNullException.ThrowIfNull(emailAccount); @@ -252,11 +258,12 @@ public virtual async Task SendCampaign(Campaign campaign, EmailAccount emai (await storeService.GetAllStores()).FirstOrDefault(); builder.AddStoreTokens(store, language, emailAccount) - .AddNewsLetterSubscriptionTokens(subscription, store, contextAccessor.StoreContext.CurrentHost); + .AddNewsLetterSubscriptionTokens(subscription, store, host); if (customer != null) - builder.AddCustomerTokens(customer, store, contextAccessor.StoreContext.CurrentHost, language) - .AddShoppingCartTokens(customer, store, language); + builder.AddCustomerTokens(customer, store, host, language) + .AddShoppingCartTokens(new CalculationContext(customer, store, currency, taxDisplayType), + host, language); var email = new QueuedEmail(); @@ -294,7 +301,11 @@ await InsertCampaignHistory(new CampaignHistory { /// Campaign /// Email account /// Email - public virtual async Task SendCampaign(Campaign campaign, EmailAccount emailAccount, string email) + /// Currency the shopping cart prices are rendered in + /// Whether the shopping cart prices include tax + /// Store address used for the links in the email + public virtual async Task SendCampaign(Campaign campaign, EmailAccount emailAccount, string email, + Currency currency, TaxDisplayType taxDisplayType, DomainHost host) { ArgumentNullException.ThrowIfNull(campaign); ArgumentNullException.ThrowIfNull(emailAccount); @@ -309,8 +320,9 @@ public virtual async Task SendCampaign(Campaign campaign, EmailAccount emailAcco builder.AddStoreTokens(store, language, emailAccount); var customer = customerRepository.Table.FirstOrDefault(x => x.Email == email.ToLowerInvariant()); if (customer != null) - builder.AddCustomerTokens(customer, store, contextAccessor.StoreContext.CurrentHost, language) - .AddShoppingCartTokens(customer, store, language); + builder.AddCustomerTokens(customer, store, host, language) + .AddShoppingCartTokens(new CalculationContext(customer, store, currency, taxDisplayType), host, + language); var liquidObject = await builder.BuildAsync(); var body = LiquidExtensions.Render(liquidObject, campaign.Body); diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetAttributeCombinationTokensCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetAttributeCombinationTokensCommandHandler.cs index edd1488158..50698c1ef2 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetAttributeCombinationTokensCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetAttributeCombinationTokensCommandHandler.cs @@ -21,7 +21,7 @@ public async Task Handle(GetAttributeCombinationToke { var liquidAttributeCombination = new LiquidAttributeCombination(request.Combination) { Formatted = await _productAttributeFormatter.FormatAttributes(request.Product, - request.Combination.Attributes, null, renderPrices: false), + request.Combination.Attributes, null, request.Language, request.Host, renderPrices: false), SKU = request.Product.FormatSku(request.Combination.Attributes) }; return liquidAttributeCombination; diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetShoppingCartTokensCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetShoppingCartTokensCommandHandler.cs index 2b1e0827d7..ae91564b77 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetShoppingCartTokensCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetShoppingCartTokensCommandHandler.cs @@ -25,29 +25,32 @@ public GetShoppingCartTokensCommandHandler( public async Task Handle(GetShoppingCartTokensCommand request, CancellationToken cancellationToken) { - var liquidShoppingCart = new LiquidShoppingCart(request.Customer); + var liquidShoppingCart = new LiquidShoppingCart(request.Context.Customer); await ProductList(); async Task ProductList() { - var attributeDescription = string.Empty; - var pictureUrl = string.Empty; - foreach (var item in request.Customer.ShoppingCartItems) + foreach (var item in request.Context.Customer.ShoppingCartItems) { var product = await _productService.GetProductById(item.ProductId); + if (product == null) + continue; + + var attributeDescription = string.Empty; + var pictureUrl = string.Empty; if (product.ProductPictures.Any()) pictureUrl = await _pictureService.GetPictureUrl( product.ProductPictures.OrderByDescending(p => p.IsDefault) .ThenBy(p => p.DisplayOrder) .FirstOrDefault()?.PictureId, 100, - storeLocation: request.Store.Url); + storeLocation: request.Context.Store.Url); //attributes if (item.Attributes != null && item.Attributes.Any()) attributeDescription = await _productAttributeFormatter.FormatAttributes(product, item.Attributes, - request.Customer); + request.Context, request.Language, request.Host); var result = new LiquidShoppingCartItem(product, attributeDescription, pictureUrl, item, request.Language); diff --git a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs index 71f990e6eb..4778a0c1ec 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs @@ -1328,7 +1328,7 @@ 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); + .AddAttributeCombinationTokens(product, combination, language, CurrentHost); var liquidObject = await builder.BuildAsync(); //event notification await _mediator.MessageTokensAdded(messageTemplate, liquidObject); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductAttributeFormatterTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductAttributeFormatterTests.cs index 182c421401..75864aa4c3 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductAttributeFormatterTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductAttributeFormatterTests.cs @@ -1,17 +1,15 @@ using Grand.Business.Catalog.Services.Products; -using Grand.Business.Common.Services.Security; using Grand.Business.Core.Interfaces.Catalog.Prices; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Catalog.Tax; -using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; using Grand.Domain.Stores; -using Grand.Infrastructure; -using Grand.Infrastructure.Configuration; -using Grand.Mediator; +using Grand.Domain.Tax; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -20,40 +18,47 @@ namespace Grand.Business.Catalog.Tests.Services.Products; [TestClass] public class ProductAttributeFormatterTests { - private IAclService _aclServiceMock; - private Mock _mediatorMock; private Mock _priceFormatterMock; private Mock _pricingServiceMock; private ProductAttributeFormatter _productAttributeFormatter; private Mock _productAttributeServiceMock; private Mock _productServiceMock; private Mock _taxServiceMock; - private Mock _translationServiceMock; - private Mock _workContextMock; + private CalculationContext _context; + private Language _language; + private DomainHost _host; [TestInitialize] public void InitializeTests() { - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _productAttributeServiceMock = new Mock(); _productAttributeServiceMock.Setup(x => x.GetProductAttributeById(It.IsAny())) - .Returns(Task.FromResult(new ProductAttribute { Name = "test" })); - _translationServiceMock = new Mock(); - _translationServiceMock.Setup(x => x.GetResource("GiftVoucherAttribute.From.Virtual")) - .Returns("Name {0}, Email {1}"); - _translationServiceMock.Setup(x => x.GetResource("GiftVoucherAttribute.For.Virtual")) - .Returns("RecName {0}, RecEmail {1}"); + .Returns(Task.FromResult(new ProductAttribute { + Name = "test", + Locales = { new TranslationEntity { LanguageId = "pl", LocaleKey = "Name", LocaleValue = "test-pl" } } + })); _taxServiceMock = new Mock(); _priceFormatterMock = new Mock(); _pricingServiceMock = new Mock(); _productServiceMock = new Mock(); - _mediatorMock = new Mock(); - _aclServiceMock = new AclService(new AccessControlConfig()); - _productAttributeFormatter = new ProductAttributeFormatter(_workContextMock.Object, - _productAttributeServiceMock.Object, + _productAttributeFormatter = new ProductAttributeFormatter(_productAttributeServiceMock.Object, _taxServiceMock.Object, _priceFormatterMock.Object, _pricingServiceMock.Object, _productServiceMock.Object); + + _context = new CalculationContext(new Customer { Id = "c1" }, new Store { Id = "s1" }, + new Currency { CurrencyCode = "EUR" }, TaxDisplayType.IncludingTax); + _language = new Language { Id = "en" }; + _host = new DomainHost { Url = "https://shop.example/" }; + } + + private static Product ProductWithCheckboxValue(double priceAdjustment = 0) + { + var product = new Product(); + var productAttributeMapping = new ProductAttributeMapping + { Id = "1", AttributeControlTypeId = AttributeControlType.Checkboxes }; + productAttributeMapping.ProductAttributeValues.Add(new ProductAttributeValue + { Id = "1", Name = "aa", PriceAdjustment = priceAdjustment }); + product.ProductAttributeMappings.Add(productAttributeMapping); + return product; } [TestMethod] @@ -62,7 +67,8 @@ public async Task FormatAttributesTest_EmptyCustomAttribute() //Arrange var product = new Product(); //Act - var result = await _productAttributeFormatter.FormatAttributes(product, new List()); + var result = await _productAttributeFormatter.FormatAttributes(product, new List(), + _context, _language, _host); //Assert Assert.AreEqual("", result); } @@ -79,7 +85,7 @@ public async Task FormatAttributesTest_GiftVoucher() new() { Key = "SenderName", Value = "Will" }, new() { Key = "SenderEmail", Value = "Will@will.com" }, new() { Key = "Message", Value = "" } - }); + }, _context, _language, _host); //Assert Assert.AreEqual("Will <Will@will.com>
John <John@john.com>", result); } @@ -88,16 +94,103 @@ public async Task FormatAttributesTest_GiftVoucher() public async Task FormatAttributesTest_CustomAttribute() { //Arrange - var product = new Product(); - var productAttributeMapping = new ProductAttributeMapping - { Id = "1", AttributeControlTypeId = AttributeControlType.Checkboxes }; - productAttributeMapping.ProductAttributeValues.Add(new ProductAttributeValue { Id = "1", Name = "aa" }); - product.ProductAttributeMappings.Add(productAttributeMapping); + var product = ProductWithCheckboxValue(); //Act var result = await _productAttributeFormatter.FormatAttributes(product, new List { new() { Key = "1", Value = "1" } - }); + }, _context, _language, _host); //Assert Assert.AreEqual("test: aa", result); } -} \ No newline at end of file + + [TestMethod] + public async Task FormatAttributes_TranslatesNamesIntoTheGivenLanguage() + { + //Arrange + var product = ProductWithCheckboxValue(); + //Act + var result = await _productAttributeFormatter.FormatAttributes(product, new List { + new() { Key = "1", Value = "1" } + }, _context, new Language { Id = "pl" }, _host); + //Assert + Assert.AreEqual("test-pl: aa", result); + } + + [TestMethod] + public async Task FormatAttributes_PricesTheAdjustmentForTheGivenContext() + { + //Arrange + var product = ProductWithCheckboxValue(5); + _pricingServiceMock.Setup(x => x.GetProductAttributeValuePriceAdjustment(It.IsAny(), + _context.Customer, _context.Store, _context.Currency)) + .ReturnsAsync(5); + _taxServiceMock.Setup(x => x.GetProductPrice(product, 5, true, _context.Customer, _context.Store)) + .ReturnsAsync((6, 20)); + _priceFormatterMock.Setup(x => x.FormatPrice(6, _context.Currency)).Returns("6 EUR"); + //Act + var result = await _productAttributeFormatter.FormatAttributes(product, new List { + new() { Key = "1", Value = "1" } + }, _context, _language, _host); + //Assert + Assert.AreEqual("test: aa [+6 EUR]", result); + } + + [TestMethod] + public async Task FormatAttributes_WithoutPrices_DoesNotNeedAContext() + { + //Arrange + var product = ProductWithCheckboxValue(5); + //Act + var result = await _productAttributeFormatter.FormatAttributes(product, new List { + new() { Key = "1", Value = "1" } + }, null, _language, _host, renderPrices: false); + //Assert + Assert.AreEqual("test: aa", result); + _pricingServiceMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task FormatAttributes_FileUpload_LinksToTheGivenHost() + { + //Arrange + var product = new Product(); + product.ProductAttributeMappings.Add(new ProductAttributeMapping + { Id = "1", AttributeControlTypeId = AttributeControlType.FileUpload, TextPrompt = "file" }); + var downloadGuid = Guid.NewGuid(); + //Act + var result = await _productAttributeFormatter.FormatAttributes(product, new List { + new() { Key = "1", Value = downloadGuid.ToString() } + }, _context, _language, _host); + //Assert + Assert.AreEqual( + $"test: file", + result); + } + + [TestMethod] + public async Task FormatAttributes_FileUpload_WithoutHost_RendersNoLink() + { + //Arrange + var product = new Product(); + product.ProductAttributeMappings.Add(new ProductAttributeMapping + { Id = "1", AttributeControlTypeId = AttributeControlType.FileUpload, TextPrompt = "file" }); + //Act + var result = await _productAttributeFormatter.FormatAttributes(product, new List { + new() { Key = "1", Value = Guid.NewGuid().ToString() } + }, _context, _language, null); + //Assert + Assert.AreEqual("test: ", result); + } + + [TestMethod] + public async Task FormatAttributes_WithPrices_RequiresAContext() + { + //Arrange + var product = ProductWithCheckboxValue(5); + //Act & Assert + await Assert.ThrowsExactlyAsync(() => + _productAttributeFormatter.FormatAttributes(product, new List { + new() { Key = "1", Value = "1" } + }, null, _language, _host)); + } +} diff --git a/src/Tests/Grand.Business.Marketing.Tests/Services/Campaigns/CampaignServiceTests.cs b/src/Tests/Grand.Business.Marketing.Tests/Services/Campaigns/CampaignServiceTests.cs index 99a643c39f..ca32ce49fa 100644 --- a/src/Tests/Grand.Business.Marketing.Tests/Services/Campaigns/CampaignServiceTests.cs +++ b/src/Tests/Grand.Business.Marketing.Tests/Services/Campaigns/CampaignServiceTests.cs @@ -5,10 +5,11 @@ using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; +using Grand.Domain.Directory; using Grand.Domain.Localization; using Grand.Domain.Messages; using Grand.Domain.Stores; -using Grand.Infrastructure; +using Grand.Domain.Tax; using Grand.Infrastructure.Events; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -29,7 +30,6 @@ public class CampaignServiceTests private IRepository _newsLetterSubscriptionRepository; private Mock _queuedEmailServiceMock; private Mock _storeServiceMock; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -43,11 +43,10 @@ public void Init() _queuedEmailServiceMock = new Mock(); _storeServiceMock = new Mock(); _languageServiceMock = new Mock(); - _workContextMock = new Mock(); _campaignService = new CampaignService(_campaignRepository, _campaignHistoryRepository, _newsLetterSubscriptionRepository, _customerRepository, _emailSenderMock.Object, _queuedEmailServiceMock.Object, _storeServiceMock.Object, _mediatorMock.Object, - _languageServiceMock.Object, _workContextMock.Object); + _languageServiceMock.Object); } [TestMethod] @@ -184,7 +183,8 @@ await _newsLetterSubscriptionRepository.InsertAsync(new NewsLetterSubscription .Returns(Task.FromResult(new Store())); //Act - await _campaignService.SendCampaign(campaign, new EmailAccount { Host = "host.com" }, "test@test.com"); + await _campaignService.SendCampaign(campaign, new EmailAccount { Host = "host.com" }, "test@test.com", + new Currency(), TaxDisplayType.IncludingTax, new DomainHost { Url = "https://localhost/" }); //Assert _emailSenderMock.Verify( diff --git a/src/Tests/Grand.Business.Messages.Tests/Commands/GetShoppingCartTokensCommandHandlerTests.cs b/src/Tests/Grand.Business.Messages.Tests/Commands/GetShoppingCartTokensCommandHandlerTests.cs new file mode 100644 index 0000000000..3487e3b1a4 --- /dev/null +++ b/src/Tests/Grand.Business.Messages.Tests/Commands/GetShoppingCartTokensCommandHandlerTests.cs @@ -0,0 +1,89 @@ +using Grand.Business.Core.Commands.Messages.Tokens; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Checkout; +using Grand.Business.Messages.Commands.Handlers.Messages; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Messages.Tests.Commands; + +[TestClass] +public class GetShoppingCartTokensCommandHandlerTests +{ + [TestMethod] + public async Task Handle_FormatsAttributesForTheMessageContextLanguageAndHost() + { + //Arrange + var product = new Product { Id = "p1" }; + var productService = new Mock(); + productService.Setup(x => x.GetProductById("p1", false)).ReturnsAsync(product); + var formatter = new Mock(); + var customer = new Customer(); + var item = new ShoppingCartItem { ProductId = "p1" }; + item.Attributes.Add(new CustomAttribute { Key = "1", Value = "1" }); + customer.ShoppingCartItems.Add(item); + var command = new GetShoppingCartTokensCommand { + Context = new CalculationContext(customer, new Store { Id = "s1" }, new Currency(), + TaxDisplayType.ExcludingTax), + Language = new Language { Id = "pl" }, + Host = new DomainHost { Url = "https://shop.example/" } + }; + var handler = new GetShoppingCartTokensCommandHandler(new Mock().Object, + productService.Object, formatter.Object); + + //Act + await handler.Handle(command, CancellationToken.None); + + //Assert + formatter.Verify(x => x.FormatAttributes(product, item.Attributes, command.Context, command.Language, + command.Host, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task Handle_ItemWithoutAttributes_DoesNotRepeatThePreviousItemsDescription_AndSkipsDeletedProducts() + { + //Arrange + var product = new Product { Id = "p1" }; + var productService = new Mock(); + productService.Setup(x => x.GetProductById("p1", false)).ReturnsAsync(product); + var formatter = new Mock(); + formatter.Setup(x => x.FormatAttributes(product, It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync("Size: XL"); + var customer = new Customer(); + var withAttributes = new ShoppingCartItem { ProductId = "p1" }; + withAttributes.Attributes.Add(new CustomAttribute { Key = "1", Value = "1" }); + customer.ShoppingCartItems.Add(withAttributes); + customer.ShoppingCartItems.Add(new ShoppingCartItem { ProductId = "deleted" }); + customer.ShoppingCartItems.Add(new ShoppingCartItem { ProductId = "p1" }); + var command = new GetShoppingCartTokensCommand { + Context = new CalculationContext(customer, new Store { Id = "s1" }, new Currency(), + TaxDisplayType.ExcludingTax), + Language = new Language { Id = "pl" }, + Host = new DomainHost { Url = "https://shop.example/" } + }; + var handler = new GetShoppingCartTokensCommandHandler(new Mock().Object, + productService.Object, formatter.Object); + + //Act + var result = await handler.Handle(command, CancellationToken.None); + + //Assert + var items = result.Items.ToList(); + Assert.HasCount(2, items); + Assert.AreEqual("Size: XL", items[0].AttributeDescription); + Assert.AreEqual(string.Empty, items[1].AttributeDescription); + } +} diff --git a/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs b/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs index 9ccc2cc185..506dcce1e3 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs @@ -213,12 +213,16 @@ await _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmailAndStoreId( var subscriptions = new List { subscription }; - await _campaignService.SendCampaign(campaign, emailAccount, subscriptions); + await _campaignService.SendCampaign(campaign, emailAccount, subscriptions, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType, + _contextAccessor.StoreContext.CurrentHost); } else { //no subscription found - await _campaignService.SendCampaign(campaign, emailAccount, model.TestEmail); + await _campaignService.SendCampaign(campaign, emailAccount, model.TestEmail, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType, + _contextAccessor.StoreContext.CurrentHost); } Success(_translationService.GetResource("admin.marketing.Campaigns.TestEmailSentToCustomers")); @@ -254,7 +258,9 @@ public async Task SendMassEmail(CampaignModel model) //subscribers of certain store? var store = await _storeService.GetStoreById(campaign.StoreId); var subscriptions = await _campaignService.CustomerSubscriptions(campaign); - var totalEmailsSent = await _campaignService.SendCampaign(campaign, emailAccount, subscriptions); + var totalEmailsSent = await _campaignService.SendCampaign(campaign, emailAccount, subscriptions, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType, + _contextAccessor.StoreContext.CurrentHost); Success( string.Format(_translationService.GetResource("admin.marketing.Campaigns.MassEmailSentToCustomers"), totalEmailsSent)); diff --git a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs index 573f7193e7..c5bb98aa72 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs @@ -13,6 +13,7 @@ using Grand.Web.Common.DataSource; using Grand.Web.Common.Security.Authorization; using Microsoft.AspNetCore.Mvc; +using Grand.Web.Common.Extensions; namespace Grand.Web.Admin.Controllers; @@ -112,7 +113,9 @@ public async Task GetCartDetails(string customerId) ProductId = sci.ProductId, Quantity = sci.Quantity, ProductName = product.Name, - AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), + AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, + _contextAccessor.CalculationContext() with { Customer = customer, Store = priceStore }, + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, customer, priceStore, @@ -184,7 +187,9 @@ public async Task GetWishlistDetails(string customerId) ProductId = sci.ProductId, Quantity = sci.Quantity, ProductName = product.Name, - AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), + AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, + _contextAccessor.CalculationContext() with { Customer = customer, Store = priceStore }, + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, customer, priceStore, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseReportsController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseReportsController.cs index 0e86766fcf..ffa261a35b 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseReportsController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseReportsController.cs @@ -341,7 +341,8 @@ public virtual async Task LowStockReportList(DataSourceRequest co Id = product.Id, Name = product.Name, Attributes = await productAttributeFormatter.FormatAttributes(product, combination.Attributes, - contextAccessor.WorkContext.CurrentCustomer, "
", true, true, true, false), + contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, + contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false), ManageInventoryMethod = enumTranslationService.GetTranslationEnum(product.ManageInventoryMethodId), StockQuantity = combination.StockQuantity, Published = product.Published diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index 4aab4bf264..0d1319c084 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -874,7 +874,9 @@ public virtual async Task> PrepareShoppingCartItemM ProductId = sci.ProductId, Quantity = sci.Quantity, ProductName = product.Name, - AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), + AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes, + _contextAccessor.CalculationContext() with { Customer = customer, Store = priceStore }, + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost), UnitPrice = priceFormatter.FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency), UnitPriceValue = price, Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, @@ -1105,9 +1107,11 @@ await outOfStockSubscriptionService.GetAllSubscriptionsByCustomerId(customerId, StoreName = store != null ? store.Shortcut : "Unknown", ProductId = x.ProductId, ProductName = product != null ? product.Name : "Unknown", - AttributeDescription = x.Attributes != null || !x.Attributes!.Any() + AttributeDescription = x.Attributes == null || !x.Attributes.Any() ? "" - : await productAttributeFormatter.FormatAttributes(product, x.Attributes), + : await productAttributeFormatter.FormatAttributes(product, x.Attributes, + _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, + _contextAccessor.StoreContext.CurrentHost), CreatedOn = _dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) }; items.Add(m); diff --git a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs index 3ac50711c3..ab9b76fbbf 100644 --- a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs @@ -16,6 +16,7 @@ using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Directory; @@ -34,6 +35,7 @@ using System.Net; using ProductExtensions = Grand.Domain.Catalog.ProductExtensions; using Grand.Business.Core.Queries.Checkout.Orders; +using Grand.Web.Common.Extensions; namespace Grand.Web.AdminShared.Services; @@ -79,6 +81,7 @@ public class OrderViewModelService : IOrderViewModelService private readonly IMediator _mediator; private readonly IEnumTranslationService _enumTranslationService; private readonly IAdminDataScope _scope; + private readonly ILanguageService _languageService; #endregion @@ -119,7 +122,8 @@ public OrderViewModelService(IOrderService orderService, IOrderStatusService orderStatusService, IMediator mediator, IProductAttributeFormatter productAttributeFormatter, IEnumTranslationService enumTranslationService, - IAdminDataScope scope) + IAdminDataScope scope, + ILanguageService languageService) { _orderService = orderService; _pricingService = priceCalculationService; @@ -158,6 +162,7 @@ public OrderViewModelService(IOrderService orderService, _productAttributeFormatter = productAttributeFormatter; _enumTranslationService = enumTranslationService; _scope = scope; + _languageService = languageService; } #endregion @@ -1159,8 +1164,17 @@ await _shoppingCartValidator.GetShoppingCartItemGiftVoucherWarnings(customer, pr shoppingCartItem)); if (warnings.Count == 0) { + //the description is stored on the order, so it is rendered for the order, not for the admin + var store = await _storeService.GetStoreById(order.StoreId) ?? _contextAccessor.StoreContext.CurrentStore; + var currency = await _currencyService.GetCurrencyByCode(order.CustomerCurrencyCode) ?? + _contextAccessor.WorkContext.WorkingCurrency; + var language = await _languageService.GetLanguageById(order.CustomerLanguageId) ?? + _contextAccessor.WorkContext.WorkingLanguage; + var host = store.Domains.FirstOrDefault(x => x.Primary) ?? store.Domains.FirstOrDefault(); var attributeDescription = - await _productAttributeFormatter.FormatAttributes(product, customattributes, customer); + await _productAttributeFormatter.FormatAttributes(product, customattributes, + new CalculationContext(customer, store, currency, order.CustomerTaxDisplayTypeId), + language, host); //save item var orderItem = new OrderItem { diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index 0de14ba26a..1bff05291b 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -2007,7 +2007,8 @@ public virtual async Task> foreach (var x in product.ProductAttributeCombinations) { var attributes = await productAttributeFormatter.FormatAttributes(product, x.Attributes, - contextAccessor.WorkContext.CurrentCustomer, "
", true, true, true, false, true, true); + contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, + contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false, true, true); var pacModel = new ProductModel.ProductAttributeCombinationModel { Id = x.Id, ProductId = product.Id, @@ -2058,7 +2059,8 @@ public virtual async Task PrepareProductAttrib model.WarehouseInventoryModels = wim; model.ProductId = product.Id; model.Attributes = await productAttributeFormatter.FormatAttributes(product, combination.Attributes, - contextAccessor.WorkContext.CurrentCustomer, "
", true, true, true, false); + contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, + contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false); if (model.UseMultipleWarehouses) foreach (var _winv in combination.WarehouseInventory) { diff --git a/src/Web/Grand.Web/Controllers/ActionCartController.cs b/src/Web/Grand.Web/Controllers/ActionCartController.cs index 5eeac268e1..6bb4f3b5ea 100644 --- a/src/Web/Grand.Web/Controllers/ActionCartController.cs +++ b/src/Web/Grand.Web/Controllers/ActionCartController.cs @@ -241,7 +241,8 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, Currency = _contextAccessor.WorkContext.WorkingCurrency, Store = _contextAccessor.StoreContext.CurrentStore, Language = _contextAccessor.WorkContext.WorkingLanguage, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType + TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, + Host = _contextAccessor.StoreContext.CurrentHost }); //added to the cart/wishlist @@ -300,7 +301,8 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, - Store = _contextAccessor.StoreContext.CurrentStore + Store = _contextAccessor.StoreContext.CurrentStore, + Host = _contextAccessor.StoreContext.CurrentHost }) : null; @@ -433,6 +435,7 @@ public virtual async Task> AddProductDetails(Pr Store = _contextAccessor.StoreContext.CurrentStore, Language = _contextAccessor.WorkContext.WorkingLanguage, TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, + Host = _contextAccessor.StoreContext.CurrentHost, Duration = duration, Parameter = parameter, ReservationId = model.Reservation, @@ -496,7 +499,8 @@ public virtual async Task> AddProductDetails(Pr Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, - Store = _contextAccessor.StoreContext.CurrentStore + Store = _contextAccessor.StoreContext.CurrentStore, + Host = _contextAccessor.StoreContext.CurrentHost }) : null; @@ -778,7 +782,8 @@ await auctionService.NewBid(customer, product, _contextAccessor.StoreContext.Cur Currency = _contextAccessor.WorkContext.WorkingCurrency, Store = _contextAccessor.StoreContext.CurrentStore, Language = _contextAccessor.WorkContext.WorkingLanguage, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType + TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, + Host = _contextAccessor.StoreContext.CurrentHost }); return Json(new { diff --git a/src/Web/Grand.Web/Controllers/OutOfStockSubscriptionController.cs b/src/Web/Grand.Web/Controllers/OutOfStockSubscriptionController.cs index 0e0d8ed204..06ef2772a2 100644 --- a/src/Web/Grand.Web/Controllers/OutOfStockSubscriptionController.cs +++ b/src/Web/Grand.Web/Controllers/OutOfStockSubscriptionController.cs @@ -13,6 +13,7 @@ using Grand.Web.Models.Catalog; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; +using Grand.Web.Common.Extensions; namespace Grand.Web.Controllers; @@ -181,7 +182,9 @@ public virtual async Task SubscribePopup(ProductModel model) Attributes = attributes, AttributeInfo = !attributes.Any() ? "" - : await _productAttributeFormatter.FormatAttributes(product, attributes), + : await _productAttributeFormatter.FormatAttributes(product, attributes, + _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, + _contextAccessor.StoreContext.CurrentHost), StoreId = _contextAccessor.StoreContext.CurrentStore.Id, WarehouseId = warehouseId }; @@ -222,7 +225,9 @@ public virtual async Task CustomerSubscriptions(int? pageNumber) ProductName = product.GetTranslation(x => x.Name, _contextAccessor.WorkContext.WorkingLanguage.Id), AttributeDescription = !subscription.Attributes.Any() ? "" - : await _productAttributeFormatter.FormatAttributes(product, subscription.Attributes), + : await _productAttributeFormatter.FormatAttributes(product, subscription.Attributes, + _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, + _contextAccessor.StoreContext.CurrentHost), SeName = product.GetSeName(_contextAccessor.WorkContext.WorkingLanguage.Id) }; model.Subscriptions.Add(subscriptionModel); diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index 775b737b8a..2a2e609fc4 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -132,7 +132,8 @@ public async Task> SidebarShoppingCart() Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, - Store = _contextAccessor.StoreContext.CurrentStore + Store = _contextAccessor.StoreContext.CurrentStore, + Host = _contextAccessor.StoreContext.CurrentHost }); return Json(model); } @@ -380,7 +381,8 @@ await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.C Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, - Store = _contextAccessor.StoreContext.CurrentStore + Store = _contextAccessor.StoreContext.CurrentStore, + Host = _contextAccessor.StoreContext.CurrentHost }); if (!model.ShoppingCartPage) return Json(new { @@ -429,7 +431,8 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, - Store = _contextAccessor.StoreContext.CurrentStore + Store = _contextAccessor.StoreContext.CurrentStore, + Host = _contextAccessor.StoreContext.CurrentHost }); var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 199bc649e7..458f171a6f 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -72,7 +72,9 @@ public async Task> SidebarWishlist() Customer = _contextAccessor.WorkContext.CurrentCustomer, Language = _contextAccessor.WorkContext.WorkingLanguage, Currency = _contextAccessor.WorkContext.WorkingCurrency, - Store = _contextAccessor.StoreContext.CurrentStore + Store = _contextAccessor.StoreContext.CurrentStore, + TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, + Host = _contextAccessor.StoreContext.CurrentHost }); return Json(model); @@ -96,11 +98,13 @@ public virtual async Task> Index(Guid? customerGuid) var model = await _mediator.Send(new GetWishlist { Cart = cart, Customer = customer, + CurrentCustomer = _contextAccessor.WorkContext.CurrentCustomer, Language = _contextAccessor.WorkContext.WorkingLanguage, Currency = _contextAccessor.WorkContext.WorkingCurrency, Store = _contextAccessor.StoreContext.CurrentStore, IsEditable = !customerGuid.HasValue, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType + TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, + Host = _contextAccessor.StoreContext.CurrentHost }); return View(model); diff --git a/src/Web/Grand.Web/Extensions/CartCalculationRequestExtensions.cs b/src/Web/Grand.Web/Extensions/CartCalculationRequestExtensions.cs new file mode 100644 index 0000000000..55a577ef72 --- /dev/null +++ b/src/Web/Grand.Web/Extensions/CartCalculationRequestExtensions.cs @@ -0,0 +1,15 @@ +using Grand.Business.Core.Utilities.Checkout; +using Grand.Web.Features.Models.ShoppingCart; + +namespace Grand.Web.Extensions; + +public static class CartCalculationRequestExtensions +{ + /// + /// Gets the calculation context of a cart request: its customer, store, currency and tax display + /// + public static CalculationContext CalculationContext(this ICartCalculationRequest request) + { + return new CalculationContext(request.Customer, request.Store, request.Currency, request.TaxDisplayType); + } +} diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index c5a80f3bc4..5588c93d1a 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -68,7 +68,9 @@ public async Task Handle(GetAddToCart request, CancellationToken { var model = new AddToCartModel { AttributeDescription = - await _productAttributeFormatter.FormatAttributes(request.Product, request.Attributes), + await _productAttributeFormatter.FormatAttributes(request.Product, request.Attributes, + request.CalculationContext(), + request.Language, request.Host), ProductSeName = request.Product.GetSeName(request.Language.Id), CartType = request.CartType, ProductId = request.Product.Id, @@ -158,8 +160,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - new CalculationContext(request.Customer, request.Store, request.Currency, - request.TaxDisplayType), subTotalIncludingTax); + request.CalculationContext(), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 40fb640ec8..89f6d407b5 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -108,8 +108,7 @@ public async Task Handle(GetMiniShoppingCart request, model.TotalProducts = cart.Sum(x => x.Quantity); if (!cart.Any()) return model; - var context = new CalculationContext(request.Customer, request.Store, request.Currency, - request.TaxDisplayType); + var context = request.CalculationContext(); //subtotal var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && @@ -152,7 +151,8 @@ public async Task Handle(GetMiniShoppingCart request, ProductSeName = sename, ProductUrl = _linkGenerator.GetPathByRouteValues("Product", new { SeName = sename }), Quantity = sci.Quantity, - AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes) + AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, context, + request.Language, request.Host) }; if (product.ProductTypeId == ProductType.Reservation) { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs index 39f4fb03ed..3ef17125b7 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs @@ -10,6 +10,7 @@ using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -20,7 +21,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetMiniWishlistHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly LinkGenerator _linkGenerator; private readonly MediaSettings _mediaSettings; private readonly IPictureService _pictureService; @@ -42,10 +42,8 @@ public GetMiniWishlistHandler( IPictureService pictureService, LinkGenerator linkGenerator, ShoppingCartSettings shoppingCartSettings, - MediaSettings mediaSettings, - IContextAccessor contextAccessor) + MediaSettings mediaSettings) { - _contextAccessor = contextAccessor; _productService = productService; _productAttributeFormatter = productAttributeFormatter; _translationService = translationService; @@ -71,6 +69,7 @@ public async Task Handle(GetMiniWishlist request, Cancellatio #region Cart items + var context = request.CalculationContext(); foreach (var sci in request.Cart) { var product = await _productService.GetProductById(sci.ProductId); @@ -86,7 +85,8 @@ public async Task Handle(GetMiniWishlist request, Cancellatio ProductSeName = sename, ProductUrl = _linkGenerator.GetPathByRouteValues("Product", new { SeName = sename }), Quantity = sci.Quantity, - AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes) + AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, + context, request.Language, request.Host) }; //unit prices @@ -98,10 +98,8 @@ public async Task Handle(GetMiniWishlist request, Cancellatio { var productprice = await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency)).unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + context.Customer, context.Store, context.Currency)).unitprice, + context.IncludingTax, context.Customer, context.Store); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); cartItemModel.UnitPriceValue = productprice.productprice; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 82712141ea..3d549b7822 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -146,9 +146,6 @@ public async Task Handle(GetShoppingCart request, Cancellatio return model; } - private static CalculationContext CalculationContextOf(GetShoppingCart request) => - new(request.Customer, request.Store, request.Currency, request.TaxDisplayType); - private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingCart request) { #region Simple properties @@ -164,7 +161,7 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC request.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, request.Store.Id); model.CheckoutAttributeInfo = - await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, CalculationContextOf(request), + await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, request.CalculationContext(), request.Language, request.Host); if (!request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) .ToList().Any()) @@ -174,7 +171,7 @@ await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, Calculati else { var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { - Context = CalculationContextOf(request), + Context = request.CalculationContext(), Cart = request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() }); @@ -209,7 +206,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu //cart warnings var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, - CalculationContextOf(request), + request.CalculationContext(), request.ValidateCheckoutAttributes, true); foreach (var warning in cartWarnings) model.Warnings.Add(warning); @@ -364,7 +361,8 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req ProductSeName = sename, ProductUrl = _linkGenerator.GetPathByRouteValues("Product", new { SeName = sename }), Quantity = sci.Quantity, - AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes), + AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, + request.CalculationContext(), request.Language, request.Host), AllowItemEditing = _shoppingCartSettings.AllowCartItemEditing && product.VisibleIndividually }; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index 472408f59a..139b2d6859 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -28,7 +28,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetWishlistHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly CatalogSettings _catalogSettings; private readonly LinkGenerator _linkGenerator; private readonly MediaSettings _mediaSettings; @@ -57,10 +56,8 @@ public GetWishlistHandler( ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings, MediaSettings mediaSettings, - IEnumTranslationService enumTranslationService, - IContextAccessor contextAccessor) + IEnumTranslationService enumTranslationService) { - _contextAccessor = contextAccessor; _permissionService = permissionService; _productService = productService; _productAttributeFormatter = productAttributeFormatter; @@ -77,9 +74,6 @@ public GetWishlistHandler( _enumTranslationService = enumTranslationService; } - private static CalculationContext CalculationContextOf(GetWishlist request) => - new(request.Customer, request.Store, request.Currency, request.TaxDisplayType); - public async Task Handle(GetWishlist request, CancellationToken cancellationToken) { var model = new WishlistModel { @@ -100,7 +94,7 @@ public async Task Handle(GetWishlist request, CancellationToken c //cart warnings var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, - CalculationContextOf(request), false, false); + request.CalculationContext(), false, false); foreach (var warning in cartWarnings) model.Warnings.Add(warning); @@ -108,6 +102,8 @@ public async Task Handle(GetWishlist request, CancellationToken c #region Cart items + //on a shared wishlist the prices are the viewer's, not the owner's + var priceContext = request.CalculationContext() with { Customer = request.CurrentCustomer }; foreach (var sci in request.Cart) { var product = await _productService.GetProductById(sci.ProductId); @@ -123,7 +119,8 @@ public async Task Handle(GetWishlist request, CancellationToken c ProductSeName = sename, ProductUrl = _linkGenerator.GetPathByRouteValues("Product", new { SeName = sename }), Quantity = sci.Quantity, - AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes), + AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, + priceContext, request.Language, request.Host), AllowItemEditing = _shoppingCartSettings.AllowCartItemEditing && product.VisibleIndividually }; @@ -153,10 +150,8 @@ public async Task Handle(GetWishlist request, CancellationToken c { var productprice = await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency)).unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + priceContext.Customer, priceContext.Store, priceContext.Currency)).unitprice, + priceContext.IncludingTax, priceContext.Customer, priceContext.Store); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); } @@ -169,12 +164,10 @@ public async Task Handle(GetWishlist request, CancellationToken c { //sub total var subtotal = await _pricingService.GetSubTotal(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + priceContext.Customer, priceContext.Store, priceContext.Currency); var shoppingCartItemDiscountBase = subtotal.discountAmount; var productprices = await _taxService.GetProductPrice(product, subtotal.subTotal, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + priceContext.IncludingTax, priceContext.Customer, priceContext.Store); cartItemModel.SubTotal = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); //display an applied discount amount @@ -182,8 +175,7 @@ public async Task Handle(GetWishlist request, CancellationToken c { shoppingCartItemDiscountBase = (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + priceContext.IncludingTax, priceContext.Customer, priceContext.Store)).productprice; if (shoppingCartItemDiscountBase > 0) cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetAddToCart.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetAddToCart.cs index 87b258028f..70401cec89 100644 --- a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetAddToCart.cs +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetAddToCart.cs @@ -11,13 +11,18 @@ namespace Grand.Web.Features.Models.ShoppingCart; -public class GetAddToCart : IRequest +public class GetAddToCart : IRequest, ICartCalculationRequest { public Product Product { get; set; } public Customer Customer { get; set; } public Language Language { get; set; } public Currency Currency { get; set; } public Domain.Stores.Store Store { get; set; } + + /// + /// Store address of the request, used for the download links of product attributes + /// + public DomainHost Host { get; set; } public ShoppingCartItem ShoppingCartItem { get; set; } public TaxDisplayType TaxDisplayType { get; set; } public int Quantity { get; set; } diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniShoppingCart.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniShoppingCart.cs index 5c1967976e..8f2a18e432 100644 --- a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniShoppingCart.cs +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniShoppingCart.cs @@ -8,11 +8,16 @@ namespace Grand.Web.Features.Models.ShoppingCart; -public class GetMiniShoppingCart : IRequest +public class GetMiniShoppingCart : IRequest, ICartCalculationRequest { public Customer Customer { get; set; } public Language Language { get; set; } public Currency Currency { get; set; } public Domain.Stores.Store Store { get; set; } + + /// + /// Store address of the request, used for the download links of product attributes + /// + public DomainHost Host { get; set; } public TaxDisplayType TaxDisplayType { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniWishlist.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniWishlist.cs index a90480f3e8..5affcbc6e0 100644 --- a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniWishlist.cs +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetMiniWishlist.cs @@ -3,16 +3,23 @@ using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; +using Grand.Domain.Tax; using Grand.Web.Models.ShoppingCart; using Grand.Mediator; namespace Grand.Web.Features.Models.ShoppingCart; -public class GetMiniWishlist : IRequest +public class GetMiniWishlist : IRequest, ICartCalculationRequest { public Customer Customer { get; set; } public Language Language { get; set; } public Currency Currency { get; set; } public Domain.Stores.Store Store { get; set; } + + /// + /// Store address of the request, used for the download links of product attributes + /// + public DomainHost Host { get; set; } + public TaxDisplayType TaxDisplayType { get; set; } public IList Cart { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs index ce49bd7fce..bb2e65e6a0 100644 --- a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs @@ -9,7 +9,7 @@ namespace Grand.Web.Features.Models.ShoppingCart; -public class GetShoppingCart : IRequest +public class GetShoppingCart : IRequest, ICartCalculationRequest { public Customer Customer { get; set; } public Language Language { get; set; } @@ -17,7 +17,7 @@ public class GetShoppingCart : IRequest public Domain.Stores.Store Store { get; set; } /// - /// Store address of the request, used for the download links of checkout attributes + /// Store address of the request, used for the download links of checkout and product attributes /// public DomainHost Host { get; set; } diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetWishlist.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetWishlist.cs index 9b7cf35fd3..5317cb0f73 100644 --- a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetWishlist.cs +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetWishlist.cs @@ -9,12 +9,25 @@ namespace Grand.Web.Features.Models.ShoppingCart; -public class GetWishlist : IRequest +public class GetWishlist : IRequest, ICartCalculationRequest { + /// + /// Owner of the wishlist + /// public Customer Customer { get; set; } + + /// + /// Customer viewing the wishlist; prices are calculated for them, which differs from Customer on a shared wishlist + /// + public Customer CurrentCustomer { get; set; } public Language Language { get; set; } public Currency Currency { get; set; } public Domain.Stores.Store Store { get; set; } + + /// + /// Store address of the request, used for the download links of product attributes + /// + public DomainHost Host { get; set; } public TaxDisplayType TaxDisplayType { get; set; } public bool IsEditable { get; set; } = true; public IList Cart { get; set; } diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/ICartCalculationRequest.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/ICartCalculationRequest.cs new file mode 100644 index 0000000000..f611da0252 --- /dev/null +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/ICartCalculationRequest.cs @@ -0,0 +1,16 @@ +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Tax; + +namespace Grand.Web.Features.Models.ShoppingCart; + +/// +/// Cart request that carries the values a calculation context is built from +/// +public interface ICartCalculationRequest +{ + Customer Customer { get; } + Domain.Stores.Store Store { get; } + Currency Currency { get; } + TaxDisplayType TaxDisplayType { get; } +}