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; }
+}