Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2f4c39d
Pass the customer explicitly when listing brands
Oct 16, 2026
9959df8
Pass the customer and store explicitly when listing collections
Oct 16, 2026
ff002d8
Pass the customer and store explicitly to category queries
Oct 16, 2026
ceec7b9
Pass the customer and store explicitly to product mapping queries
Oct 16, 2026
afc911c
Pass the customer and store explicitly to product queries
Oct 16, 2026
32fdc93
Reject recommendation queries without a customer
Oct 16, 2026
3ed5514
Add a calculation context for checkout calculations
Oct 16, 2026
5fe2334
Format order email tax rates in the order currency
Oct 16, 2026
1ce5574
Format prices in an explicitly given currency
Oct 16, 2026
52b5c0d
Pass the customer explicitly to checkout attribute queries
Oct 16, 2026
892d865
Pass tax display, customer and store explicitly to tax calculations
Oct 16, 2026
5e047bb
Cache checkout attributes per customer group set
Oct 16, 2026
669ce7b
Pass customer, store and currency explicitly to cart item pricing
Oct 16, 2026
1e947c9
Pass context, language and host to the checkout attribute formatter
Oct 16, 2026
d1d5651
Pass a calculation context to the checkout calculations
Oct 16, 2026
3ac776b
Validate the cart for an explicitly given context
Oct 16, 2026
4f3c4b2
Pass the customer and store explicitly to cart reads and cart merge
Oct 16, 2026
2dba4d6
Take the place order context from the command
Oct 16, 2026
15f6bf0
Build the cart page context from its request
Oct 16, 2026
f6dd5ba
Keep required products when merging the cart at login
Oct 16, 2026
43a1ec6
Derive recommended products customer groups from the query customer
Oct 16, 2026
10d8f73
Derive recommended products customer groups from the query customer
Oct 16, 2026
6120ba0
Merge branch 'refactor/business-explicit-context-catalog' into refact…
Oct 16, 2026
f7dee58
Merge branch 'refactor/business-explicit-context-pricing' into refact…
Oct 16, 2026
08983ce
Take customer details from the query customer and stop reading the cu…
Oct 16, 2026
87916a3
Price a customer's cart in the admin panel for that customer
Oct 16, 2026
81c3eed
Round loyalty points amounts in the primary store currency
Oct 16, 2026
6d38be6
Build calculation contexts from the request and drop duplicated state
Oct 16, 2026
4660ba8
Require the language and host on PlaceOrderCommand
Oct 16, 2026
636de0c
Require a customer when checkout attributes apply ACL
Oct 16, 2026
a059be4
Check required products against the guest cart in the login merge
Oct 16, 2026
058f089
Build the mini cart and add-to-cart models from their request
Oct 16, 2026
8694a5c
Validate a shared wishlist for its owner
Oct 16, 2026
b4914b4
Pass a CatalogAccess to the catalog read services
Oct 16, 2026
dc3061a
Merge remote-tracking branch 'origin/refactor/business-explicit-conte…
Oct 16, 2026
51dd676
Merge remote-tracking branch 'origin/develop' into refactor/business-…
Oct 16, 2026
c6c3444
Merge remote-tracking branch 'origin/refactor/business-explicit-conte…
Oct 16, 2026
a6ccc36
Merge remote-tracking branch 'origin/develop' into refactor/business-…
Oct 16, 2026
33a59f6
Pass a CatalogAccess to the checkout attribute query
Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard
protected virtual async Task<IActionResult> LoginUser(Customer user, string returnUrl)
{
//raise event
await _mediator.Publish(new CustomerLoggedInEvent(user));
await _mediator.Publish(new CustomerLoggedInEvent(user,
_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id));

//authenticate
await _authenticationService.SignIn(user, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ public async Task Handle(OrderNotificationCommand request, CancellationToken can

try
{
if (request.WorkContext.OriginalCustomerIfImpersonated != null)
if (request.OriginalCustomerIfImpersonated != null)
//this order is placed by a store administrator impersonating a customer
await _orderService.InsertOrderNote(new OrderNote {
Note =
$"Order placed by a store owner ('{request.WorkContext.OriginalCustomerIfImpersonated.Email}'. ID = {request.WorkContext.OriginalCustomerIfImpersonated.Id}) impersonating the customer.",
$"Order placed by a store owner ('{request.OriginalCustomerIfImpersonated.Email}'. ID = {request.OriginalCustomerIfImpersonated.Id}) impersonating the customer.",
DisplayToCustomer = false,
OrderId = request.Order.Id
});
Expand All @@ -59,7 +59,7 @@ await _orderService.InsertOrderNote(new OrderNote {
});

//send email notifications
await _messageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.WorkContext.CurrentCustomer,
await _messageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.Customer,
_languageSettings.DefaultAdminLanguageId);

string orderPlacedAttachmentFilePath = string.Empty, orderPlacedAttachmentFileName = string.Empty;
Expand Down Expand Up @@ -87,13 +87,13 @@ await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguag
_logger.LogError(ex, "Error - order placed attachment file {OrderOrderNumber}", request.Order.OrderNumber);
}

await _messageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.WorkContext.CurrentCustomer, request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName, orderPlacedAttachments);
await _messageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer, request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName, orderPlacedAttachments);

if (request.Order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId)))
{
var vendors = await _mediator.Send(new GetVendorsInOrderQuery { Order = request.Order });
foreach (var vendor in vendors)
await _messageProviderService.SendOrderPlacedVendorMessage(request.Order, request.WorkContext.CurrentCustomer, vendor,
await _messageProviderService.SendOrderPlacedVendorMessage(request.Order, request.Customer, vendor,
_languageSettings.DefaultAdminLanguageId);
}
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
using Grand.Business.Core.Events.Customers;
using Grand.Business.Core.Interfaces.Checkout.Orders;
using Grand.Infrastructure;
using Grand.Mediator;

namespace Grand.Business.Checkout.Events.ShoppingCart;

public class CustomerLoggedInEventHandler : INotificationHandler<CustomerLoggedInEvent>
{
private readonly IShoppingCartService _shoppingCartService;
private readonly IContextAccessor _contextAccessor;

public CustomerLoggedInEventHandler(IShoppingCartService shoppingCartService, IContextAccessor contextAccessor)
public CustomerLoggedInEventHandler(IShoppingCartService shoppingCartService)
{
_shoppingCartService = shoppingCartService;
_contextAccessor = contextAccessor;
}

public async Task Handle(CustomerLoggedInEvent notification, CancellationToken cancellationToken)
{
//migrate shopping cart
await _shoppingCartService.MigrateShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, notification.Customer, true);
await _shoppingCartService.MigrateShoppingCart(notification.PreviousCustomer, notification.Customer,
notification.StoreId, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
using Grand.Business.Core.Interfaces.Catalog.Tax;
using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes;
using Grand.Business.Core.Interfaces.Common.Directory;
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.Stores;
using Grand.Domain.Tax;
using Grand.Infrastructure;
using Grand.SharedKernel.Extensions;
using System.Net;

Expand All @@ -24,15 +23,12 @@ public class CheckoutAttributeFormatter : ICheckoutAttributeFormatter
private readonly ICurrencyService _currencyService;
private readonly IPriceFormatter _priceFormatter;
private readonly ITaxService _taxService;
private readonly IContextAccessor _contextAccessor;

public CheckoutAttributeFormatter(IContextAccessor contextAccessor,
ICheckoutAttributeParser checkoutAttributeParser,
public CheckoutAttributeFormatter(ICheckoutAttributeParser checkoutAttributeParser,
ICurrencyService currencyService,
ITaxService taxService,
IPriceFormatter priceFormatter)
{
_contextAccessor = contextAccessor;
_checkoutAttributeParser = checkoutAttributeParser;
_currencyService = currencyService;
_taxService = taxService;
Expand All @@ -43,16 +39,18 @@ public CheckoutAttributeFormatter(IContextAccessor contextAccessor,
/// Formats attributes
/// </summary>
/// <param name="customAttributes">Attributes</param>
/// <param name="customer">Customer</param>
/// <param name="store">Store</param>
/// <param name="context">Customer, store, currency and tax display the prices are rendered for</param>
/// <param name="language">Language of the attribute names</param>
/// <param name="host">Store address used for file download links (read only when allowHyperlinks is true)</param>
/// <param name="separator">Separator</param>
/// <param name="htmlEncode">A value indicating whether to encode (HTML) values</param>
/// <param name="renderPrices">A value indicating whether to render prices</param>
/// <param name="allowHyperlinks">A value indicating whether to HTML hyperlink tags could be rendered (if required)</param>
/// <returns>Attributes</returns>
public virtual async Task<string> FormatAttributes(IList<CustomAttribute> customAttributes,
Customer customer,
Store store,
CalculationContext context,
Language language,
DomainHost host,
string separator = "<br />",
bool htmlEncode = true,
bool renderPrices = true,
Expand Down Expand Up @@ -80,7 +78,7 @@ public virtual async Task<string> FormatAttributes(IList<CustomAttribute> custom
case AttributeControlType.MultilineTextbox:
{
//multiline text box
var attributeName = attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id);
var attributeName = attribute.GetTranslation(a => a.Name, language.Id);
//encode (if required)
if (htmlEncode)
attributeName = WebUtility.HtmlEncode(attributeName);
Expand All @@ -95,14 +93,14 @@ public virtual async Task<string> FormatAttributes(IList<CustomAttribute> custom
{
var attributeText = string.Empty;
var attributeName =
attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id);
attribute.GetTranslation(a => a.Name, language.Id);
if (allowHyperlinks)
{
//hyperlinks are allowed
var downloadLink =
$"{_contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}";
$"{host.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}";
attributeText =
$"<a href=\"{downloadLink}\" class=\"fileuploadattribute\">{attribute.GetTranslation(a => a.TextPrompt, _contextAccessor.WorkContext.WorkingLanguage.Id)}</a>";
$"<a href=\"{downloadLink}\" class=\"fileuploadattribute\">{attribute.GetTranslation(a => a.TextPrompt, language.Id)}</a>";
}

formattedAttribute = $"{attributeName}: {attributeText}";
Expand All @@ -114,7 +112,7 @@ public virtual async Task<string> FormatAttributes(IList<CustomAttribute> custom
{
//other attributes (text box, datepicker)
formattedAttribute =
$"{attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id)}: {valueStr}";
$"{attribute.GetTranslation(a => a.Name, language.Id)}: {valueStr}";
//encode (if required)
if (htmlEncode)
formattedAttribute = WebUtility.HtmlEncode(formattedAttribute);
Expand All @@ -128,21 +126,20 @@ public virtual async Task<string> FormatAttributes(IList<CustomAttribute> custom
if (attributeValue != null)
{
formattedAttribute =
$"{attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id)}: {attributeValue.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id)}";
$"{attribute.GetTranslation(a => a.Name, language.Id)}: {attributeValue.GetTranslation(a => a.Name, language.Id)}";
if (renderPrices)
{
var priceAdjustmentBase =
(await _taxService.GetCheckoutAttributePrice(attribute, attributeValue,
_contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax,
customer, store))
context.IncludingTax, context.Customer, context.Store))
.checkoutPrice;
var priceAdjustment =
await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase,
_contextAccessor.WorkContext.WorkingCurrency);
context.Currency);
if (priceAdjustmentBase > 0)
{
var priceAdjustmentStr = _priceFormatter.FormatPrice(priceAdjustment,
_contextAccessor.WorkContext.WorkingCurrency);
context.Currency);
formattedAttribute += $" [+{priceAdjustmentStr}]";
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes;
using Grand.Business.Core.Utilities.Catalog;
using Grand.Data;
using Grand.Domain.Customers;
using Grand.Domain.Orders;
using Grand.Infrastructure;
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Caching.Constants;
using Grand.Infrastructure.Configuration;
Expand All @@ -25,12 +25,11 @@ public CheckoutAttributeService(
ICacheBase cacheBase,
IRepository<CheckoutAttribute> checkoutAttributeRepository,
IMediator mediator,
IContextAccessor contextAccessor, AccessControlConfig accessControlConfig)
AccessControlConfig accessControlConfig)
{
_cacheBase = cacheBase;
_checkoutAttributeRepository = checkoutAttributeRepository;
_mediator = mediator;
_contextAccessor = contextAccessor;
_accessControlConfig = accessControlConfig;
}

Expand All @@ -41,7 +40,6 @@ public CheckoutAttributeService(
private readonly IRepository<CheckoutAttribute> _checkoutAttributeRepository;
private readonly IMediator _mediator;
private readonly ICacheBase _cacheBase;
private readonly IContextAccessor _contextAccessor;
private readonly AccessControlConfig _accessControlConfig;

#endregion
Expand All @@ -51,38 +49,35 @@ public CheckoutAttributeService(
/// <summary>
/// Gets all checkout attributes
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <param name="access">Customer whose groups limit the result and the store, or every attribute of the store</param>
/// <param name="excludeShippableAttributes">A value indicating whether we should exclude shippable attributes</param>
/// <param name="ignoreAcl"></param>
/// <returns>Checkout attributes</returns>
public virtual async Task<IList<CheckoutAttribute>> GetAllCheckoutAttributes(string storeId = "",
bool excludeShippableAttributes = false, bool ignoreAcl = false)
public virtual async Task<IList<CheckoutAttribute>> GetAllCheckoutAttributes(CatalogAccess access,
bool excludeShippableAttributes = false)
{
var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, storeId, excludeShippableAttributes, ignoreAcl);
ArgumentNullException.ThrowIfNull(access);

var applyAcl = !access.ShowHidden && !_accessControlConfig.IgnoreAcl;
var allowedCustomerGroupsIds = applyAcl ? access.Customer.GetCustomerGroupIds() : [];
var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, access.StoreId, excludeShippableAttributes,
access.ShowHidden, string.Join(",", allowedCustomerGroupsIds));
return await _cacheBase.GetAsync(key, async () =>
{
var query = from p in _checkoutAttributeRepository.Table
select p;

query = query.OrderBy(c => c.DisplayOrder);

if ((!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) ||
(!ignoreAcl && !_accessControlConfig.IgnoreAcl))
{
if (!ignoreAcl && !_accessControlConfig.IgnoreAcl)
{
var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds();
query = from p in query
where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x))
select p;
}

//Store acl
if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations)
query = from p in query
where !p.LimitedToStores || p.Stores.Contains(storeId)
select p;
}
if (applyAcl)
query = from p in query
where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x))
select p;

//Store acl
if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations)
query = from p in query
where !p.LimitedToStores || p.Stores.Contains(access.StoreId)
select p;

if (excludeShippableAttributes) query = query.Where(x => !x.ShippableProductRequired);
return await _checkoutAttributeRepository.ToListAsync(query);
Expand Down
Loading
Loading