diff --git a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs index 0a08767243..9c06bc0d7e 100644 --- a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs @@ -169,7 +169,8 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard protected virtual async Task 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); diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs index 66eb921eeb..4013d2b751 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs @@ -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 }); @@ -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; @@ -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); } } 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 15584bdef1..6633308e16 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -25,7 +25,6 @@ using Grand.Domain.Shipping; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; -using Grand.Infrastructure; using Grand.SharedKernel; using Grand.SharedKernel.Extensions; using Grand.Mediator; @@ -67,7 +66,6 @@ public class PlaceOrderCommandHandler : IRequestHandler Handle(PlaceOrderCommand command, Cancellati var result = new PlaceOrderResult(); try { + ArgumentNullException.ThrowIfNull(command.Context); + ArgumentNullException.ThrowIfNull(command.Language); + ArgumentNullException.ThrowIfNull(command.Host); + //prepare order details - var details = await PreparePlaceOrderDetails(); + var details = await PreparePlaceOrderDetails(command); //event notification await _mediator.PlaceOrderDetailsEvent(result, details); @@ -181,7 +181,11 @@ await UpdatePaymentTransaction(processPayment.paymentTransaction, result.PlacedO #region Events & notes - await _mediator.Send(new OrderNotificationCommand { Order = result.PlacedOrder, WorkContext = _contextAccessor.WorkContext }, cancellationToken); + await _mediator.Send(new OrderNotificationCommand { + Order = result.PlacedOrder, + Customer = details.Customer, + OriginalCustomerIfImpersonated = command.OriginalCustomerIfImpersonated + }, cancellationToken); //check order status await _mediator.Send(new CheckOrderStatusCommand { Order = result.PlacedOrder }, cancellationToken); @@ -270,7 +274,7 @@ private async Task PreparePaymentTransaction(PlaceOrderConta var paymentTransaction = new PaymentTransaction(); var paymentTransactionId = details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.PaymentTransaction, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); if (!string.IsNullOrEmpty(paymentTransactionId)) { paymentTransaction = await _paymentTransactionService.GetById(paymentTransactionId); @@ -288,7 +292,7 @@ private async Task PreparePaymentTransaction(PlaceOrderConta paymentTransaction.OrderGuid = paymentTransaction.OrderGuid == Guid.Empty ? Guid.NewGuid() : paymentTransaction.OrderGuid; paymentTransaction.PaymentMethodSystemName = details.PaymentMethodSystemName; - paymentTransaction.StoreId = _contextAccessor.StoreContext.CurrentStore.Id; + paymentTransaction.StoreId = details.Store.Id; paymentTransaction.CustomerId = details.Customer.Id; paymentTransaction.TransactionAmount = details.OrderTotal; paymentTransaction.CurrencyRate = details.CurrencyRate; @@ -377,16 +381,12 @@ private async Task GetShoppingCartItemWeight(ShoppingCartItem shoppingCa return weight; } - //TODO M1 PR 3: build the calculation context from PlaceOrderContainer instead of the ambient context - private CalculationContext AmbientCalculationContext => - new(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType); - - protected virtual async Task PreparePlaceOrderDetails() + protected virtual async Task PreparePlaceOrderDetails(PlaceOrderCommand command) { var details = new PlaceOrderContainer { - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Store = _contextAccessor.StoreContext.CurrentStore, + Context = command.Context, + Customer = command.Context.Customer, + Store = command.Context.Store }; if (details.Customer == null) throw new ArgumentException("Customer is not set"); @@ -413,8 +413,8 @@ protected virtual async Task PreparePlaceOrderDetails() //customer currency var currencyTmp = await _currencyService.GetCurrencyById( details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.CurrencyId, - _contextAccessor.StoreContext.CurrentStore.Id)); - var customerCurrency = currencyTmp is { Published: true } ? currencyTmp : _contextAccessor.WorkContext.WorkingCurrency; + details.Store.Id)); + var customerCurrency = currencyTmp is { Published: true } ? currencyTmp : details.Context.Currency; details.Currency = customerCurrency; var primaryStoreCurrency = await _currencyService.GetPrimaryStoreCurrency(); details.CurrencyRate = Math.Round(customerCurrency.Rate / primaryStoreCurrency.Rate, 6); @@ -423,10 +423,10 @@ protected virtual async Task PreparePlaceOrderDetails() //customer language details.Language = await _languageService.GetLanguageById( details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId, - _contextAccessor.StoreContext.CurrentStore.Id)); + details.Store.Id)); if (details.Language is not { Published: true }) - details.Language = _contextAccessor.WorkContext.WorkingLanguage; + details.Language = command.Language; details.BillingAddress = details.Customer.BillingAddress; if (!string.IsNullOrEmpty(details.BillingAddress.CountryId)) @@ -439,14 +439,15 @@ protected virtual async Task PreparePlaceOrderDetails() //checkout attributes details.CheckoutAttributes = details.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); details.CheckoutAttributeDescription = - await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, details.Customer, details.Store); + await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, details.Context, + command.Language, command.Host); //load and validate customer shopping cart details.Cart = details.Customer.ShoppingCartItems .Where(sci => sci.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) - .LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, _contextAccessor.StoreContext.CurrentStore.Id) + .LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, details.Store.Id) .ToList(); if (!details.Cart.Any()) @@ -454,7 +455,8 @@ protected virtual async Task PreparePlaceOrderDetails() //validate the entire shopping cart var warnings = - await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, true, true); + await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, details.Context, + true, true); if (warnings.Any()) throw new GrandException(string.Join(", ", warnings)); //validate individual cart items @@ -486,13 +488,13 @@ protected virtual async Task PreparePlaceOrderDetails() if (_taxSettings.AllowCustomersToSelectTaxDisplayType) details.TaxDisplayType = (TaxDisplayType)details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.TaxDisplayTypeId, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); else details.TaxDisplayType = _taxSettings.TaxDisplayType; //sub total //sub total (incl tax) - var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, AmbientCalculationContext, true); + var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, details.Context, true); var orderSubTotalDiscountAmount = shoppingCartSubTotal.discountAmount; var orderSubTotalAppliedDiscounts = shoppingCartSubTotal.appliedDiscounts; var subTotalWithoutDiscountBase = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -505,7 +507,7 @@ protected virtual async Task PreparePlaceOrderDetails() details.AppliedDiscounts.Add(disc); //sub total (excl tax) - shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, AmbientCalculationContext, false); + shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, details.Context, false); orderSubTotalDiscountAmount = shoppingCartSubTotal.discountAmount; subTotalWithoutDiscountBase = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -519,7 +521,7 @@ protected virtual async Task PreparePlaceOrderDetails() { var pickupPoint = details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedPickupPoint, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); if (_shippingSettings.AllowPickUpInStore && pickupPoint != null) { details.PickUpInStore = true; @@ -545,7 +547,7 @@ protected virtual async Task PreparePlaceOrderDetails() var shippingOption = details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedShippingOption, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); if (shippingOption != null) { details.ShippingMethodName = shippingOption.Name; @@ -560,9 +562,9 @@ protected virtual async Task PreparePlaceOrderDetails() //shipping total var (orderShippingTotalInclTax, _, shippingTotalDiscounts) = - await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, AmbientCalculationContext, true); + await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, details.Context, true); var orderShippingTotalExclTax = - (await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, AmbientCalculationContext, false)) + (await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, details.Context, false)) .shoppingCartShippingTotal; if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) throw new GrandException("Shipping total couldn't be calculated"); @@ -575,8 +577,8 @@ protected virtual async Task PreparePlaceOrderDetails() //payment var paymentMethodSystemName = - _contextAccessor.WorkContext.CurrentCustomer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedPaymentMethod, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedPaymentMethod, + details.Store.Id); details.PaymentMethodSystemName = paymentMethodSystemName; var paymentAdditionalFee = await _paymentService.GetAdditionalHandlingFee(details.Cart, paymentMethodSystemName); @@ -589,7 +591,7 @@ protected virtual async Task PreparePlaceOrderDetails() //tax total //tax amount - var (taxTotal, taxRates) = await _orderTotalCalculationService.GetTaxTotal(details.Cart, AmbientCalculationContext); + var (taxTotal, taxRates) = await _orderTotalCalculationService.GetTaxTotal(details.Cart, details.Context); details.OrderTaxTotal = taxTotal; //tax rates @@ -601,7 +603,7 @@ protected virtual async Task PreparePlaceOrderDetails() //order total (and applied discounts, gift vouchers, loyalty points) var (orderTotal, orderDiscountAmount, orderAppliedDiscounts, appliedGiftVouchers, redeemedLoyaltyPoints, - redeemedLoyaltyPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotal(details.Cart, AmbientCalculationContext); + redeemedLoyaltyPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotal(details.Cart, details.Context); if (!orderTotal.HasValue) throw new GrandException("Order total couldn't be calculated"); @@ -623,15 +625,15 @@ protected virtual async Task PrepareOrderItem(ShoppingCartItem sc, Pr PlaceOrderContainer details) { var scUnitPrice = (await _pricingService.GetUnitPrice(sc, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency)).unitprice; + details.Customer, details.Store, + details.Context.Currency)).unitprice; var scUnitPriceWithoutDisc = (await _pricingService.GetUnitPrice(sc, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, false)).unitprice; + details.Customer, details.Store, + details.Context.Currency, false)).unitprice; var (scSubTotal, discountAmount, scDiscounts) = await _pricingService.GetSubTotal(sc, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + details.Customer, details.Store, + details.Context.Currency); var prices = await _taxService.GetTaxProductPrice(product, details.Customer, details.Store, scUnitPrice, @@ -653,7 +655,7 @@ protected virtual async Task PrepareOrderItem(ShoppingCartItem sc, Pr var itemWeight = await GetShoppingCartItemWeight(sc); var warehouseId = !string.IsNullOrEmpty(sc.WarehouseId) ? sc.WarehouseId - : _contextAccessor.StoreContext.CurrentStore.DefaultWarehouseId; + : details.Store.DefaultWarehouseId; if (!product.UseMultipleWarehouses && string.IsNullOrEmpty(warehouseId)) if (!string.IsNullOrEmpty(product.WarehouseId)) warehouseId = product.WarehouseId; @@ -750,7 +752,7 @@ protected virtual async Task GenerateGiftVoucher(PlaceOrderContainer details, Sh SenderEmail = giftVoucherSenderEmail, Message = giftVoucherMessage, IsRecipientNotified = false, - StoreId = _orderSettings.GiftVouchers_Assign_StoreId ? _contextAccessor.StoreContext.CurrentStore.Id : string.Empty + StoreId = _orderSettings.GiftVouchers_Assign_StoreId ? details.Store.Id : string.Empty }; await _giftVoucherService.InsertGiftVoucher(gc); } diff --git a/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs b/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs index ffcb5d67c9..22e9efb41c 100644 --- a/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs +++ b/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs @@ -1,6 +1,5 @@ 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; @@ -8,17 +7,16 @@ namespace Grand.Business.Checkout.Events.ShoppingCart; public class CustomerLoggedInEventHandler : INotificationHandler { 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); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs index 8e8ba47e63..bbc29f8081 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs @@ -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; @@ -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; @@ -43,16 +39,18 @@ public CheckoutAttributeFormatter(IContextAccessor contextAccessor, /// Formats attributes /// /// Attributes - /// Customer - /// Store + /// Customer, store, currency and tax display the prices are rendered for + /// Language of the attribute names + /// Store address used for file download links (read only when allowHyperlinks is true) /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to render prices /// A value indicating whether to HTML hyperlink tags could be rendered (if required) /// Attributes public virtual async Task FormatAttributes(IList customAttributes, - Customer customer, - Store store, + CalculationContext context, + Language language, + DomainHost host, string separator = "
", bool htmlEncode = true, bool renderPrices = true, @@ -80,7 +78,7 @@ public virtual async Task FormatAttributes(IList 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); @@ -95,14 +93,14 @@ public virtual async Task FormatAttributes(IList 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 = - $"{attribute.GetTranslation(a => a.TextPrompt, _contextAccessor.WorkContext.WorkingLanguage.Id)}"; + $"{attribute.GetTranslation(a => a.TextPrompt, language.Id)}"; } formattedAttribute = $"{attributeName}: {attributeText}"; @@ -114,7 +112,7 @@ public virtual async Task FormatAttributes(IList 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); @@ -128,21 +126,20 @@ public virtual async Task FormatAttributes(IList 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}]"; } } diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs index 6bc68c5850..9d9a98047c 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs @@ -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; @@ -25,12 +25,11 @@ public CheckoutAttributeService( ICacheBase cacheBase, IRepository checkoutAttributeRepository, IMediator mediator, - IContextAccessor contextAccessor, AccessControlConfig accessControlConfig) + AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _checkoutAttributeRepository = checkoutAttributeRepository; _mediator = mediator; - _contextAccessor = contextAccessor; _accessControlConfig = accessControlConfig; } @@ -41,7 +40,6 @@ public CheckoutAttributeService( private readonly IRepository _checkoutAttributeRepository; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; - private readonly IContextAccessor _contextAccessor; private readonly AccessControlConfig _accessControlConfig; #endregion @@ -51,14 +49,18 @@ public CheckoutAttributeService( /// /// Gets all checkout attributes /// - /// Store identifier + /// Customer whose groups limit the result and the store, or every attribute of the store /// A value indicating whether we should exclude shippable attributes - /// /// Checkout attributes - public virtual async Task> GetAllCheckoutAttributes(string storeId = "", - bool excludeShippableAttributes = false, bool ignoreAcl = false) + public virtual async Task> 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 @@ -66,23 +68,16 @@ public virtual async Task> GetAllCheckoutAttributes(str 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); diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs index 068209b507..47d4e63491 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Commands.Checkout.Orders; using Grand.Business.Core.Events.Checkout.ShoppingCart; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; @@ -8,7 +8,6 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Extensions; using Grand.Mediator; @@ -22,14 +21,12 @@ public class ShoppingCartService : IShoppingCartService #region Ctor public ShoppingCartService( - IContextAccessor contextAccessor, IProductService productService, ICustomerService customerService, IMediator mediator, IShoppingCartValidator shoppingCartValidator, ShoppingCartSettings shoppingCartSettings) { - _contextAccessor = contextAccessor; _productService = productService; _customerService = customerService; _mediator = mediator; @@ -41,7 +38,6 @@ public ShoppingCartService( #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IProductService _productService; private readonly ICustomerService _customerService; private readonly IMediator _mediator; @@ -55,14 +51,17 @@ public ShoppingCartService( /// /// Gets shopping cart /// + /// Customer whose cart is read /// Store identifier; pass null to load all records /// Shopping cart type; pass null to load all records /// Shopping Cart - public virtual async Task> GetShoppingCart(string storeId = null, + public virtual async Task> GetShoppingCart(Customer customer, string storeId = null, params ShoppingCartType[] shoppingCartType) { + ArgumentNullException.ThrowIfNull(customer); + var model = new List(); - var cart = _contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.ToList(); + var cart = customer.ShoppingCartItems.ToList(); if (!string.IsNullOrEmpty(storeId)) cart = cart.LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, storeId).ToList(); @@ -393,11 +392,13 @@ public virtual async Task DeleteShoppingCartItem(Customer customer, ShoppingCart /// /// From customer /// To customer + /// Store whose selected checkout attributes move along /// /// A value indicating whether to coupon codes (discount and gift voucher) should be also /// re-applied /// - public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, bool includeCouponCodes) + public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, + bool includeCouponCodes) { ArgumentNullException.ThrowIfNull(fromCustomer); ArgumentNullException.ThrowIfNull(toCustomer); @@ -405,14 +406,24 @@ public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer to if (fromCustomer.Id == toCustomer.Id) return; //the same customer - //shopping cart items + //shopping cart items; the whole cart moves together, so a product's required products + //may not be in the target cart yet when the product itself is added - they are checked + //against the source cart instead, which holds the whole set being moved var fromCart = fromCustomer.ShoppingCartItems.ToList(); foreach (var sci in fromCart) + { + var product = await _productService.GetProductById(sci.ProductId); + if (product != null && + (await _shoppingCartValidator.GetRequiredProductWarnings(fromCustomer, sci, product, sci.StoreId)).Any()) + continue; + await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, sci.WarehouseId, sci.Attributes, sci.EnteredPrice, sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false, sci.ReservationId, sci.Parameter, sci.Duration, - new ShoppingCartValidatorOptions()); + new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false }); + } + foreach (var sci in fromCart) await DeleteShoppingCartItem(fromCustomer, sci); //copy discount and gift voucher coupon codes @@ -430,9 +441,9 @@ await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, } //move selected checkout attributes - var checkoutAttributes = fromCustomer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); + var checkoutAttributes = fromCustomer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, storeId); await _customerService.UpdateUserField(toCustomer, SystemCustomerFieldNames.CheckoutAttributes, checkoutAttributes, - _contextAccessor.StoreContext.CurrentStore.Id); + storeId); } #endregion diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs index 2094e071f6..47c4cfdd6b 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs @@ -6,7 +6,6 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Validators; using Grand.Mediator; @@ -17,14 +16,10 @@ public class ShoppingCartValidator : IShoppingCartValidator private readonly IMediator _mediator; private readonly IValidatorFactory _validatorFactory; - private readonly IContextAccessor _contextAccessor; - public ShoppingCartValidator( - IContextAccessor contextAccessor, IMediator mediator, IValidatorFactory validatorFactory) { - _contextAccessor = contextAccessor; _mediator = mediator; _validatorFactory = validatorFactory; } @@ -122,14 +117,16 @@ public virtual async Task> GetReservationProductWarnings(Customer } public virtual async Task> GetShoppingCartWarnings(IList shoppingCart, - IList checkoutAttributes, bool validateCheckoutAttributes, bool validateAmount) + IList checkoutAttributes, CalculationContext context, bool validateCheckoutAttributes, + bool validateAmount) { + ArgumentNullException.ThrowIfNull(context); + var warnings = new List(); checkoutAttributes ??= new List(); var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartWarningsValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, shoppingCart)); + .ValidateAsync(new ShoppingCartWarningsValidatorRecord(context.Customer, context.Store, shoppingCart)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -138,8 +135,7 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( - new ShoppingCartCheckoutAttributesValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, + new ShoppingCartCheckoutAttributesValidatorRecord(context.Customer, context.Store, shoppingCart, checkoutAttributes)); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); @@ -150,11 +146,7 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( - new ShoppingCartTotalAmountValidatorRecord(shoppingCart, - //TODO M1 PR 3: the cart validator still reads the ambient context - new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, - _contextAccessor.WorkContext.TaxDisplayType))); + new ShoppingCartTotalAmountValidatorRecord(shoppingCart, context)); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); } @@ -173,7 +165,7 @@ public async Task> CheckCommonWarnings(Customer customer, IList(); var result = await _validatorFactory.GetValidator().ValidateAsync( - new ShoppingCartCommonWarningsValidatorRecord(customer, _contextAccessor.StoreContext.CurrentStore, currentCart, + new ShoppingCartCommonWarningsValidatorRecord(customer, currentCart, product, shoppingCartType, rentalStartDate, rentalEndDate, quantity, reservationId)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -231,8 +223,7 @@ public virtual async Task> GetRequiredProductWarnings(Customer cus if (!product.RequireOtherProducts) return warnings; var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartRequiredProductValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartRequiredProductValidatorRecord(customer, storeId, product, shoppingCartItem)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); return warnings; diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs index 24ef123b73..8ff102e60a 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs @@ -1,6 +1,7 @@ using FluentValidation; using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -27,7 +28,7 @@ public ShoppingCartCheckoutAttributesValidator(ITranslationService translationSe //existing checkout attributes var attributes2 = - await checkoutAttributeService.GetAllCheckoutAttributes(value.Store.Id, + await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(value.Customer, value.Store.Id), !value.ShoppingCarts.RequiresShipping()); foreach (var a2 in attributes2) { diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs index 4f1a5b1f4e..43bd391384 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs @@ -5,13 +5,11 @@ using Grand.Domain.Customers; using Grand.Domain.Orders; using Grand.Domain.Permissions; -using Grand.Domain.Stores; namespace Grand.Business.Checkout.Validators; public record ShoppingCartCommonWarningsValidatorRecord( Customer Customer, - Store Store, IList ShoppingCarts, Product Product, ShoppingCartType ShoppingCartType, diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs index 2010053bb3..cc455c493b 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs @@ -4,13 +4,12 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Domain.Stores; namespace Grand.Business.Checkout.Validators; public record ShoppingCartRequiredProductValidatorRecord( Customer Customer, - Store Store, + string StoreId, Product Product, ShoppingCartItem ShoppingCartItem); @@ -23,7 +22,7 @@ public ShoppingCartRequiredProductValidator(ITranslationService translationServi { var cart = value.Customer.ShoppingCartItems .Where(sci => sci.ShoppingCartTypeId == value.ShoppingCartItem.ShoppingCartTypeId) - .LimitPerStore(shoppingCartSettings.SharedCartBetweenStores, value.Store.Id) + .LimitPerStore(shoppingCartSettings.SharedCartBetweenStores, value.StoreId) .ToList(); var requiredProducts = new List(); diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs index 057eaf52d2..f372553d64 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs @@ -1,6 +1,30 @@ using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; using Grand.Mediator; namespace Grand.Business.Core.Commands.Checkout.Orders; -public class PlaceOrderCommand : IRequest; \ No newline at end of file +public class PlaceOrderCommand : IRequest +{ + /// + /// Customer, store, working currency and tax display the order is calculated for (required) + /// + public CalculationContext Context { get; set; } + + /// + /// Language used when the customer has no published language of their own (required) + /// + public Language Language { get; set; } + + /// + /// Store address used for file download links in the checkout attribute description (required) + /// + public DomainHost Host { get; set; } + + /// + /// Staff member who places the order while impersonating the customer, if any + /// + public Customer OriginalCustomerIfImpersonated { get; set; } +} diff --git a/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs b/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs index c079e614bc..e451bac685 100644 --- a/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs @@ -1,5 +1,5 @@ -using Grand.Domain.Orders; -using Grand.Infrastructure; +using Grand.Domain.Customers; +using Grand.Domain.Orders; using Grand.Mediator; namespace Grand.Business.Core.Commands.Messages.Common; @@ -7,5 +7,14 @@ namespace Grand.Business.Core.Commands.Messages.Common; public class OrderNotificationCommand : IRequest { public Order Order { get; set; } - public IWorkContext WorkContext { get; set; } + + /// + /// Customer the order was placed for + /// + public Customer Customer { get; set; } + + /// + /// Staff member who placed the order while impersonating the customer, if any + /// + public Customer OriginalCustomerIfImpersonated { get; set; } } diff --git a/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs b/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs index adeda2f2ef..7b05608c7d 100644 --- a/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs +++ b/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs @@ -8,9 +8,11 @@ namespace Grand.Business.Core.Events.Customers; /// public class CustomerLoggedInEvent : INotification { - public CustomerLoggedInEvent(Customer customer) + public CustomerLoggedInEvent(Customer customer, Customer previousCustomer, string storeId) { Customer = customer; + PreviousCustomer = previousCustomer; + StoreId = storeId; } /// @@ -20,4 +22,14 @@ public Customer Customer { get; private set; } + + /// + /// Customer the request was signed in as before the login (usually the guest) + /// + public Customer PreviousCustomer { get; } + + /// + /// Store of the request the customer logged in through + /// + public string StoreId { get; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs index ef50d2e43c..b16bdab2c9 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs @@ -1,5 +1,6 @@ +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Common; -using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; @@ -13,16 +14,18 @@ public interface ICheckoutAttributeFormatter /// Formats attributes /// /// Attributes - /// Customer + /// Customer, store, currency and tax display the prices are rendered for + /// Language of the attribute names + /// Store address used for file download links (read only when allowHyperlinks is true) /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to render prices /// A value indicating whether to HTML hyperlink tags could be rendered (if required) - /// Store /// Attributes Task FormatAttributes(IList customAttributes, - Customer customer, - Store store, + CalculationContext context, + Language language, + DomainHost host, string separator = "
", bool htmlEncode = true, bool renderPrices = true, diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs index a6897ba1a9..abf28f5e54 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs @@ -1,3 +1,4 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; @@ -12,12 +13,11 @@ public interface ICheckoutAttributeService /// /// Gets all checkout attributes /// - /// Store identifier + /// Customer whose groups limit the result and the store, or every attribute of the store /// A value indicating whether we should exclude shippable attributes - /// /// Checkout attributes - Task> GetAllCheckoutAttributes(string storeId = "", - bool excludeShippableAttributes = false, bool ignoreAcl = false); + Task> GetAllCheckoutAttributes(CatalogAccess access, + bool excludeShippableAttributes = false); /// /// Gets a checkout attribute diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs index ae62d61c9c..cd67b2b767 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs @@ -13,10 +13,12 @@ public interface IShoppingCartService /// /// Gets shopping cart /// + /// Customer whose cart is read /// Store identifier; pass null to load all records /// Shopping cart type; pass null to load all records /// Shopping Cart - Task> GetShoppingCart(string storeId = null, params ShoppingCartType[] shoppingCartType); + Task> GetShoppingCart(Customer customer, string storeId = null, + params ShoppingCartType[] shoppingCartType); /// /// Finds a shopping cart item in the cart @@ -107,9 +109,10 @@ Task DeleteShoppingCartItem(Customer customer, ShoppingCartItem shoppingCartItem /// /// From customer /// To customer + /// Store whose selected checkout attributes move along /// /// A value indicating whether to coupon codes (discount and gift voucher) should be also /// re-applied /// - Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, bool includeCouponCodes); + Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, bool includeCouponCodes); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs index 8edaf3c768..c80fb60fa5 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs @@ -64,11 +64,13 @@ Task> GetShoppingCartItemWarnings(Customer customer, ShoppingCartI /// /// Shopping cart /// Checkout attributes + /// Customer, store, currency and tax display the cart is validated for /// A value indicating whether to validate checkout attributes /// A value indicating whether to validate subtotal/total attributes /// Warnings Task> GetShoppingCartWarnings(IList shoppingCart, - IList checkoutAttributes, bool validateCheckoutAttributes, bool validateAmount); + IList checkoutAttributes, CalculationContext context, bool validateCheckoutAttributes, + bool validateAmount); /// /// Validates shopping cart item for reservation products diff --git a/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs b/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs index 5c6849cbfc..eb6bff6aa8 100644 --- a/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs +++ b/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs @@ -13,6 +13,11 @@ namespace Grand.Business.Core.Utilities.Checkout; public class PlaceOrderContainer { + /// + /// Customer, store, working currency and tax display the order totals are calculated for + /// + public CalculationContext Context { get; set; } + public Customer Customer { get; set; } public Store Store { get; set; } public Language Language { get; set; } diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs b/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs index d1560a18ae..5376c984a7 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs @@ -19,9 +19,10 @@ public static partial class CacheKey /// /// {0} : store ID /// {1} : >A value indicating whether we should exclude shippable attributes - /// {2} : ignore ACL? + /// {2} : show hidden (group-limited) attributes? + /// {3} : customer group ids (empty when ACL does not apply) /// - public static string CHECKOUTATTRIBUTES_ALL_KEY => "Grand.checkoutattribute.all-{0}-{1}-{2}"; + public static string CHECKOUTATTRIBUTES_ALL_KEY => "Grand.checkoutattribute.all-{0}-{1}-{2}-{3}"; /// /// Key for caching diff --git a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs index 338642a0a7..658376972a 100644 --- a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs +++ b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs @@ -144,7 +144,8 @@ public async Task Callback(string returnUrl) return RedirectToRoute("OidcLoginFailed", new { session = true }); //the identity provider is the sign-in: its multi-factor policy applies, not the store's two-factor code - await _mediator.Publish(new CustomerLoggedInEvent(result.Customer)); + await _mediator.Publish(new CustomerLoggedInEvent(result.Customer, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); await _authenticationService.SignIn(result.Customer, false, sessionId); return RedirectLocal(returnUrl); } diff --git a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs index d55e33e5fd..368ad2e18b 100644 --- a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs +++ b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs @@ -41,7 +41,7 @@ public async Task PaymentInfo() if (_brainTreePaymentSettings.Use3DS) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart); if (!cart.Any()) diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs new file mode 100644 index 0000000000..fc0213c050 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs @@ -0,0 +1,63 @@ +using Grand.Business.Checkout.Commands.Handlers.Orders; +using Grand.Business.Core.Commands.Messages.Common; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Mediator; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Checkout.Tests.Commands.Handlers.Orders; + +[TestClass] +public class OrderNotificationCommandHandlerTests +{ + private OrderNotificationCommandHandler _handler; + private Mock _messageProviderServiceMock; + private Mock _orderServiceMock; + + [TestInitialize] + public void Init() + { + _orderServiceMock = new Mock(); + _messageProviderServiceMock = new Mock(); + _handler = new OrderNotificationCommandHandler(_orderServiceMock.Object, _messageProviderServiceMock.Object, + new OrderSettings(), new LanguageSettings(), new Mock().Object, + new Mock>().Object, new Mock().Object); + } + + [TestMethod] + public async Task Handle_AddsImpersonationNoteForTheOriginalCustomer() + { + //Arrange + var staff = new Customer { Email = "staff@shop.example" }; + + //Act + await _handler.Handle(new OrderNotificationCommand { + Order = new Order { Id = "order" }, Customer = new Customer(), OriginalCustomerIfImpersonated = staff + }, CancellationToken.None); + + //Assert + _orderServiceMock.Verify(x => x.InsertOrderNote(It.Is(n => + n.OrderId == "order" && n.Note.Contains("staff@shop.example") && n.Note.Contains("impersonating"))), Times.Once); + } + + [TestMethod] + public async Task Handle_SendsTheCustomerMessageForTheCommandCustomer() + { + //Arrange + var customer = new Customer(); + var order = new Order { Id = "order", CustomerLanguageId = "en" }; + + //Act + await _handler.Handle(new OrderNotificationCommand { Order = order, Customer = customer }, CancellationToken.None); + + //Assert + _messageProviderServiceMock.Verify(x => x.SendOrderPlacedCustomerMessage(order, customer, "en", + It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs new file mode 100644 index 0000000000..3a9600a974 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs @@ -0,0 +1,128 @@ +using Grand.Business.Checkout.Commands.Handlers.Orders; +using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Interfaces.Catalog.Discounts; +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.Checkout.CheckoutAttributes; +using Grand.Business.Core.Interfaces.Checkout.GiftVouchers; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Checkout.Payments; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Payments; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Grand.Infrastructure.Configuration; +using Grand.Mediator; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Checkout.Tests.Commands.Handlers.Orders; + +[TestClass] +public class PlaceOrderCommandHandlerTests +{ + private Mock _checkoutAttributeFormatterMock; + private PlaceOrderCommandHandler _handler; + + [TestInitialize] + public void Init() + { + _checkoutAttributeFormatterMock = new Mock(); + var currencyService = new Mock(); + currencyService.Setup(x => x.GetPrimaryStoreCurrency()).ReturnsAsync(new Currency { CurrencyCode = "USD", Rate = 1 }); + + _handler = new PlaceOrderCommandHandler(new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock>().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, _checkoutAttributeFormatterMock.Object, + new Mock().Object, new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, currencyService.Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, new ShippingSettings(), + new ShoppingCartSettings(), new PaymentSettings(), new OrderSettings(), new OidcLoginConfig(), + new TaxSettings()); + } + + [TestMethod] + public async Task Handle_WithoutContext_ReturnsError() + { + //Act + var result = await _handler.Handle(new PlaceOrderCommand(), CancellationToken.None); + + //Assert + Assert.IsNotEmpty(result.Errors); + Assert.IsNull(result.PlacedOrder); + } + + [TestMethod] + public async Task Handle_WithoutLanguage_ReturnsError() + { + //Arrange + var command = new PlaceOrderCommand { + Context = new CalculationContext(new Customer(), new Store(), new Currency(), TaxDisplayType.IncludingTax), + Host = new DomainHost { Url = "https://shop.example/" } + }; + + //Act + var result = await _handler.Handle(command, CancellationToken.None); + + //Assert + Assert.IsTrue(result.Errors.Any(e => e.Contains(nameof(PlaceOrderCommand.Language)))); + Assert.IsNull(result.PlacedOrder); + } + + [TestMethod] + public async Task Handle_WithoutHost_ReturnsError() + { + //Arrange + var command = new PlaceOrderCommand { + Context = new CalculationContext(new Customer(), new Store(), new Currency(), TaxDisplayType.IncludingTax), + Language = new Language { Id = "pl" } + }; + + //Act + var result = await _handler.Handle(command, CancellationToken.None); + + //Assert + Assert.IsTrue(result.Errors.Any(e => e.Contains(nameof(PlaceOrderCommand.Host)))); + Assert.IsNull(result.PlacedOrder); + } + + [TestMethod] + public async Task Handle_PreparesDetailsFromTheCommand() + { + //Arrange + var customer = new Customer { BillingAddress = new Address { Email = "buyer@shop.example" } }; + var command = new PlaceOrderCommand { + Context = new CalculationContext(customer, new Store { Id = "store" }, new Currency { CurrencyCode = "EUR", Rate = 0.95 }, + TaxDisplayType.IncludingTax), + Language = new Language { Id = "pl" }, + Host = new DomainHost { Url = "https://shop.example/" } + }; + + //Act + var result = await _handler.Handle(command, CancellationToken.None); + + //Assert + _checkoutAttributeFormatterMock.Verify(x => x.FormatAttributes(It.IsAny>(), command.Context, + command.Language, command.Host, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + Assert.IsTrue(result.Errors.Any(e => e.Contains("Cart is empty"))); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs index 1fd906e096..483434ff51 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs @@ -2,7 +2,6 @@ using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Domain.Customers; -using Grand.Infrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -13,28 +12,40 @@ public class CustomerLoggedInEventHandlerTests { private CustomerLoggedInEventHandler _customerLoggedInEventHandler; private Mock _shoppingCartServiceMock; - private Mock _workContextMock; [TestInitialize] public void Init() { _shoppingCartServiceMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(s => s.WorkContext.CurrentCustomer).Returns(new Customer()); - _customerLoggedInEventHandler = - new CustomerLoggedInEventHandler(_shoppingCartServiceMock.Object, _workContextMock.Object); + _customerLoggedInEventHandler = new CustomerLoggedInEventHandler(_shoppingCartServiceMock.Object); } [TestMethod] public async Task HandleTest() { //Arrange - var notification = new CustomerLoggedInEvent(new Customer()); + var notification = new CustomerLoggedInEvent(new Customer(), new Customer(), ""); //Act await _customerLoggedInEventHandler.Handle(notification, CancellationToken.None); //Assert - _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(It.IsAny(), It.IsAny(), true), + _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(It.IsAny(), It.IsAny(), + It.IsAny(), true), Times.Once); } + + [TestMethod] + public async Task Handle_MigratesFromThePreviousCustomerInTheEventStore() + { + //Arrange + var guest = new Customer(); + var customer = new Customer(); + + //Act + await _customerLoggedInEventHandler.Handle(new CustomerLoggedInEvent(customer, guest, "store-b"), + CancellationToken.None); + + //Assert + _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(guest, customer, "store-b", true), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs new file mode 100644 index 0000000000..5c3dbc8415 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs @@ -0,0 +1,106 @@ +using Grand.Business.Checkout.Services.CheckoutAttributes; +using Grand.Business.Core.Interfaces.Catalog.Prices; +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.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.Checkout.Tests.Services.CheckoutAttributes; + +[TestClass] +public class CheckoutAttributeFormatterTests +{ + private readonly Customer _customer = new(); + private readonly Currency _eur = new() { CurrencyCode = "EUR" }; + private readonly DomainHost _host = new() { Url = "https://shop.example/" }; + private readonly Language _language = new() { Id = "pl" }; + private readonly Store _store = new() { Id = "store" }; + private CheckoutAttributeFormatter _formatter; + private Mock _parserMock; + private Mock _taxServiceMock; + + [TestInitialize] + public void Init() + { + _parserMock = new Mock(); + _taxServiceMock = new Mock(); + var currencyService = new Mock(); + currencyService.Setup(x => x.ConvertFromPrimaryStoreCurrency(12.3, _eur)).ReturnsAsync(11.69); + var priceFormatter = new Mock(); + priceFormatter.Setup(x => x.FormatPrice(11.69, _eur)).Returns("€11.69"); + _formatter = new CheckoutAttributeFormatter(_parserMock.Object, currencyService.Object, + _taxServiceMock.Object, priceFormatter.Object); + } + + private CalculationContext Context(TaxDisplayType taxDisplayType) => + new(_customer, _store, _eur, taxDisplayType); + + [TestMethod] + public async Task FormatAttributes_PricesTheValueInTheContextCurrencyAndTaxDisplay() + { + //Arrange + var value = new CheckoutAttributeValue { Id = "yes", Name = "Yes", PriceAdjustment = 10 }; + var attribute = new CheckoutAttribute { + Id = "wrap", Name = "Gift wrapping", AttributeControlTypeId = AttributeControlType.DropdownList, + CheckoutAttributeValues = { value } + }; + var selected = new List { new() { Key = "wrap", Value = "yes" } }; + _parserMock.Setup(x => x.ParseCheckoutAttributes(selected)).ReturnsAsync(new List { attribute }); + _taxServiceMock.Setup(x => x.GetCheckoutAttributePrice(attribute, value, true, _customer, _store)) + .ReturnsAsync((12.3, 23)); + + //Act + var result = await _formatter.FormatAttributes(selected, Context(TaxDisplayType.IncludingTax), _language, _host, + htmlEncode: false); + + //Assert + Assert.AreEqual("Gift wrapping: Yes [+€11.69]", result); + } + + [TestMethod] + public async Task FormatAttributes_TranslatesNamesIntoTheGivenLanguage() + { + //Arrange + var attribute = new CheckoutAttribute { + Id = "msg", Name = "Gift message", AttributeControlTypeId = AttributeControlType.TextBox, + Locales = { new TranslationEntity { LanguageId = "pl", LocaleKey = "Name", LocaleValue = "Wiadomość" } } + }; + var selected = new List { new() { Key = "msg", Value = "Hi" } }; + _parserMock.Setup(x => x.ParseCheckoutAttributes(selected)).ReturnsAsync(new List { attribute }); + + //Act + var result = await _formatter.FormatAttributes(selected, Context(TaxDisplayType.ExcludingTax), _language, _host, + htmlEncode: false); + + //Assert + Assert.AreEqual("Wiadomość: Hi", result); + } + + [TestMethod] + public async Task FormatAttributes_BuildsFileLinksOnTheGivenHost() + { + //Arrange + var downloadGuid = Guid.NewGuid(); + var attribute = new CheckoutAttribute { + Id = "file", Name = "Logo", TextPrompt = "Download", AttributeControlTypeId = AttributeControlType.FileUpload + }; + var selected = new List { new() { Key = "file", Value = downloadGuid.ToString() } }; + _parserMock.Setup(x => x.ParseCheckoutAttributes(selected)).ReturnsAsync(new List { attribute }); + + //Act + var result = await _formatter.FormatAttributes(selected, Context(TaxDisplayType.ExcludingTax), _language, _host); + + //Assert + StringAssert.Contains(result, $"https://shop.example/download/getfileupload/?downloadId={downloadGuid}"); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs index 5c5131eec3..4366aafc1d 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs @@ -23,7 +23,6 @@ public class CheckoutAttributeParserTests private IRepository _checkoutAttributeRepo; private ICheckoutAttributeService _checkoutAttributeService; private IMediator _eventPublisher; - private Mock _workContextMock; private CheckoutAttribute ca1, ca2, ca3; private Mock cacheManager; @@ -119,10 +118,9 @@ public void TestInitialize() } cacheManager = new Mock(); - _workContextMock = new Mock(); _checkoutAttributeService = new CheckoutAttributeService(cacheManager.Object, _checkoutAttributeRepo, - _eventPublisher, _workContextMock.Object, new AccessControlConfig()); + _eventPublisher, new AccessControlConfig()); _checkoutAttributeParser = new CheckoutAttributeParser(_checkoutAttributeService); diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs index 71eff8af83..573c44188e 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs @@ -1,8 +1,10 @@ using Grand.Business.Checkout.Services.CheckoutAttributes; using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; +using Grand.Data.Tests.MongoDb; +using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Events; @@ -19,7 +21,6 @@ public class CheckoutAttributeServiceTests private Mock _mediatorMock; private Mock> _repositoryMock; private ICheckoutAttributeService _service; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -27,9 +28,8 @@ public void Init() _cacheMock = new Mock(); _repositoryMock = new Mock>(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); _service = new CheckoutAttributeService(_cacheMock.Object, _repositoryMock.Object, _mediatorMock.Object, - _workContextMock.Object, new AccessControlConfig()); + new AccessControlConfig()); } [TestMethod] @@ -76,4 +76,73 @@ public void DeleteCheckoutAttribute_NullArgument_ThrowException() { Assert.ThrowsExactlyAsync(async () => await _service.DeleteCheckoutAttribute(null)); } + + [TestMethod] + public async Task GetAllCheckoutAttributes_WithoutAccess_ThrowException() + { + await Assert.ThrowsExactlyAsync(async () => + await _service.GetAllCheckoutAttributes(null)); + } + + [TestMethod] + public async Task GetAllCheckoutAttributes_IncludingHidden_ReturnsGroupLimitedAttributes() + { + //Arrange + var repository = new MongoDBRepositoryTest(); + await repository.InsertAsync(new CheckoutAttribute { Name = "members only", LimitedToGroups = true, CustomerGroups = ["members"] }); + await repository.InsertAsync(new CheckoutAttribute { Name = "everyone" }); + var cache = new Mock(); + cache.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + var service = new CheckoutAttributeService(cache.Object, repository, _mediatorMock.Object, new AccessControlConfig()); + + //Act + var result = await service.GetAllCheckoutAttributes(CatalogAccess.IncludingHidden()); + + //Assert + Assert.AreEqual(2, result.Count); + } + + [TestMethod] + public async Task GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomer() + { + //Arrange + var repository = new MongoDBRepositoryTest(); + await repository.InsertAsync(new CheckoutAttribute { Name = "members only", LimitedToGroups = true, CustomerGroups = ["members"] }); + await repository.InsertAsync(new CheckoutAttribute { Name = "everyone" }); + var cache = new Mock(); + cache.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + var service = new CheckoutAttributeService(cache.Object, repository, _mediatorMock.Object, new AccessControlConfig()); + var guest = new Customer(); + guest.Groups.Add("guests"); + + //Act + var result = await service.GetAllCheckoutAttributes(CatalogAccess.For(guest, "")); + + //Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual("everyone", result[0].Name); + } + + [TestMethod] + public async Task GetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroups() + { + //Arrange + var keys = new List(); + _cacheMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Callback((string key, Func>> _) => keys.Add(key)) + .ReturnsAsync(new List()); + var member = new Customer(); + member.Groups.Add("members"); + var guest = new Customer(); + guest.Groups.Add("guests"); + + //Act + await _service.GetAllCheckoutAttributes(CatalogAccess.For(member, "store")); + await _service.GetAllCheckoutAttributes(CatalogAccess.For(guest, "store")); + + //Assert + Assert.AreNotEqual(keys[0], keys[1]); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs index 46ede9fa40..9c883d8dc3 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs @@ -26,26 +26,17 @@ public class ShoppingCartServiceTests private ShoppingCartService _shoppingCartService; private ShoppingCartSettings _shoppingCartSettings; private Mock _shoppingCartValidatorMock; - private Mock _workContextMock; [TestInitialize] public void Init() { - _workContextMock = new Mock(); _productServiceMock = new Mock(); _shoppingCartSettings = new ShoppingCartSettings(); _customerServiceMock = new Mock(); _mediatorMock = new Mock(); _shoppingCartValidatorMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - var customer = new Customer(); - customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); - _workContextMock.Setup(c => c.WorkContext.WorkingCurrency).Returns(() => new Currency()); - _workContextMock.Setup(c => c.WorkContext.TaxDisplayType).Returns(() => TaxDisplayType.ExcludingTax); - - _shoppingCartService = new ShoppingCartService(_workContextMock.Object, _productServiceMock.Object, + _shoppingCartService = new ShoppingCartService(_productServiceMock.Object, _customerServiceMock.Object, _mediatorMock.Object, _shoppingCartValidatorMock.Object, _shoppingCartSettings); } @@ -57,13 +48,12 @@ public async Task GetShoppingCartTest() customer.Groups.Add("1"); customer.ShoppingCartItems.Add(new ShoppingCartItem { ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); //Act - var result = await _shoppingCartService.GetShoppingCart(); + var result = await _shoppingCartService.GetShoppingCart(customer); //Assert Assert.IsNotEmpty(result); } @@ -78,7 +68,6 @@ public async Task FindShoppingCartItemTest() { ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "", ProductId = "1" }); customer.ShoppingCartItems.Add(new ShoppingCartItem { ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "", ProductId = "2" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -97,7 +86,6 @@ public async Task AddToCartTest_Success() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -122,7 +110,6 @@ public async Task AddToCartTest_Warning() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -147,7 +134,6 @@ public async Task UpdateShoppingCartItemTest() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -174,7 +160,6 @@ public async Task DeleteShoppingCartItemTest() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -200,7 +185,6 @@ public async Task MigrateShoppingCartTest() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -211,13 +195,101 @@ public async Task MigrateShoppingCartTest() _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); var customer2 = new Customer(); //Act - await _shoppingCartService.MigrateShoppingCart(customer, customer2, false); + await _shoppingCartService.MigrateShoppingCart(customer, customer2, "", false); //Assert Assert.IsNotEmpty(customer2.ShoppingCartItems); Assert.IsEmpty(customer.ShoppingCartItems); } + + [TestMethod] + public async Task GetShoppingCart_ReturnsTheItemsOfTheGivenCustomer() + { + //Arrange + var customer = new Customer(); + customer.ShoppingCartItems.Add(new ShoppingCartItem + { ProductId = "1", ShoppingCartTypeId = ShoppingCartType.Wishlist, StoreId = "store" }); + _productServiceMock.Setup(a => a.GetProductById("1", false)).ReturnsAsync(new Product { Id = "1" }); + + //Act + var result = await _shoppingCartService.GetShoppingCart(customer, "store", ShoppingCartType.Wishlist); + + //Assert + Assert.HasCount(1, result); + Assert.AreSame(customer.ShoppingCartItems.First(), result[0]); + } + + [TestMethod] + public async Task MigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetCartYet() + { + //Arrange + var guest = new Customer(); + guest.ShoppingCartItems.Add(new ShoppingCartItem + { ProductId = "A", ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "", Quantity = 1 }); + var customer = new Customer(); + _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)) + .ReturnsAsync(new Product { Id = "A", Published = true }); + _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny(), null, null, + It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + //Act + await _shoppingCartService.MigrateShoppingCart(guest, customer, "", false); + + //Assert + //the guest's whole cart moves together, so a required product may still be on its way to the target cart + _shoppingCartValidatorMock.Verify(x => x.GetShoppingCartItemWarnings(customer, It.IsAny(), + It.IsAny(), It.Is(o => !o.GetRequiredProductWarnings)), Times.Once); + } + + [TestMethod] + public async Task MigrateShoppingCart_ChecksRequiredProductsAgainstTheSourceCart() + { + //Arrange + var guest = new Customer(); + var item = new ShoppingCartItem + { ProductId = "A", ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "store", Quantity = 1 }; + guest.ShoppingCartItems.Add(item); + var customer = new Customer(); + var product = new Product { Id = "A", Published = true, RequireOtherProducts = true, RequiredProductIds = "B" }; + _productServiceMock.Setup(a => a.GetProductById("A", false)).ReturnsAsync(product); + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(guest, item, product, "store")) + .ReturnsAsync(new List { "Product B is required" }); + + //Act + await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", false); + + //Assert + Assert.IsEmpty(customer.ShoppingCartItems); + _shoppingCartValidatorMock.Verify(x => x.GetShoppingCartItemWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task MigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStore() + { + //Arrange + var fromCustomer = new Customer(); + var toCustomer = new Customer(); + + //Act + await _shoppingCartService.MigrateShoppingCart(fromCustomer, toCustomer, "store-b", false); + + //Assert + _customerServiceMock.Verify(x => x.UpdateUserField(toCustomer, SystemCustomerFieldNames.CheckoutAttributes, + It.IsAny>(), "store-b"), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs new file mode 100644 index 0000000000..b2503596ba --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs @@ -0,0 +1,85 @@ +using FluentValidation; +using FluentValidation.Results; +using Grand.Business.Checkout.Services.Orders; +using Grand.Business.Checkout.Validators; +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.Orders; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Grand.Infrastructure.Validators; +using Grand.Mediator; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using IValidatorFactory = Grand.Infrastructure.Validators.IValidatorFactory; + +namespace Grand.Business.Checkout.Tests.Services.Orders; + +[TestClass] +public class ShoppingCartValidatorTests +{ + private readonly List _records = []; + private ShoppingCartValidator _validator; + private Mock _validatorFactoryMock; + + [TestInitialize] + public void Init() + { + _validatorFactoryMock = new Mock(); + Capture(); + Capture(); + Capture(); + Capture(); + _validator = new ShoppingCartValidator(new Mock().Object, _validatorFactoryMock.Object); + } + + private void Capture() + { + var validator = new Mock>(); + validator.Setup(x => x.ValidateAsync(It.IsAny(), It.IsAny())) + .Callback((T record, CancellationToken _) => _records.Add(record)) + .ReturnsAsync(new ValidationResult()); + _validatorFactoryMock.Setup(x => x.GetValidator()).Returns(validator.Object); + } + + [TestMethod] + public async Task GetShoppingCartWarnings_ValidatesForTheContextCustomerAndStore() + { + //Arrange + var context = new CalculationContext(new Customer(), new Store { Id = "store" }, + new Currency { CurrencyCode = "EUR" }, TaxDisplayType.IncludingTax); + var cart = new List(); + + //Act + await _validator.GetShoppingCartWarnings(cart, new List(), context, true, true); + + //Assert + var warnings = _records.OfType().Single(); + Assert.AreSame(context.Customer, warnings.Customer); + Assert.AreSame(context.Store, warnings.Store); + var attributes = _records.OfType().Single(); + Assert.AreSame(context.Customer, attributes.Customer); + Assert.AreSame(context.Store, attributes.Store); + var amount = _records.OfType().Single(); + Assert.AreSame(context, amount.Context); + } + + [TestMethod] + public async Task GetRequiredProductWarnings_UsesTheGivenCustomerAndStore() + { + //Arrange + var customer = new Customer(); + var product = new Product { RequireOtherProducts = true }; + + //Act + await _validator.GetRequiredProductWarnings(customer, new ShoppingCartItem(), product, "store-b"); + + //Assert + var record = _records.OfType().Single(); + Assert.AreSame(customer, record.Customer); + Assert.AreEqual("store-b", record.StoreId); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs index eed2d7d6c0..259ffd2ffe 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -123,7 +124,7 @@ public async Task ShoppingCartCheckoutAttributesValidator_Success() } })); - _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), true, false)) + _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.Is(a => !a.ShowHidden), true)) .Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Name = "test", IsRequired = true, AttributeControlTypeId = AttributeControlType.TextBox @@ -162,7 +163,7 @@ public async Task ShoppingCartCheckoutAttributesValidator_Fail() } })); - _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), true, false)) + _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.Is(a => !a.ShowHidden), true)) .Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Name = "test", IsRequired = true, AttributeControlTypeId = AttributeControlType.TextBox, @@ -337,7 +338,7 @@ public async Task ShoppingCartRequiredProductValidator_Success() var result = await shoppingCartRequiredProductValidator.ValidateAsync( new ShoppingCartRequiredProductValidatorRecord( customer, - new Store { Id = "1" }, + "1", product, new ShoppingCartItem { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } @@ -375,7 +376,7 @@ public async Task ShoppingCartRequiredProductValidator_Fail() var result = await shoppingCartRequiredProductValidator.ValidateAsync( new ShoppingCartRequiredProductValidatorRecord( customer, - new Store { Id = "1" }, + "1", product, new ShoppingCartItem { ProductId = "2", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } @@ -567,7 +568,6 @@ public async Task ShoppingCartCommonWarningsValidator_Success() var result = await shoppingCartCommonWarningsValidator.ValidateAsync( new ShoppingCartCommonWarningsValidatorRecord( customer, - new Store { Id = "1" }, new List { new() { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" @@ -603,7 +603,6 @@ public async Task ShoppingCartCommonWarningsValidator_Fail() var result = await shoppingCartCommonWarningsValidator.ValidateAsync( new ShoppingCartCommonWarningsValidatorRecord( customer, - new Store { Id = "1" }, new List { new() { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" diff --git a/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs b/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs index 9df29753d4..879f426e88 100644 --- a/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs +++ b/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs @@ -29,7 +29,7 @@ public async Task HandleTest() CannotLoginUntilDateUtc = DateTime.Now }; //Act - await _handler.Handle(new CustomerLoggedInEvent(customer), CancellationToken.None); + await _handler.Handle(new CustomerLoggedInEvent(customer, new Customer(), ""), CancellationToken.None); //Assert Assert.AreEqual(0, customer.FailedLoginAttempts); diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs new file mode 100644 index 0000000000..16564e2f88 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs @@ -0,0 +1,62 @@ +using Grand.Business.Core.Interfaces.Catalog.Directory; +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.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Directory; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CheckoutAttributeViewModelServiceTests +{ + private Mock _checkoutAttributeServiceMock; + private CheckoutAttributeViewModelService _service; + + [TestInitialize] + public void Setup() + { + _checkoutAttributeServiceMock = new Mock(); + var parserMock = new Mock(); + parserMock.Setup(x => x.ParseCheckoutAttributes(It.IsAny>())) + .ReturnsAsync(new List()); + parserMock.Setup(x => x.ParseCheckoutAttributeValues(It.IsAny>())) + .ReturnsAsync(new List()); + + _service = new CheckoutAttributeViewModelService(_checkoutAttributeServiceMock.Object, parserMock.Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new CurrencySettings(), new Mock().Object, + new MeasureSettings(), new Mock().Object); + } + + [TestMethod] + public async Task PrepareConditionAttributes_OffersAttributesLimitedToCustomerGroups() + { + //Arrange + var limited = new CheckoutAttribute { + Id = "limited", Name = "Members gift", LimitedToGroups = true, CustomerGroups = ["members"], + AttributeControlTypeId = AttributeControlType.DropdownList + }; + _checkoutAttributeServiceMock + .Setup(x => x.GetAllCheckoutAttributes(It.Is(a => a.ShowHidden), false)) + .ReturnsAsync(new List { limited }); + var model = new CheckoutAttributeModel(); + + //Act + await _service.PrepareConditionAttributes(model, new CheckoutAttribute { Id = "edited" }); + + //Assert + Assert.AreEqual(1, model.ConditionModel.ConditionAttributes.Count); + Assert.AreEqual("limited", model.ConditionModel.ConditionAttributes[0].Id); + } +} diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs index 9cec195e40..3073dd5c97 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs @@ -49,6 +49,8 @@ public void Init() _customers = new Mock(); _customers.Setup(x => x.GetCustomerByEmail("a@b.c", It.IsAny())).ReturnsAsync(_customer); var context = new Mock(); + context.Setup(x => x.StoreContext.CurrentStore).Returns(new Domain.Stores.Store { Id = "store" }); + context.Setup(x => x.WorkContext.CurrentCustomer).Returns(new Customer()); var settings = new CustomerSettings { TwoFactorAuthenticationEnabled = true }; _controller = new AccountController(new Mock().Object, new Mock().Object, context.Object, _customers.Object, new Mock().Object, _manager.Object, new Mock().Object, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index aab0f6e03c..6dbde89931 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -219,7 +219,8 @@ protected async Task SignInAction(Customer customer, bool createP await _authenticationService.SignIn(customer, createPersistent); //raise event - await _mediator.Publish(new CustomerLoggedInEvent(customer)); + await _mediator.Publish(new CustomerLoggedInEvent(customer, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); return RedirectToRoute($"{GetCurrentArea()}Index"); } diff --git a/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs index da4b224bb4..33753db972 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Directory; @@ -29,7 +30,7 @@ public class CheckoutAttributeViewModelService( { public virtual async Task> PrepareCheckoutAttributeListModel(string storeId = "") { - var checkoutAttributes = await checkoutAttributeService.GetAllCheckoutAttributes(storeId: storeId, ignoreAcl: true); + var checkoutAttributes = await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.IncludingHidden(storeId)); return checkoutAttributes.Select((Func)(x => { var attributeModel = x.ToModel(); @@ -170,7 +171,7 @@ public virtual async Task PrepareConditionAttributes(CheckoutAttributeModel mode model.ConditionModel = new ConditionModel { EnableCondition = checkoutAttribute.ConditionAttribute.Any(), SelectedAttributeId = selectedAttribute != null ? selectedAttribute.Id : "", - ConditionAttributes = (await checkoutAttributeService.GetAllCheckoutAttributes(ignoreAcl: false)) + ConditionAttributes = (await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.IncludingHidden())) //ignore this attribute and non-combinable attributes .Where(x => x.Id != checkoutAttribute.Id && x.CanBeUsedAsCondition()) .Select(x => diff --git a/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs index 0a01f349a3..c380626aaa 100644 --- a/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -32,7 +33,7 @@ public async Task> Handle(SaveCheckoutAttributesCommand r var customAttributes = new List(); var checkoutAttributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, + await _checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(request.Customer, request.Store.Id), !request.Cart.RequiresShipping()); foreach (var attribute in checkoutAttributes) switch (attribute.AttributeControlTypeId) diff --git a/src/Web/Grand.Web/Components/EmailWishlist.cs b/src/Web/Grand.Web/Components/EmailWishlist.cs index 3faad0e9ef..51c2479324 100644 --- a/src/Web/Grand.Web/Components/EmailWishlist.cs +++ b/src/Web/Grand.Web/Components/EmailWishlist.cs @@ -39,7 +39,7 @@ public async Task InvokeAsync() !_shoppingCartSettings.EmailWishlistEnabled) return Content(""); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); if (!cart.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/EstimateShipping.cs b/src/Web/Grand.Web/Components/EstimateShipping.cs index a718f516e2..83b59cf1fd 100644 --- a/src/Web/Grand.Web/Components/EstimateShipping.cs +++ b/src/Web/Grand.Web/Components/EstimateShipping.cs @@ -24,7 +24,7 @@ public EstimateShippingViewComponent(IMediator mediator, IShoppingCartService sh public async Task InvokeAsync() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart); var model = await _mediator.Send(new GetEstimateShipping { diff --git a/src/Web/Grand.Web/Controllers/AccountController.cs b/src/Web/Grand.Web/Controllers/AccountController.cs index 8738e8d078..101fa48163 100644 --- a/src/Web/Grand.Web/Controllers/AccountController.cs +++ b/src/Web/Grand.Web/Controllers/AccountController.cs @@ -357,7 +357,8 @@ private async Task SignInAction(Customer customer, bool createPer string returnUrl = null) { //raise event - await _mediator.Publish(new CustomerLoggedInEvent(customer)); + await _mediator.Publish(new CustomerLoggedInEvent(customer, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); //sign in new customer await _authenticationService.SignIn(customer, createPersistentCookie); diff --git a/src/Web/Grand.Web/Controllers/ActionCartController.cs b/src/Web/Grand.Web/Controllers/ActionCartController.cs index 0ecb50ebc4..f40980bd54 100644 --- a/src/Web/Grand.Web/Controllers/ActionCartController.cs +++ b/src/Web/Grand.Web/Controllers/ActionCartController.cs @@ -182,7 +182,7 @@ public virtual async Task> AddProductCatalog(Pr var warehouseId = GetWarehouse(product); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, model.ShoppingCartTypeId); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, model.ShoppingCartTypeId); if (model.ShoppingCartTypeId != ShoppingCartType.Wishlist) { @@ -255,7 +255,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, }); //display notification message and update appropriate blocks - var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)).Sum(x => x.Quantity); var updatetopwishlistsectionhtml = string.Format(_translationService.GetResource("Wishlist.HeaderQuantity"), qty); @@ -289,7 +289,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, var updatetopcartsectionhtml = string.Format( _translationService.GetResource("ShoppingCart.HeaderQuantity"), - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity)); @@ -451,7 +451,7 @@ public virtual async Task> AddProductDetails(Pr }); //display notification message and update appropriate blocks - var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)).Sum(x => x.Quantity); var updatetopwishlistsectionhtml = string.Format(_translationService.GetResource("Wishlist.HeaderQuantity"), qty); @@ -485,7 +485,7 @@ public virtual async Task> AddProductDetails(Pr var updatetopcartsectionhtml = string.Format( _translationService.GetResource("ShoppingCart.HeaderQuantity"), - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity)); diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 3ba659f5bd..8b62cb1bdb 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -19,6 +19,7 @@ using Grand.Infrastructure.Extensions; using Grand.SharedKernel.Attributes; using Grand.Web.Common.Controllers; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Filters; using Grand.Web.Extensions; using Grand.Web.Features.Models.Checkout; @@ -87,7 +88,7 @@ public virtual async Task Index() { var customer = _contextAccessor.WorkContext.CurrentCustomer; - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); if (!cart.Any()) @@ -101,7 +102,8 @@ public virtual async Task Index() //validation (cart) var checkoutAttributes = customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); - var scWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, true, true); + var scWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, + _contextAccessor.CalculationContext(), true, true); if (scWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); @@ -123,7 +125,7 @@ await _shoppingCartValidator.GetShoppingCartItemWarnings(customer, sci, product, public virtual async Task Start() { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); if (!cart.Any()) @@ -141,7 +143,8 @@ public virtual async Task Start() var checkoutAttributes = _contextAccessor.WorkContext.CurrentCustomer .GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); - var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, true, true); + var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, + _contextAccessor.CalculationContext(), true, true); if (cartWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); @@ -188,7 +191,7 @@ public virtual async Task SaveBilling( try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -286,7 +289,7 @@ public virtual async Task SaveShipping( try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -462,7 +465,7 @@ public virtual async Task SaveShippingMethod(CheckoutShippingMeth var customer = _contextAccessor.WorkContext.CurrentCustomer; var store = _contextAccessor.StoreContext.CurrentStore; - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -547,7 +550,7 @@ public virtual async Task SavePaymentMethod(CheckoutPaymentMethod try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -617,7 +620,7 @@ public virtual async Task SavePaymentInfo(IDictionary ConfirmOrder() try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -717,7 +720,12 @@ public virtual async Task ConfirmOrder() error = 1, message = _translationService.GetResource("Checkout.MinOrderPlacementInterval") }); - var placeOrderResult = await _mediator.Send(new PlaceOrderCommand()); + var placeOrderResult = await _mediator.Send(new PlaceOrderCommand { + Context = _contextAccessor.CalculationContext(), + Language = _contextAccessor.WorkContext.WorkingLanguage, + Host = _contextAccessor.StoreContext.CurrentHost, + OriginalCustomerIfImpersonated = _contextAccessor.WorkContext.OriginalCustomerIfImpersonated + }); if (placeOrderResult.Success) { var paymentMethod = diff --git a/src/Web/Grand.Web/Controllers/ProductController.cs b/src/Web/Grand.Web/Controllers/ProductController.cs index bcb7801b8b..e3a19dc491 100644 --- a/src/Web/Grand.Web/Controllers/ProductController.cs +++ b/src/Web/Grand.Web/Controllers/ProductController.cs @@ -239,7 +239,7 @@ public virtual async Task GetDatesForMonth(string productId, int if (!string.IsNullOrEmpty(parameter)) query = query.Where(x => x.Parameter == parameter); var reservations = query.ToList(); - var inCart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id)) + var inCart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)) .Where(x => !string.IsNullOrEmpty(x.ReservationId)).ToList(); foreach (var cartItem in inCart) { diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index 923eeb86c7..c4d7369fbc 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -127,7 +127,7 @@ public virtual async Task CheckoutAttributeChange(CheckoutAttribu [FromServices] ICheckoutAttributeParser checkoutAttributeParser, [FromServices] ICheckoutAttributeFormatter checkoutAttributeFormatter) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var checkoutAttributes = await _mediator.Send(new SaveCheckoutAttributesCommand { @@ -140,8 +140,7 @@ public virtual async Task CheckoutAttributeChange(CheckoutAttribu var enabledAttributeIds = new List(); var disabledAttributeIds = new List(); var attributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.StoreContext.CurrentStore.Id, - !cart.RequiresShipping()); + await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.CatalogAccess(), !cart.RequiresShipping()); foreach (var attribute in attributes) { var conditionMet = await checkoutAttributeParser.IsConditionMet(attribute, checkoutAttributes); @@ -168,7 +167,8 @@ await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.StoreC disabledattributeids = disabledAttributeIds.ToArray(), model = orderTotals, checkoutattributeinfo = - await checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore) + await checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, _contextAccessor.CalculationContext(), + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost) }); } @@ -253,7 +253,7 @@ public virtual async Task> Cart(bool checkoutAtt if (!await _permissionService.Authorize(StandardPermission.EnableShoppingCart)) return RedirectToRoute("HomePage"); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var model = await _mediator.Send(new GetShoppingCart { Cart = cart, ValidateCheckoutAttributes = checkoutAttributes, @@ -270,7 +270,7 @@ public virtual async Task> Cart(bool checkoutAtt [DenySystemAccount] public async Task> CartSummary() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var model = await _mediator.Send(new GetShoppingCart { @@ -290,7 +290,7 @@ public async Task> CartSummary() [DenySystemAccount] public async Task> CartTotal() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var model = await _mediator.Send(new GetOrderTotals { @@ -315,7 +315,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode warnings = string.Join(',', ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))) }); - var cart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) + var cart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) .FirstOrDefault(x => x.Id == model.ShoppingCartId); var warnings = new List(); @@ -329,7 +329,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode } var cartModel = await _mediator.Send(new GetShoppingCart { - Cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()), + Cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()), Customer = _contextAccessor.WorkContext.CurrentCustomer, Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, @@ -353,7 +353,7 @@ public virtual async Task ClearCart() if (!await _permissionService.Authorize(StandardPermission.EnableShoppingCart)) return RedirectToRoute("HomePage"); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); foreach (var sci in cart) await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, sci, @@ -373,7 +373,7 @@ public virtual async Task DeleteCartItem(DeleteCartItemModel mode if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .FirstOrDefault(sci => sci.Id == model.Id); @@ -395,7 +395,7 @@ await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.C sidebarshoppingcartmodel = miniShoppingCartmodel }); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var shoppingcartmodel = await _mediator.Send(new GetShoppingCart { Cart = cart, Customer = _contextAccessor.WorkContext.CurrentCustomer, @@ -427,7 +427,7 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .FirstOrDefault(sci => sci.Id == model.Id); @@ -445,7 +445,7 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo Store = _contextAccessor.StoreContext.CurrentStore }); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var shoppingcartmodel = await _mediator.Send(new GetShoppingCart { Cart = cart, Customer = _contextAccessor.WorkContext.CurrentCustomer, @@ -472,7 +472,7 @@ public virtual IActionResult ContinueShopping() [DenySystemAccount] public virtual async Task StartCheckout(CheckoutAttributeSelectedModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); List checkoutAttributes; //parse and save checkout attributes @@ -489,7 +489,8 @@ public virtual async Task StartCheckout(CheckoutAttributeSelected SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); var checkoutAttributeWarnings = - await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, true, true); + await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, + _contextAccessor.CalculationContext(), true, true); if (checkoutAttributeWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); //everything is OK @@ -504,7 +505,7 @@ public virtual async Task StartCheckout(CheckoutAttributeSelected [HttpPost] public virtual async Task ApplyDiscountCoupon(DiscountCouponModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var message = string.Empty; var isApplied = false; @@ -547,7 +548,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto [HttpPost] public virtual async Task ApplyGiftVoucher(GiftVoucherCouponModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var message = string.Empty; var isApplied = false; @@ -590,7 +591,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto [HttpPost] public virtual async Task GetEstimateShipping(EstimateShippingModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var result = await _mediator.Send(new GetEstimateShippingResult { @@ -628,7 +629,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto } } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var model = await _mediator.Send(new GetShoppingCart { Cart = cart, @@ -663,7 +664,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto } } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var model = await _mediator.Send(new GetShoppingCart { Cart = cart, diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 5afe8ecc0f..c6adf55a12 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -119,7 +119,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode if (ModelState.IsValid) { var cart = - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) .FirstOrDefault(x => x.Id == model.ShoppingCartId); if (cart != null) { @@ -141,7 +141,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode success = !warnings.Any(), warnings = string.Join(", ", warnings), totalproducts = - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) .Sum(x => x.Quantity) }); } @@ -216,7 +216,7 @@ public virtual async Task EmailWishlist(WishlistEmailAFriendModel !_shoppingCartSettings.EmailWishlistEnabled) return Content(""); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); if (!cart.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 72395fb36c..c5a80f3bc4 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -132,7 +132,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri model.Picture = await PrepareCartItemPicture(request); } - var cart = await _shoppingCartService.GetShoppingCart(request.Store.Id, request.CartType); + var cart = await _shoppingCartService.GetShoppingCart(request.Customer, request.Store.Id, request.CartType); if (request.CartType != ShoppingCartType.Auctions) { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 99431262da..40fb640ec8 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -8,6 +8,7 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -103,7 +104,7 @@ public async Task Handle(GetMiniShoppingCart request, if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var cart = await _shoppingCartService.GetShoppingCart(request.Store.Id, shoppingCartTypes.ToArray()); + var cart = await _shoppingCartService.GetShoppingCart(request.Customer, request.Store.Id, shoppingCartTypes.ToArray()); model.TotalProducts = cart.Sum(x => x.Quantity); if (!cart.Any()) return model; @@ -121,7 +122,7 @@ public async Task Handle(GetMiniShoppingCart request, var requiresShipping = cart.RequiresShipping(); var checkoutAttributesExist = - (await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, !requiresShipping)) + (await _checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(request.Customer, request.Store.Id), !requiresShipping)) .Any(); var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 37f3c6f485..3d1799d183 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -12,6 +12,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Permissions; using Grand.Domain.Catalog; @@ -145,6 +146,9 @@ 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 @@ -160,7 +164,8 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC request.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, request.Store.Id); model.CheckoutAttributeInfo = - await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, request.Customer, request.Store); + await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, CalculationContextOf(request), + request.Language, _contextAccessor.StoreContext.CurrentHost); if (!request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) .ToList().Any()) { @@ -169,7 +174,7 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC else { var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { - Context = _contextAccessor.CalculationContext(), + Context = CalculationContextOf(request), Cart = request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() }); @@ -193,8 +198,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu { var discount = await _discountService.GetDiscountByCouponCode(couponCode); if (discount is { RequiresCouponCode: true } && - (await _discountValidationService.ValidateDiscount(discount, request.Customer, request.Store, - request.Currency)).IsValid) + (await _discountValidationService.ValidateDiscount(discount, request.Customer, request.Store, request.Currency)).IsValid) model.DiscountBox.AppliedDiscountsWithCodes.Add(new ShoppingCartModel.DiscountBoxModel.DiscountInfoModel { Id = discount.Id, CouponCode = couponCode @@ -205,6 +209,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu //cart warnings var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, + CalculationContextOf(request), request.ValidateCheckoutAttributes, true); foreach (var warning in cartWarnings) model.Warnings.Add(warning); @@ -217,7 +222,7 @@ private async Task PrepareCheckoutAttributes(ShoppingCartModel model, GetShoppin #region Checkout attributes var checkoutAttributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, + await _checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(request.Customer, request.Store.Id), !request.Cart.RequiresShipping()); foreach (var attribute in checkoutAttributes) { @@ -251,8 +256,8 @@ await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, //display price if allowed if (!await _permissionService.Authorize(StandardPermission.DisplayPrices)) continue; var priceAdjustmentBase = (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)) .checkoutPrice; var priceAdjustment = await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, request.Currency); @@ -451,20 +456,18 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req else { var unitprices = await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + request.Customer, request.Store, request.Currency); var appliedDiscounts = unitprices.appliedDiscounts; var productprices = await _taxService.GetProductPrice(product, unitprices.unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store); cartItemModel.UnitPriceWithoutDiscountValue = (await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, false)).unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + request.Customer, request.Store, request.Currency, false)).unitprice, + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)).productprice; cartItemModel.UnitPriceWithoutDiscount = _priceFormatter.FormatPrice(cartItemModel.UnitPriceWithoutDiscountValue, request.Currency); @@ -481,13 +484,12 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req //sub total var subtotal = await _pricingService.GetSubTotal(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + request.Customer, request.Store, request.Currency); var shoppingCartItemDiscountBase = subtotal.discountAmount; var shoppingCartItemSubTotalWithDiscount = (await _taxService.GetProductPrice(product, subtotal.subTotal, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)).productprice; cartItemModel.SubTotal = _priceFormatter.FormatPrice(shoppingCartItemSubTotalWithDiscount, request.Currency); cartItemModel.SubTotalValue = shoppingCartItemSubTotalWithDiscount; @@ -497,8 +499,8 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req { shoppingCartItemDiscountBase = (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)).productprice; if (shoppingCartItemDiscountBase > 0) cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index f6f43be4cb..472408f59a 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -15,6 +15,7 @@ using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Localization; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; @@ -76,6 +77,9 @@ 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 { @@ -95,7 +99,8 @@ public async Task Handle(GetWishlist request, CancellationToken c model.ShowSku = _catalogSettings.ShowSkuOnProductDetailsPage; //cart warnings - var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, false, false); + var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, + CalculationContextOf(request), false, false); foreach (var warning in cartWarnings) model.Warnings.Add(warning); diff --git a/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs b/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs index 5d0c48e401..8fc61cc9f3 100644 --- a/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs +++ b/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs @@ -36,7 +36,7 @@ public UpdateQuantityValidator( } } - var cart = (await shoppingCartService.GetShoppingCart(contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) + var cart = (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) .FirstOrDefault(z => z.Id == x.ShoppingCartId); if (cart == null) context.AddFailure("Shopping cart item not found"); }); diff --git a/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml b/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml index 1216b00608..68e2824aba 100644 --- a/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml +++ b/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml @@ -21,13 +21,13 @@ var shoppingCartItems = contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() - ? (await shoppingCartService.GetShoppingCart(contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) + ? (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity) : 0; var wishlistItems = contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() - ? (await shoppingCartService.GetShoppingCart(contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + ? (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) .Sum(x => x.Quantity) : 0; }