From a43f67b935db6b514ae7370bd1a86c5afbb8d76e Mon Sep 17 00:00:00 2001 From: Konstantin Savosteev Date: Mon, 25 Nov 2024 14:39:42 +0200 Subject: [PATCH] VCST-2092: add configurable products support (#14) --- src/VirtoCommerce.XCart.Core/CartAggregate.cs | 38 ++++- .../Commands/AddCartItemCommand.cs | 6 + .../CreateConfiguredLineItemCommand.cs | 22 +++ .../ConfiguredLineItemContainer.cs | 148 ++++++++++++++++++ .../ExpConfigurationLineItem.cs | 15 ++ .../Models/CartProductsRequest.cs | 21 +++ .../Models/ConfigurableProductOption.cs | 8 + .../Models/ExpConfigurationSection.cs | 19 +++ .../Models/ICartProductContainerRequest.cs | 10 ++ .../Models/ProductConfigurationSection.cs | 8 + .../Queries/GetProductConfigurationQuery.cs | 50 ++++++ .../Schemas/CartConfigurationItemType.cs | 14 ++ .../Schemas/ConfigurableProductOptionInput.cs | 13 ++ .../Schemas/ConfigurationLineItemType.cs | 86 ++++++++++ .../Schemas/ConfigurationQueryResponseType.cs | 15 ++ .../Schemas/ConfigurationSectionInput.cs | 13 ++ .../Schemas/ConfigurationSectionType.cs | 21 +++ .../Schemas/InputAddItemType.cs | 3 + .../InputCreateConfiguredLineItemCommand.cs | 16 ++ .../Schemas/LineItemType.cs | 10 +- .../Services/ICartProductsLoaderService.cs | 16 ++ .../IConfiguredLineItemContainerService.cs | 10 ++ .../VirtoCommerce.XCart.Core.csproj | 4 +- .../Commands/AddCartItemCommandHandler.cs | 36 ++++- .../CreateConfiguredLineItemBuilder.cs | 29 ++++ .../CreateConfiguredLineItemHandler.cs | 65 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Mapping/CartMappingProfile.cs | 32 ++++ .../GetProductConfigurationQueryBulder.cs | 54 +++++++ .../GetProductConfigurationQueryHandler.cs | 79 ++++++++++ .../Services/CartAggregateRepository.cs | 48 +++++- .../Services/CartProductService.cs | 122 ++++++++++++++- .../IConfiguredLineItemContainerService.cs | 59 +++++++ .../VirtoCommerce.XCart.Data.csproj | 2 +- src/VirtoCommerce.XCart.Web/module.manifest | 6 +- 35 files changed, 1084 insertions(+), 16 deletions(-) create mode 100644 src/VirtoCommerce.XCart.Core/Commands/CreateConfiguredLineItemCommand.cs create mode 100644 src/VirtoCommerce.XCart.Core/ConfiguredLineItemContainer.cs create mode 100644 src/VirtoCommerce.XCart.Core/ExpConfigurationLineItem.cs create mode 100644 src/VirtoCommerce.XCart.Core/Models/CartProductsRequest.cs create mode 100644 src/VirtoCommerce.XCart.Core/Models/ConfigurableProductOption.cs create mode 100644 src/VirtoCommerce.XCart.Core/Models/ExpConfigurationSection.cs create mode 100644 src/VirtoCommerce.XCart.Core/Models/ICartProductContainerRequest.cs create mode 100644 src/VirtoCommerce.XCart.Core/Models/ProductConfigurationSection.cs create mode 100644 src/VirtoCommerce.XCart.Core/Queries/GetProductConfigurationQuery.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/CartConfigurationItemType.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/ConfigurableProductOptionInput.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/ConfigurationLineItemType.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/ConfigurationQueryResponseType.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionInput.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionType.cs create mode 100644 src/VirtoCommerce.XCart.Core/Schemas/InputCreateConfiguredLineItemCommand.cs create mode 100644 src/VirtoCommerce.XCart.Core/Services/ICartProductsLoaderService.cs create mode 100644 src/VirtoCommerce.XCart.Core/Services/IConfiguredLineItemContainerService.cs create mode 100644 src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemBuilder.cs create mode 100644 src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemHandler.cs create mode 100644 src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryBulder.cs create mode 100644 src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryHandler.cs create mode 100644 src/VirtoCommerce.XCart.Data/Services/IConfiguredLineItemContainerService.cs diff --git a/src/VirtoCommerce.XCart.Core/CartAggregate.cs b/src/VirtoCommerce.XCart.Core/CartAggregate.cs index a54c3c0..b1636e5 100644 --- a/src/VirtoCommerce.XCart.Core/CartAggregate.cs +++ b/src/VirtoCommerce.XCart.Core/CartAggregate.cs @@ -167,6 +167,42 @@ public virtual Task UpdateCartComment(string comment) return Task.FromResult(this); } + /// + /// Always add a new line item for a configured item. + /// + /// + /// + /// + public virtual async Task AddConfiguredItemAsync(NewCartItem newCartItem, LineItem newConfiguredItem) + { + ArgumentNullException.ThrowIfNull(newCartItem); + ArgumentNullException.ThrowIfNull(newConfiguredItem); + + EnsureCartExists(); + + if (newCartItem.CartProduct != null) + { + CartProducts[newCartItem.CartProduct.Id] = newCartItem.CartProduct; + + newConfiguredItem.Id = null; + newConfiguredItem.SelectedForCheckout = IsSelectedForCheckout; + newConfiguredItem.Quantity = newCartItem.Quantity; + newConfiguredItem.Note = newCartItem.Comment; + + Cart.Items.Add(newConfiguredItem); + + if (newCartItem.DynamicProperties != null) + { + await UpdateCartItemDynamicProperties(newConfiguredItem, newCartItem.DynamicProperties); + } + + await SetItemFulfillmentCenterAsync(newConfiguredItem, newCartItem.CartProduct); + await UpdateVendor(newConfiguredItem, newCartItem.CartProduct); + } + + return this; + } + public virtual async Task AddItemAsync(NewCartItem newCartItem) { EnsureCartExists(); @@ -956,7 +992,7 @@ protected virtual Task InnerChangeItemQuantityAsync(LineItem line { ArgumentNullException.ThrowIfNull(lineItem); - if (!lineItem.IsReadOnly && product != null) + if (!lineItem.IsReadOnly && product?.Price != null) { var tierPrice = product.Price.GetTierPrice(quantity); if (CheckPricePolicy(tierPrice)) diff --git a/src/VirtoCommerce.XCart.Core/Commands/AddCartItemCommand.cs b/src/VirtoCommerce.XCart.Core/Commands/AddCartItemCommand.cs index 1198cdd..911cfa9 100644 --- a/src/VirtoCommerce.XCart.Core/Commands/AddCartItemCommand.cs +++ b/src/VirtoCommerce.XCart.Core/Commands/AddCartItemCommand.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using VirtoCommerce.Xapi.Core.Models; using VirtoCommerce.XCart.Core.Commands.BaseCommands; +using VirtoCommerce.XCart.Core.Models; namespace VirtoCommerce.XCart.Core.Commands { @@ -23,5 +24,10 @@ public class AddCartItemCommand : CartCommand /// Dynamic properties /// public IList DynamicProperties { get; set; } + + /// + /// Configurable product sections + /// + public IList ConfigurationSections { get; set; } } } diff --git a/src/VirtoCommerce.XCart.Core/Commands/CreateConfiguredLineItemCommand.cs b/src/VirtoCommerce.XCart.Core/Commands/CreateConfiguredLineItemCommand.cs new file mode 100644 index 0000000..174e579 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Commands/CreateConfiguredLineItemCommand.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Commands; + +public class CreateConfiguredLineItemCommand : ICommand, ICartProductContainerRequest +{ + public string StoreId { get; set; } + + public string UserId { get; set; } + + public string OrganizationId { get; set; } + + public string CurrencyCode { get; set; } + + public string CultureName { get; set; } + + public string ConfigurableProductId { get; set; } + + public IList ConfigurationSections { get; set; } = []; +} diff --git a/src/VirtoCommerce.XCart.Core/ConfiguredLineItemContainer.cs b/src/VirtoCommerce.XCart.Core/ConfiguredLineItemContainer.cs new file mode 100644 index 0000000..c7150e3 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/ConfiguredLineItemContainer.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.CartModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.CustomerModule.Core.Model; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core +{ + public class ConfiguredLineItemContainer : ICloneable + { + public Currency Currency { get; set; } + public Store Store { get; set; } + public Member Member { get; set; } + public string CultureName { get; set; } + public string UserId { get; set; } + public IList ProductsIncludeFields { get; set; } + + public CartProduct ConfigurableProduct { get; set; } + public IList Items { get; set; } = new List(); + + public LineItem AddItem(CartProduct cartProduct, int quantity) + { + var lineItem = AbstractTypeFactory.TryCreateInstance(); + lineItem.ProductId = cartProduct.Id; + lineItem.Name = cartProduct.Product.Name; + lineItem.Sku = cartProduct.Product.Code; + lineItem.ImageUrl = cartProduct.Product.ImgSrc; + lineItem.CatalogId = cartProduct.Product.CatalogId; + lineItem.CategoryId = cartProduct.Product.CategoryId; + + lineItem.Quantity = quantity; + + // calculate prices and only static rewards + if (cartProduct.Price != null) + { + lineItem.Currency = cartProduct.Price.Currency.Code; + + var tierPrice = cartProduct.Price.GetTierPrice(quantity); + if (tierPrice.Price.Amount > 0) + { + lineItem.SalePrice = tierPrice.ActualPrice.Amount; + lineItem.ListPrice = tierPrice.Price.Amount; + } + + lineItem.DiscountAmount = Math.Max(0, lineItem.ListPrice - lineItem.SalePrice); + lineItem.PlacedPrice = lineItem.ListPrice - lineItem.DiscountAmount; + lineItem.ExtendedPrice = lineItem.PlacedPrice * lineItem.Quantity; + } + + Items.Add(lineItem); + + return lineItem; + } + + public ExpConfigurationLineItem CreateConfiguredLineItem() + { + var lineItem = AbstractTypeFactory.TryCreateInstance(); + + lineItem.IsConfigured = true; + lineItem.Quantity = 1; + + lineItem.Discounts = []; + lineItem.TaxDetails = []; + + lineItem.ProductId = ConfigurableProduct.Product.Id; + lineItem.Sku = $"Configuration-{ConfigurableProduct.Product.Code}"; + + lineItem.CatalogId = ConfigurableProduct.Product.CatalogId; + lineItem.CategoryId = ConfigurableProduct.Product.CategoryId; + + lineItem.Name = ConfigurableProduct.Product.Name; + lineItem.ImageUrl = ConfigurableProduct.Product.ImgSrc; + lineItem.ProductOuterId = ConfigurableProduct.Product.OuterId; + lineItem.ProductType = ConfigurableProduct.Product.ProductType; + lineItem.TaxType = ConfigurableProduct.Product.TaxType; + + lineItem.FulfillmentCenterId = ConfigurableProduct.Inventory?.FulfillmentCenterId; + lineItem.FulfillmentCenterName = ConfigurableProduct.Inventory?.FulfillmentCenterName; + lineItem.VendorId = ConfigurableProduct.Product.Vendor; + + // create sub items + lineItem.ConfigurationItems = Items + .Select(x => + { + var subItem = AbstractTypeFactory.TryCreateInstance(); + + subItem.ProductId = x.ProductId; + subItem.Name = x.Name; + subItem.Sku = x.Sku; + subItem.ImageUrl = x.ImageUrl; + subItem.Quantity = x.Quantity; + subItem.CatalogId = x.CatalogId; + subItem.CategoryId = x.CategoryId; + + return subItem; + }) + .ToList(); + + // prices + lineItem.Currency = Currency.Code; + + UpdatePrice(lineItem); + + return new ExpConfigurationLineItem + { + Item = lineItem, + Currency = Currency, + CultureName = CultureName, + UserId = UserId, + StoreId = Store.Id, + }; + } + + public void UpdatePrice(LineItem lineItem) + { + var configurableProductPrice = ConfigurableProduct.Price ?? new Xapi.Core.Models.ProductPrice(Currency); + + lineItem.ListPrice = Items.Sum(x => x.ListPrice * x.Quantity) + configurableProductPrice.ListPrice.Amount; + lineItem.SalePrice = Items.Sum(x => x.SalePrice * x.Quantity) + configurableProductPrice.SalePrice.Amount; + + lineItem.DiscountAmount = Math.Max(0, lineItem.ListPrice - lineItem.SalePrice); + lineItem.PlacedPrice = lineItem.ListPrice - lineItem.DiscountAmount; + lineItem.ExtendedPrice = lineItem.PlacedPrice * lineItem.Quantity; + } + + public CartProductsRequest GetCartProductsRequest() + { + var request = AbstractTypeFactory.TryCreateInstance(); + + request.Store = Store; + request.Currency = Currency; + request.CultureName = CultureName; + request.Member = Member; + request.UserId = UserId; + + return request; + } + + public object Clone() + { + return MemberwiseClone(); + } + } +} diff --git a/src/VirtoCommerce.XCart.Core/ExpConfigurationLineItem.cs b/src/VirtoCommerce.XCart.Core/ExpConfigurationLineItem.cs new file mode 100644 index 0000000..b8ee5ec --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/ExpConfigurationLineItem.cs @@ -0,0 +1,15 @@ +using VirtoCommerce.CartModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Currency; + +namespace VirtoCommerce.XCart.Core +{ + public class ExpConfigurationLineItem + { + public string StoreId { get; set; } + public string UserId { get; set; } + public string CultureName { get; set; } + + public LineItem Item { get; set; } + public Currency Currency { get; set; } + } +} diff --git a/src/VirtoCommerce.XCart.Core/Models/CartProductsRequest.cs b/src/VirtoCommerce.XCart.Core/Models/CartProductsRequest.cs new file mode 100644 index 0000000..ae3d355 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Models/CartProductsRequest.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.CustomerModule.Core.Model; +using VirtoCommerce.StoreModule.Core.Model; + +namespace VirtoCommerce.XCart.Core.Models; + +public class CartProductsRequest +{ + public Store Store { get; set; } + public string CultureName { get; set; } + public Currency Currency { get; set; } + public Member Member { get; set; } + public string UserId { get; set; } + + public IList ProductIds { get; set; } + public IList ProductsIncludeFields { get; set; } + + public bool LoadPrice { get; set; } = true; + public bool LoadInventory { get; set; } = true; +} diff --git a/src/VirtoCommerce.XCart.Core/Models/ConfigurableProductOption.cs b/src/VirtoCommerce.XCart.Core/Models/ConfigurableProductOption.cs new file mode 100644 index 0000000..707c49d --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Models/ConfigurableProductOption.cs @@ -0,0 +1,8 @@ +namespace VirtoCommerce.XCart.Core.Models; + +public class ConfigurableProductOption +{ + public string ProductId { get; set; } + + public int Quantity { get; set; } = 1; +} diff --git a/src/VirtoCommerce.XCart.Core/Models/ExpConfigurationSection.cs b/src/VirtoCommerce.XCart.Core/Models/ExpConfigurationSection.cs new file mode 100644 index 0000000..4482788 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Models/ExpConfigurationSection.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.XCart.Core.Models; + +public class ExpProductConfigurationSection +{ + public string Id { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string Description { get; set; } + public bool IsRequired { get; set; } + + public IList Options { get; set; } = new List(); +} + +public class ProductConfigurationQueryResponse +{ + public IList ConfigurationSections { get; set; } = new List(); +} diff --git a/src/VirtoCommerce.XCart.Core/Models/ICartProductContainerRequest.cs b/src/VirtoCommerce.XCart.Core/Models/ICartProductContainerRequest.cs new file mode 100644 index 0000000..714b2cb --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Models/ICartProductContainerRequest.cs @@ -0,0 +1,10 @@ +namespace VirtoCommerce.XCart.Core.Models; + +public interface ICartProductContainerRequest +{ + string StoreId { get; set; } + string UserId { get; set; } + string OrganizationId { get; set; } + string CurrencyCode { get; set; } + string CultureName { get; set; } +} diff --git a/src/VirtoCommerce.XCart.Core/Models/ProductConfigurationSection.cs b/src/VirtoCommerce.XCart.Core/Models/ProductConfigurationSection.cs new file mode 100644 index 0000000..163cd18 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Models/ProductConfigurationSection.cs @@ -0,0 +1,8 @@ +namespace VirtoCommerce.XCart.Core.Models; + +public class ProductConfigurationSection +{ + public string SectionId { get; set; } + + public ConfigurableProductOption Value { get; set; } +} diff --git a/src/VirtoCommerce.XCart.Core/Queries/GetProductConfigurationQuery.cs b/src/VirtoCommerce.XCart.Core/Queries/GetProductConfigurationQuery.cs new file mode 100644 index 0000000..e76f313 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Queries/GetProductConfigurationQuery.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.Xapi.Core.BaseQueries; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Queries; + +public class GetProductConfigurationQuery : Query, ICartProductContainerRequest +{ + public string ConfigurableProductId { get; set; } + + public string StoreId { get; set; } + + public string UserId { get; set; } + + public string OrganizationId { get; set; } + + public string CurrencyCode { get; set; } + + public string CultureName { get; set; } + + public Store Store { get; set; } + + public IList IncludeFields { get; set; } = Array.Empty(); + + public override IEnumerable GetArguments() + { + yield return Argument>(nameof(ConfigurableProductId)); + + yield return Argument>(nameof(StoreId), description: "Store Id"); + yield return Argument(nameof(UserId), description: "User Id"); + yield return Argument(nameof(CultureName), description: "Currency code (\"USD\")"); + yield return Argument(nameof(CurrencyCode), description: "Culture name (\"en-US\")"); + } + + public override void Map(IResolveFieldContext context) + { + ConfigurableProductId = context.GetArgument(nameof(ConfigurableProductId)); + + StoreId = context.GetArgument(nameof(StoreId)); + UserId = context.GetArgument(nameof(UserId)) ?? context.GetCurrentUserId(); + OrganizationId = context.GetCurrentOrganizationId(); + CultureName = context.GetArgument(nameof(CultureName)); + CurrencyCode = context.GetArgument(nameof(CurrencyCode)); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/CartConfigurationItemType.cs b/src/VirtoCommerce.XCart.Core/Schemas/CartConfigurationItemType.cs new file mode 100644 index 0000000..17e31aa --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/CartConfigurationItemType.cs @@ -0,0 +1,14 @@ +using VirtoCommerce.CartModule.Core.Model; +using VirtoCommerce.Xapi.Core.Schemas; + +namespace VirtoCommerce.XCart.Core.Schemas +{ + public class CartConfigurationItemType : ExtendableGraphType + { + public CartConfigurationItemType() + { + Field(x => x.Id, nullable: false).Description("Configuration item ID"); + Field(x => x.Name, nullable: true).Description("Configuration item name"); + } + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/ConfigurableProductOptionInput.cs b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurableProductOptionInput.cs new file mode 100644 index 0000000..6ad348c --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurableProductOptionInput.cs @@ -0,0 +1,13 @@ +using GraphQL.Types; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Schemas; + +public class ConfigurableProductOptionInput : InputObjectGraphType +{ + public ConfigurableProductOptionInput() + { + Field>("productId"); + Field>("quantity"); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationLineItemType.cs b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationLineItemType.cs new file mode 100644 index 0000000..17bfc29 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationLineItemType.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQL.DataLoader; +using GraphQL.Resolvers; +using GraphQL.Types; +using MediatR; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Xapi.Core.Helpers; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.XCatalog.Core.Models; +using VirtoCommerce.XCatalog.Core.Queries; +using VirtoCommerce.XCatalog.Core.Schemas; + +namespace VirtoCommerce.XCart.Core.Schemas +{ + public class ConfigurationLineItemType : ExtendableGraphType + { + public ConfigurationLineItemType( + IMediator mediator, + IDataLoaderContextAccessor dataLoader, + ICurrencyService currencyService) + { + Field(x => x.Item.Id, nullable: true).Description("Item id"); + Field(x => x.Item.Quantity, nullable: true).Description("Quantity"); + + var productField = new FieldType + { + Name = "product", + Type = GraphTypeExtenstionHelper.GetActualType(), + Resolver = new FuncFieldResolver>(context => + { + var includeFields = context.SubFields.Values.GetAllNodesPaths(context).ToArray(); + var loader = dataLoader.Context.GetOrAddBatchLoader("configurationLineItems_products", async (ids) => + { + var userId = context.GetArgumentOrValue("userId") ?? context.Source.UserId; + var cultureName = context.GetArgumentOrValue("cultureName") ?? context.Source.CultureName; + var storeId = context.Source.StoreId; + var currencyCode = context.Source.Currency.Code; + + var request = new LoadProductsQuery + { + StoreId = storeId, + CurrencyCode = currencyCode, + UserId = userId, + ObjectIds = ids.ToArray(), + IncludeFields = includeFields.ToArray(), + }; + + var allCurrencies = await currencyService.GetAllCurrenciesAsync(); + context.SetCurrencies(allCurrencies, cultureName); + context.UserContext.TryAdd("currencyCode", currencyCode); + context.UserContext.TryAdd("storeId", storeId); + context.UserContext.TryAdd("cultureName", cultureName); + + var response = await mediator.Send(request); + + return response.Products.ToDictionary(x => x.Id); + }); + return loader.LoadAsync(context.Source.Item.ProductId); + }) + }; + AddField(productField); + + Field>("currency", + "Currency", + resolve: context => context.Source.Currency); + + Field>("listPrice", + "List price", + resolve: context => context.Source.Item.ListPrice.ToMoney(context.Source.Currency)); + + Field>("extendedPrice", + "Extended price", + resolve: context => context.Source.Item.ExtendedPrice.ToMoney(context.Source.Currency)); + + Field>("salePrice", + "Sale price", + resolve: context => context.Source.Item.SalePrice.ToMoney(context.Source.Currency)); + + Field>("discountAmount", + "Total discount amount", + resolve: context => context.Source.Item.DiscountAmount.ToMoney(context.Source.Currency)); + } + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationQueryResponseType.cs b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationQueryResponseType.cs new file mode 100644 index 0000000..5154fae --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationQueryResponseType.cs @@ -0,0 +1,15 @@ +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Schemas; + +public class ConfigurationQueryResponseType : ExtendableGraphType +{ + public ConfigurationQueryResponseType() + { + Field>( + nameof(ProductConfigurationQueryResponse.ConfigurationSections), + resolve: context => context.Source.ConfigurationSections); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionInput.cs b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionInput.cs new file mode 100644 index 0000000..8b37fea --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionInput.cs @@ -0,0 +1,13 @@ +using GraphQL.Types; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Schemas; + +public class ConfigurationSectionInput : InputObjectGraphType +{ + public ConfigurationSectionInput() + { + Field>("sectionId"); + Field("value"); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionType.cs b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionType.cs new file mode 100644 index 0000000..81ae343 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/ConfigurationSectionType.cs @@ -0,0 +1,21 @@ +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Schemas; + +public class ConfigurationSectionType : ExtendableGraphType +{ + public ConfigurationSectionType() + { + Field(x => x.Id, nullable: false).Description("Configuration section id"); + Field(x => x.Name, nullable: true).Description("Configuration section name"); + Field(x => x.Type, nullable: true).Description("Configuration section type"); + Field(x => x.Description, nullable: true).Description("Configuration section description"); + Field(x => x.IsRequired, nullable: false).Description("Is configuration section required"); + + ExtendableField>( + nameof(ExpProductConfigurationSection.Options), + resolve: context => context.Source.Options); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/InputAddItemType.cs b/src/VirtoCommerce.XCart.Core/Schemas/InputAddItemType.cs index 841ea57..e4d359f 100644 --- a/src/VirtoCommerce.XCart.Core/Schemas/InputAddItemType.cs +++ b/src/VirtoCommerce.XCart.Core/Schemas/InputAddItemType.cs @@ -17,6 +17,9 @@ public InputAddItemType() "Comment"); Field>("dynamicProperties"); + + // Configurable product support + Field>("configurationSections"); } } } diff --git a/src/VirtoCommerce.XCart.Core/Schemas/InputCreateConfiguredLineItemCommand.cs b/src/VirtoCommerce.XCart.Core/Schemas/InputCreateConfiguredLineItemCommand.cs new file mode 100644 index 0000000..1bc4c6f --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/InputCreateConfiguredLineItemCommand.cs @@ -0,0 +1,16 @@ +using GraphQL.Types; +using VirtoCommerce.XCart.Core.Commands; + +namespace VirtoCommerce.XCart.Core.Schemas; + +public class InputCreateConfiguredLineItemCommand : InputObjectGraphType +{ + public InputCreateConfiguredLineItemCommand() + { + Field("storeId"); + Field("currencyCode"); + Field("cultureName"); + Field>("configurableProductId"); + Field>("configurationSections"); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs b/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs index 4d51dd4..8b2ff2f 100644 --- a/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs +++ b/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs @@ -116,10 +116,10 @@ public LineItemType( Field(x => x.FulfillmentCenterName, nullable: true).Description("Line item fulfillment center name value"); Field>>>("discounts", "Discounts", - resolve: context => context.Source.Discounts); + resolve: context => context.Source.Discounts ?? []); Field>>>("taxDetails", "Tax details", - resolve: context => context.Source.TaxDetails); + resolve: context => context.Source.TaxDetails ?? []); Field>("discountAmount", "Discount amount", resolve: context => context.Source.DiscountAmount.ToMoney(context.GetCart().Currency)); @@ -176,6 +176,12 @@ public LineItemType( }) }; AddField(vendorField); + + ExtendableField>( + "configurationItems", + "Configuration items for configurable product", + resolve: context => context.Source.ConfigurationItems ?? []); + } } } diff --git a/src/VirtoCommerce.XCart.Core/Services/ICartProductsLoaderService.cs b/src/VirtoCommerce.XCart.Core/Services/ICartProductsLoaderService.cs new file mode 100644 index 0000000..59af44b --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Services/ICartProductsLoaderService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Services +{ + public interface ICartProductsLoaderService + { + /// + /// Load products and fill their inventory data and prices based on specified + /// + /// Request (cart data to use, product ids) + /// List of + Task> GetCartProductsByIdsAsync(CartProductsRequest request); + } +} diff --git a/src/VirtoCommerce.XCart.Core/Services/IConfiguredLineItemContainerService.cs b/src/VirtoCommerce.XCart.Core/Services/IConfiguredLineItemContainerService.cs new file mode 100644 index 0000000..cc2079b --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Services/IConfiguredLineItemContainerService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using VirtoCommerce.XCart.Core.Models; + +namespace VirtoCommerce.XCart.Core.Services +{ + public interface IConfiguredLineItemContainerService + { + Task CreateContainerAsync(ICartProductContainerRequest request); + } +} diff --git a/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj b/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj index ea0dd10..05e4278 100644 --- a/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj +++ b/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/VirtoCommerce.XCart.Data/Commands/AddCartItemCommandHandler.cs b/src/VirtoCommerce.XCart.Data/Commands/AddCartItemCommandHandler.cs index a932c45..6674c08 100644 --- a/src/VirtoCommerce.XCart.Data/Commands/AddCartItemCommandHandler.cs +++ b/src/VirtoCommerce.XCart.Data/Commands/AddCartItemCommandHandler.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediatR; using VirtoCommerce.XCart.Core; using VirtoCommerce.XCart.Core.Commands; using VirtoCommerce.XCart.Core.Commands.BaseCommands; @@ -12,24 +13,51 @@ namespace VirtoCommerce.XCart.Data.Commands public class AddCartItemCommandHandler : CartCommandHandler { private readonly ICartProductService _cartProductService; + private readonly IMediator _mediator; - public AddCartItemCommandHandler(ICartAggregateRepository cartAggregateRepository, ICartProductService cartProductService) + public AddCartItemCommandHandler( + ICartAggregateRepository cartAggregateRepository, + ICartProductService cartProductService, + IMediator mediator) : base(cartAggregateRepository) { _cartProductService = cartProductService; + _mediator = mediator; } public override async Task Handle(AddCartItemCommand request, CancellationToken cancellationToken) { var cartAggregate = await GetOrCreateCartFromCommandAsync(request); var product = (await _cartProductService.GetCartProductsByIdsAsync(cartAggregate, new[] { request.ProductId })).FirstOrDefault(); - await cartAggregate.AddItemAsync(new NewCartItem(request.ProductId, request.Quantity) + + var newItem = new NewCartItem(request.ProductId, request.Quantity) { Comment = request.Comment, DynamicProperties = request.DynamicProperties, Price = request.Price, - CartProduct = product - }); + CartProduct = product, + }; + + if (product?.Product?.IsConfigurable == true) + { + var createConfigurableProductCommand = new CreateConfiguredLineItemCommand + { + StoreId = request.StoreId, + UserId = request.UserId, + OrganizationId = request.OrganizationId, + CultureName = request.CultureName, + CurrencyCode = request.CurrencyCode, + ConfigurableProductId = request.ProductId, + ConfigurationSections = request.ConfigurationSections, + }; + + var mediatorResult = await _mediator.Send(createConfigurableProductCommand, cancellationToken); + await cartAggregate.AddConfiguredItemAsync(newItem, mediatorResult.Item); + } + else + { + await cartAggregate.AddItemAsync(newItem); + } return await SaveCartAsync(cartAggregate); } diff --git a/src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemBuilder.cs b/src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemBuilder.cs new file mode 100644 index 0000000..7ad3671 --- /dev/null +++ b/src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemBuilder.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using GraphQL; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using VirtoCommerce.Xapi.Core.BaseQueries; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.XCart.Core; +using VirtoCommerce.XCart.Core.Commands; +using VirtoCommerce.XCart.Core.Schemas; + +namespace VirtoCommerce.XCart.Data.Commands; + +public class CreateConfiguredLineItemBuilder : CommandBuilder +{ + public CreateConfiguredLineItemBuilder(IMediator mediator, IAuthorizationService authorizationService) + : base(mediator, authorizationService) + { + } + + protected override string Name => "createConfiguredLineItem"; + + protected override Task BeforeMediatorSend(IResolveFieldContext context, CreateConfiguredLineItemCommand request) + { + request.UserId = context.GetCurrentUserId(); + request.OrganizationId = context.GetCurrentOrganizationId(); + + return base.BeforeMediatorSend(context, request); + } +} diff --git a/src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemHandler.cs b/src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemHandler.cs new file mode 100644 index 0000000..74ba3d6 --- /dev/null +++ b/src/VirtoCommerce.XCart.Data/Commands/CreateConfiguredLineItemHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using VirtoCommerce.XCart.Core; +using VirtoCommerce.XCart.Core.Commands; +using VirtoCommerce.XCart.Core.Services; + +namespace VirtoCommerce.XCart.Data.Commands; + +public class CreateConfiguredLineItemHandler : IRequestHandler +{ + private readonly IConfiguredLineItemContainerService _configuredLineItemContainerService; + private readonly ICartProductsLoaderService _cartProductService; + + public CreateConfiguredLineItemHandler( + IConfiguredLineItemContainerService configuredLineItemContainerService, + ICartProductsLoaderService cartProductService) + { + _configuredLineItemContainerService = configuredLineItemContainerService; + _cartProductService = cartProductService; + } + + public async Task Handle(CreateConfiguredLineItemCommand request, CancellationToken cancellationToken) + { + var container = await _configuredLineItemContainerService.CreateContainerAsync(request); + + var productsRequest = container.GetCartProductsRequest(); + productsRequest.ProductIds = new[] { request.ConfigurableProductId }; + + var product = (await _cartProductService.GetCartProductsByIdsAsync(productsRequest)).FirstOrDefault(); + if (product == null) + { + throw new OperationCanceledException($"Product with id {request.ConfigurableProductId} not found"); + } + + container.ConfigurableProduct = product; + + // need to take productId and quantity from the configuration + var selectedProductIds = request.ConfigurationSections + .Where(x => x.Value != null) + .Select(section => section.Value.ProductId) + .ToList(); + + productsRequest.ProductIds = selectedProductIds; + productsRequest.LoadInventory = false; + var products = await _cartProductService.GetCartProductsByIdsAsync(productsRequest); + + foreach (var productOption in request.ConfigurationSections.Select(x => x.Value)) + { + var selectedProduct = products.FirstOrDefault(x => x.Product.Id == productOption.ProductId); + if (selectedProduct == null) + { + throw new OperationCanceledException($"Product with id {productOption.ProductId} not found"); + } + + _ = container.AddItem(selectedProduct, productOption.Quantity); + } + + var configuredItem = container.CreateConfiguredLineItem(); + + return configuredItem; + } +} diff --git a/src/VirtoCommerce.XCart.Data/Extensions/ServiceCollectionExtensions.cs b/src/VirtoCommerce.XCart.Data/Extensions/ServiceCollectionExtensions.cs index ce9f752..e688a59 100644 --- a/src/VirtoCommerce.XCart.Data/Extensions/ServiceCollectionExtensions.cs +++ b/src/VirtoCommerce.XCart.Data/Extensions/ServiceCollectionExtensions.cs @@ -29,9 +29,11 @@ public static IServiceCollection AddXCart(this IServiceCollection services, IGra services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddTransient(); services.AddTransient>(provider => () => provider.CreateScope().ServiceProvider.GetRequiredService()); + services.AddTransient(); services.AddPipeline(builder => { diff --git a/src/VirtoCommerce.XCart.Data/Mapping/CartMappingProfile.cs b/src/VirtoCommerce.XCart.Data/Mapping/CartMappingProfile.cs index 9efb383..3f7b88d 100644 --- a/src/VirtoCommerce.XCart.Data/Mapping/CartMappingProfile.cs +++ b/src/VirtoCommerce.XCart.Data/Mapping/CartMappingProfile.cs @@ -151,6 +151,38 @@ public CartMappingProfile() return priceEvalContext; }); + CreateMap().ConvertUsing((cartAggr, priceEvalContext, context) => + { + priceEvalContext = AbstractTypeFactory.TryCreateInstance(); + priceEvalContext.Language = cartAggr.CultureName; + priceEvalContext.StoreId = cartAggr.Store.Id; + priceEvalContext.CatalogId = cartAggr.Store.Catalog; + priceEvalContext.Currency = cartAggr.Currency.Code; + + var contact = cartAggr.Member; + if (contact != null) + { + priceEvalContext.CustomerId = contact.Id; + + var address = contact.Addresses.FirstOrDefault(x => x.AddressType == CoreModule.Core.Common.AddressType.Shipping) + ?? contact.Addresses.FirstOrDefault(x => x.AddressType == CoreModule.Core.Common.AddressType.Billing); + + if (address != null) + { + priceEvalContext.GeoCity = address.City; + priceEvalContext.GeoCountry = address.CountryCode; + priceEvalContext.GeoState = address.RegionName; + priceEvalContext.GeoZipCode = address.PostalCode; + } + if (contact.Groups != null) + { + priceEvalContext.UserGroups = contact.Groups.ToArray(); + } + } + + return priceEvalContext; + }); + CreateMap() .ConvertUsing((lineItem, productPromoEntry, context) => { diff --git a/src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryBulder.cs b/src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryBulder.cs new file mode 100644 index 0000000..51514d8 --- /dev/null +++ b/src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryBulder.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.StoreModule.Core.Services; +using VirtoCommerce.Xapi.Core.BaseQueries; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.XCart.Core.Models; +using VirtoCommerce.XCart.Core.Queries; +using VirtoCommerce.XCart.Core.Schemas; + +namespace VirtoCommerce.XCatalog.Data.Queries; + +public class GetProductConfigurationQueryBuilder : QueryBuilder +{ + protected override string Name => "productConfiguration"; + + private readonly IStoreService _storeService; + private readonly ICurrencyService _currencyService; + + public GetProductConfigurationQueryBuilder( + IMediator mediator, + IAuthorizationService authorizationService, + IStoreService storeService, + ICurrencyService currencyService) + : base(mediator, authorizationService) + { + _storeService = storeService; + _currencyService = currencyService; + } + + protected override async Task BeforeMediatorSend(IResolveFieldContext context, GetProductConfigurationQuery request) + { + await base.BeforeMediatorSend(context, request); + + request.IncludeFields = context.SubFields?.Values.GetAllNodesPaths(context).ToArray() ?? []; + + if (!string.IsNullOrEmpty(request.StoreId)) + { + var store = await _storeService.GetByIdAsync(request.StoreId); + request.Store = store; + context.UserContext["store"] = store; + context.UserContext["catalog"] = store.Catalog; + } + + context.CopyArgumentsToUserContext(); + + var currencies = await _currencyService.GetAllCurrenciesAsync(); + context.SetCurrencies(currencies, request.CultureName); + } +} diff --git a/src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryHandler.cs b/src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryHandler.cs new file mode 100644 index 0000000..903ab17 --- /dev/null +++ b/src/VirtoCommerce.XCart.Data/Queries/GetProductConfigurationQueryHandler.cs @@ -0,0 +1,79 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VirtoCommerce.CatalogModule.Core.Services; +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.XCart.Core; +using VirtoCommerce.XCart.Core.Models; +using VirtoCommerce.XCart.Core.Queries; +using VirtoCommerce.XCart.Core.Services; + +namespace VirtoCommerce.XCatalog.Data.Queries; + +public class GetProductConfigurationQueryHandler : IQueryHandler +{ + private readonly IConfigurableProductService _configurableProductService; + private readonly IConfiguredLineItemContainerService _configuredLineItemContainerService; + private readonly ICartProductsLoaderService _cartProductService; + + public GetProductConfigurationQueryHandler( + IConfigurableProductService configurableProductService, + IConfiguredLineItemContainerService configuredLineItemContainerService, + ICartProductsLoaderService cartProductService) + { + _configurableProductService = configurableProductService; + _configuredLineItemContainerService = configuredLineItemContainerService; + _cartProductService = cartProductService; + } + + public async Task Handle(GetProductConfigurationQuery request, CancellationToken cancellationToken) + { + var configuration = await _configurableProductService.GetProductConfigurationAsync(request.ConfigurableProductId); + + var container = await _configuredLineItemContainerService.CreateContainerAsync(request); + + var allProductIds = configuration.Sections.SelectMany(x => x.Options.Select(x => x.ProductId)).Distinct().ToArray(); + + var productsRequest = container.GetCartProductsRequest(); + productsRequest.ProductIds = allProductIds; + var cartProducts = await _cartProductService.GetCartProductsByIdsAsync(productsRequest); + + var productByIds = cartProducts.ToDictionary(x => x.Product.Id, x => x); + + var result = new ProductConfigurationQueryResponse(); + foreach (var section in configuration.Sections) + { + var configurationSection = new ExpProductConfigurationSection + { + Id = section.Id, + Name = section.Name, + IsRequired = section.IsRequired, + Description = section.Description, + Type = section.Type, + }; + result.ConfigurationSections.Add(configurationSection); + + foreach (var option in section.Options) + { + if (productByIds.TryGetValue(option.ProductId, out var cartProduct)) + { + var item = container.AddItem(cartProduct, option.Quantity); + item.Id = option.Id; + + var expConfigurationLineItem = new ExpConfigurationLineItem + { + Item = item, + Currency = container.Currency, + CultureName = container.CultureName, + UserId = container.UserId, + StoreId = container.Store.Id, + }; + + configurationSection.Options.Add(expConfigurationLineItem); + } + } + } + + return result; + } +} diff --git a/src/VirtoCommerce.XCart.Data/Services/CartAggregateRepository.cs b/src/VirtoCommerce.XCart.Data/Services/CartAggregateRepository.cs index 57d55ca..2b28dfb 100644 --- a/src/VirtoCommerce.XCart.Data/Services/CartAggregateRepository.cs +++ b/src/VirtoCommerce.XCart.Data/Services/CartAggregateRepository.cs @@ -290,14 +290,58 @@ private async Task InnerGetCartAggregateFromCartNoCacheAsync(Shop aggregate.ValidationWarnings.AddRange(result.Errors); } - // update price - aggregate.SetLineItemTierPrice(cartProduct.Price, lineItem.Quantity, lineItem); + // update price for non-configured line items immediately + if (!lineItem.IsConfigured) + { + aggregate.SetLineItemTierPrice(cartProduct.Price, lineItem.Quantity, lineItem); + } } + await UpdateConfiguredLineItemPrice(aggregate); + await aggregate.RecalculateAsync(); return aggregate; } } + + private async Task UpdateConfiguredLineItemPrice(CartAggregate aggregate) + { + var configurationLineItems = aggregate.LineItems.Where(x => x.IsConfigured).ToArray(); + + var configProductsIds = configurationLineItems + .Where(x => !x.ConfigurationItems.IsNullOrEmpty()) + .SelectMany(x => x.ConfigurationItems.Select(x => x.ProductId)) + .Distinct() + .ToArray(); + + if (configProductsIds.Length == 0) + { + return; + } + + var configProducts = await _cartProductsService.GetCartProductsByIdsAsync(aggregate, configProductsIds.ToArray()); + + foreach (var configurationLineItem in configurationLineItems) + { + var contaner = AbstractTypeFactory.TryCreateInstance(); + + if (aggregate.CartProducts.TryGetValue(configurationLineItem.ProductId, out var configurableProduct)) + { + contaner.ConfigurableProduct = configurableProduct; + } + + foreach (var configurationItem in configurationLineItem.ConfigurationItems ?? []) + { + var product = configProducts.FirstOrDefault(x => x.Product.Id == configurationItem.ProductId); + if (product != null) + { + contaner.AddItem(product, configurationItem.Quantity); + } + } + + contaner.UpdatePrice(configurationLineItem); + } + } } } diff --git a/src/VirtoCommerce.XCart.Data/Services/CartProductService.cs b/src/VirtoCommerce.XCart.Data/Services/CartProductService.cs index 4e217bd..3a0e698 100644 --- a/src/VirtoCommerce.XCart.Data/Services/CartProductService.cs +++ b/src/VirtoCommerce.XCart.Data/Services/CartProductService.cs @@ -18,7 +18,7 @@ namespace VirtoCommerce.XCart.Data.Services { - public class CartProductService : ICartProductService + public class CartProductService : ICartProductService, ICartProductsLoaderService { private readonly IItemService _productService; private readonly IInventorySearchService _inventorySearchService; @@ -86,6 +86,29 @@ public async Task> GetCartProductsByIdsAsync(CartAggregate ag return cartProducts; } + /// + /// Load s with all dependencies + /// + /// Request + /// List of s + public async Task> GetCartProductsByIdsAsync(CartProductsRequest request) + { + if (request is null || request.ProductIds.IsNullOrEmpty()) + { + return new List(); + } + + var cartProducts = await GetCartProductsAsync(request.ProductIds, request.Store.Id, request.Currency.Code, request.UserId, request.ProductsIncludeFields ?? IncludeFields); + + var productsToLoadDependencies = cartProducts.Where(x => x.LoadDependencies).ToList(); + if (productsToLoadDependencies.Count != 0) + { + await Task.WhenAll(LoadDependencies(request, productsToLoadDependencies)); + } + + return cartProducts; + } + /// /// Load s /// @@ -130,6 +153,29 @@ protected virtual async Task> GetCartProductsAsync(IList + /// Load all properties for s + /// + /// Request + /// List of s + /// List of s + protected virtual List LoadDependencies(CartProductsRequest request, List products) + { + var result = new List(); + + if (request.LoadInventory) + { + result.Add(ApplyInventoriesToCartProductAsync(request, products)); + } + + if (request.LoadPrice) + { + result.Add(ApplyPricesToCartProductAsync(request, products)); + } + + return result; + } + /// /// Load inventories and apply them to s /// @@ -174,6 +220,50 @@ protected virtual async Task ApplyInventoriesToCartProductAsync(CartAggregate ag } } + /// + /// Load inventories and apply them to s + /// + /// Request + /// List of s + protected virtual async Task ApplyInventoriesToCartProductAsync(CartProductsRequest request, List products) + { + if (products.IsNullOrEmpty()) + { + return; + } + + var ids = products.Select(x => x.Id).ToArray(); + + var countResult = await _inventorySearchService.SearchInventoriesAsync(new InventorySearchCriteria + { + ProductIds = ids, + Skip = 0, + Take = DefaultPageSize + }); + + var allLoadInventories = countResult.Results.ToList(); + + if (countResult.TotalCount > DefaultPageSize) + { + for (var i = DefaultPageSize; i < countResult.TotalCount; i += DefaultPageSize) + { + var loadInventoriesTask = await _inventorySearchService.SearchInventoriesAsync(new InventorySearchCriteria + { + ProductIds = ids, + Skip = i, + Take = DefaultPageSize + }); + + allLoadInventories.AddRange(loadInventoriesTask.Results); + } + } + + foreach (var cartProduct in products) + { + cartProduct.ApplyInventories(allLoadInventories, request.Store); + } + } + /// /// Evaluate prices and apply them to s /// @@ -203,5 +293,35 @@ protected virtual async Task ApplyPricesToCartProductAsync(CartAggregate aggrega cartProduct.ApplyPrices(evalPricesTask, aggregate.Currency); } } + + /// + /// Evaluate prices and apply them to s + /// + /// Request + /// List of s + protected virtual async Task ApplyPricesToCartProductAsync(CartProductsRequest request, List products) + { + if (request is null || products.IsNullOrEmpty()) + { + return; + } + + var pricesEvalContext = _mapper.Map(request); + pricesEvalContext.ProductIds = products.Select(x => x.Id).ToArray(); + + // There was a call to pipeline execution and stack overflow comes as a result of infinite cart getting, + // because the LoadCartToEvalContextMiddleware catches pipeline execution. + // Replaced to direct mapping. + _mapper.Map(request, pricesEvalContext); + + await _loadUserToEvalContextService.SetShopperDataFromMember(pricesEvalContext, pricesEvalContext.CustomerId); + + var evalPricesTask = await _pricingEvaluatorService.EvaluateProductPricesAsync(pricesEvalContext); + + foreach (var cartProduct in products) + { + cartProduct.ApplyPrices(evalPricesTask, request.Currency); + } + } } } diff --git a/src/VirtoCommerce.XCart.Data/Services/IConfiguredLineItemContainerService.cs b/src/VirtoCommerce.XCart.Data/Services/IConfiguredLineItemContainerService.cs new file mode 100644 index 0000000..ce62966 --- /dev/null +++ b/src/VirtoCommerce.XCart.Data/Services/IConfiguredLineItemContainerService.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.CustomerModule.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.StoreModule.Core.Services; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.XCart.Core; +using VirtoCommerce.XCart.Core.Models; +using VirtoCommerce.XCart.Core.Services; + +namespace VirtoCommerce.XCart.Data.Services; +public class ConfiguredLineItemContainerService : IConfiguredLineItemContainerService +{ + private readonly ICurrencyService _currencyService; + private readonly IMemberResolver _memberResolver; + private readonly IStoreService _storeService; + + public ConfiguredLineItemContainerService( + ICurrencyService currencyService, + IMemberResolver memberResolver, + IStoreService storeService) + { + _currencyService = currencyService; + _memberResolver = memberResolver; + _storeService = storeService; + } + + public async Task CreateContainerAsync(ICartProductContainerRequest request) + { + var storeLoadTask = _storeService.GetByIdAsync(request.StoreId); + var allCurrenciesLoadTask = _currencyService.GetAllCurrenciesAsync(); + await Task.WhenAll(storeLoadTask, allCurrenciesLoadTask); + + var store = storeLoadTask.Result; + var allCurrencies = allCurrenciesLoadTask.Result; + + if (store == null) + { + throw new OperationCanceledException($"Store with id {request.StoreId} not found"); + } + + var language = !string.IsNullOrEmpty(request.CultureName) ? request.CultureName : store.DefaultLanguage; + var currencyCode = !string.IsNullOrEmpty(request.CurrencyCode) ? request.CurrencyCode : store.DefaultCurrency; + var currency = allCurrencies.GetCurrencyForLanguage(currencyCode, language); + + var member = await _memberResolver.ResolveMemberByIdAsync(request.UserId); + + var container = AbstractTypeFactory.TryCreateInstance(); + + container.Store = store; + container.Member = member; + container.Currency = currency; + container.CultureName = language; + container.UserId = request.UserId; + + return container; + } +} diff --git a/src/VirtoCommerce.XCart.Data/VirtoCommerce.XCart.Data.csproj b/src/VirtoCommerce.XCart.Data/VirtoCommerce.XCart.Data.csproj index 1fc32c1..5df4a31 100644 --- a/src/VirtoCommerce.XCart.Data/VirtoCommerce.XCart.Data.csproj +++ b/src/VirtoCommerce.XCart.Data/VirtoCommerce.XCart.Data.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/VirtoCommerce.XCart.Web/module.manifest b/src/VirtoCommerce.XCart.Web/module.manifest index 3d2293d..f5b1833 100644 --- a/src/VirtoCommerce.XCart.Web/module.manifest +++ b/src/VirtoCommerce.XCart.Web/module.manifest @@ -4,16 +4,16 @@ 3.816.0 - 3.861.0 + 3.867.0 - + - + Cart Experience API