diff --git a/src/VirtoCommerce.XCart.Core/CartAggregate.cs b/src/VirtoCommerce.XCart.Core/CartAggregate.cs index f339b71..b782137 100644 --- a/src/VirtoCommerce.XCart.Core/CartAggregate.cs +++ b/src/VirtoCommerce.XCart.Core/CartAggregate.cs @@ -180,73 +180,84 @@ public virtual async Task AddConfiguredItemAsync(NewCartItem newC EnsureCartExists(); - if (newCartItem.CartProduct != null) + if (newCartItem.CartProduct == null) { - CartProducts[newCartItem.CartProduct.Id] = newCartItem.CartProduct; + return this; + } - newConfiguredItem.Id = null; - newConfiguredItem.SelectedForCheckout = IsSelectedForCheckout; - newConfiguredItem.Quantity = newCartItem.Quantity; - newConfiguredItem.Note = newCartItem.Comment; + CartProducts[newCartItem.CartProduct.Id] = newCartItem.CartProduct; - Cart.Items.Add(newConfiguredItem); + newConfiguredItem.Id = null; + newConfiguredItem.SelectedForCheckout = IsSelectedForCheckout; + newConfiguredItem.Quantity = newCartItem.Quantity; + newConfiguredItem.Note = newCartItem.Comment; - if (newCartItem.DynamicProperties != null) - { - await UpdateCartItemDynamicProperties(newConfiguredItem, newCartItem.DynamicProperties); - } + Cart.Items.Add(newConfiguredItem); - await SetItemFulfillmentCenterAsync(newConfiguredItem, newCartItem.CartProduct); - await UpdateVendor(newConfiguredItem, newCartItem.CartProduct); + 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(); - ArgumentNullException.ThrowIfNull(newCartItem); + EnsureCartExists(); + var validationResult = await AbstractTypeFactory.TryCreateInstance().ValidateAsync(newCartItem, options => options.IncludeRuleSets(ValidationRuleSet)); if (!validationResult.IsValid) { OperationValidationErrors.AddRange(validationResult.Errors); - } - else if (newCartItem.CartProduct != null) - { - if (newCartItem.IsWishlist && newCartItem.CartProduct.Price == null) + + if (!newCartItem.IgnoreValidationErrors) { - newCartItem.CartProduct.Price = new ProductPrice(Currency); + return this; } + } + + if (newCartItem.CartProduct == null) + { + return this; + } - var lineItem = _mapper.Map(newCartItem.CartProduct); + if (newCartItem.IsWishlist && newCartItem.CartProduct.Price == null) + { + newCartItem.CartProduct.Price = new ProductPrice(Currency); + } - lineItem.SelectedForCheckout = IsSelectedForCheckout; - lineItem.Quantity = newCartItem.Quantity; + var lineItem = _mapper.Map(newCartItem.CartProduct); - if (newCartItem.Price != null) - { - lineItem.ListPrice = newCartItem.Price.Value; - lineItem.SalePrice = newCartItem.Price.Value; - } - else - { - SetLineItemTierPrice(newCartItem.CartProduct.Price, newCartItem.Quantity, lineItem); - } + lineItem.Currency ??= Currency.Code; + lineItem.SelectedForCheckout = newCartItem.IsSelectedForCheckout ?? IsSelectedForCheckout; + lineItem.Quantity = newCartItem.Quantity; - if (!string.IsNullOrEmpty(newCartItem.Comment)) - { - lineItem.Note = newCartItem.Comment; - } + if (newCartItem.Price != null) + { + lineItem.ListPrice = newCartItem.Price.Value; + lineItem.SalePrice = newCartItem.Price.Value; + } + else + { + SetLineItemTierPrice(newCartItem.CartProduct.Price, newCartItem.Quantity, lineItem); + } - CartProducts[newCartItem.CartProduct.Id] = newCartItem.CartProduct; - await SetItemFulfillmentCenterAsync(lineItem, newCartItem.CartProduct); - await UpdateVendor(lineItem, newCartItem.CartProduct); - await InnerAddLineItemAsync(lineItem, newCartItem.CartProduct, newCartItem.DynamicProperties); + if (!string.IsNullOrEmpty(newCartItem.Comment)) + { + lineItem.Note = newCartItem.Comment; } + CartProducts[newCartItem.CartProduct.Id] = newCartItem.CartProduct; + await SetItemFulfillmentCenterAsync(lineItem, newCartItem.CartProduct); + await UpdateVendor(lineItem, newCartItem.CartProduct); + await InnerAddLineItemAsync(lineItem, newCartItem.CartProduct, newCartItem.DynamicProperties); + return this; } @@ -270,7 +281,9 @@ await AddItemAsync(new NewCartItem(item.ProductId, item.Quantity) DynamicProperties = item.DynamicProperties, Price = item.Price, IsWishlist = item.IsWishlist, + IsSelectedForCheckout = item.IsSelectedForCheckout, CartProduct = product, + IgnoreValidationErrors = item.IgnoreValidationErrors, }); } else @@ -1150,6 +1163,7 @@ public virtual async Task UpdateConfiguredLineItemPrice(IList.TryCreateInstance(); + contaner.Currency = Currency; if (CartProducts.TryGetValue(configurationLineItem.ProductId, out var configurableProduct)) { diff --git a/src/VirtoCommerce.XCart.Core/Commands/ChangeCartCurrencyCommand.cs b/src/VirtoCommerce.XCart.Core/Commands/ChangeCartCurrencyCommand.cs new file mode 100644 index 0000000..f51a877 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Commands/ChangeCartCurrencyCommand.cs @@ -0,0 +1,9 @@ +using VirtoCommerce.XCart.Core.Commands.BaseCommands; + +namespace VirtoCommerce.XCart.Core.Commands +{ + public class ChangeCartCurrencyCommand : CartCommand + { + public string NewCurrencyCode { get; set; } + } +} diff --git a/src/VirtoCommerce.XCart.Core/Models/NewCartItem.cs b/src/VirtoCommerce.XCart.Core/Models/NewCartItem.cs index f1a167a..2025082 100644 --- a/src/VirtoCommerce.XCart.Core/Models/NewCartItem.cs +++ b/src/VirtoCommerce.XCart.Core/Models/NewCartItem.cs @@ -33,5 +33,9 @@ public NewCartItem(string productId, int quantity) public IList DynamicProperties { get; set; } public bool IsWishlist { get; set; } + + public bool? IsSelectedForCheckout { get; set; } + + public bool IgnoreValidationErrors { get; set; } } } diff --git a/src/VirtoCommerce.XCart.Core/Schemas/InputChangeCartCurrencyType.cs b/src/VirtoCommerce.XCart.Core/Schemas/InputChangeCartCurrencyType.cs new file mode 100644 index 0000000..c042fd6 --- /dev/null +++ b/src/VirtoCommerce.XCart.Core/Schemas/InputChangeCartCurrencyType.cs @@ -0,0 +1,12 @@ +using GraphQL.Types; + +namespace VirtoCommerce.XCart.Core.Schemas +{ + public class InputChangeCartCurrencyType : InputCartBaseType + { + public InputChangeCartCurrencyType() + { + Field>("newCurrencyCode", "Second cart currency"); + } + } +} diff --git a/src/VirtoCommerce.XCart.Data/Commands/ChangeCartCurrencyCommandHandler.cs b/src/VirtoCommerce.XCart.Data/Commands/ChangeCartCurrencyCommandHandler.cs new file mode 100644 index 0000000..63d930c --- /dev/null +++ b/src/VirtoCommerce.XCart.Data/Commands/ChangeCartCurrencyCommandHandler.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VirtoCommerce.CartModule.Core.Model; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Xapi.Core.Models; +using VirtoCommerce.XCart.Core; +using VirtoCommerce.XCart.Core.Commands; +using VirtoCommerce.XCart.Core.Commands.BaseCommands; +using VirtoCommerce.XCart.Core.Models; +using VirtoCommerce.XCart.Core.Services; + +namespace VirtoCommerce.XCart.Data.Commands +{ + public class ChangeCartCurrencyCommandHandler : CartCommandHandler + { + private readonly ICartProductService _cartProductService; + + public ChangeCartCurrencyCommandHandler( + ICartAggregateRepository cartAggregateRepository, + ICartProductService cartProductService) + : base(cartAggregateRepository) + { + _cartProductService = cartProductService; + } + + public override async Task Handle(ChangeCartCurrencyCommand request, CancellationToken cancellationToken) + { + // get (or create) both carts + var currentCurrencyCartAggregate = await GetOrCreateCartFromCommandAsync(request) + ?? throw new OperationCanceledException("Cart not found"); + + var newCurrencyCartRequest = new ChangeCartCurrencyCommand + { + StoreId = request.StoreId ?? currentCurrencyCartAggregate.Cart.StoreId, + CartName = request.CartName ?? currentCurrencyCartAggregate.Cart.Name, + CartType = request.CartType ?? currentCurrencyCartAggregate.Cart.Type, + UserId = request.UserId ?? currentCurrencyCartAggregate.Cart.CustomerId, + OrganizationId = request.OrganizationId ?? currentCurrencyCartAggregate.Cart.OrganizationId, + CultureName = request.CultureName ?? currentCurrencyCartAggregate.Cart.LanguageCode, + CurrencyCode = request.NewCurrencyCode, + }; + + var newCurrencyCartAggregate = await GetOrCreateCartFromCommandAsync(newCurrencyCartRequest); + + // clear (old) cart items and add items from the currency cart + newCurrencyCartAggregate.Cart.Items.Clear(); + + await CopyItems(currentCurrencyCartAggregate, newCurrencyCartAggregate); + + await CartRepository.SaveAsync(newCurrencyCartAggregate); + return newCurrencyCartAggregate; + } + + protected virtual async Task CopyItems(CartAggregate currentCurrencyCartAggregate, CartAggregate newCurrencyCartAggregate) + { + var ordinaryItems = currentCurrencyCartAggregate.LineItems + .Where(x => !x.IsConfigured) + .ToArray(); + + if (ordinaryItems.Length > 0) + { + var newCartItems = ordinaryItems + .Select(x => new NewCartItem(x.ProductId, x.Quantity) + { + IgnoreValidationErrors = true, + Comment = x.Note, + IsSelectedForCheckout = x.SelectedForCheckout, + DynamicProperties = x.DynamicProperties.SelectMany(x => x.Values.Select(y => new DynamicPropertyValue() + { + Name = x.Name, + Value = y.Value, + Locale = y.Locale, + })).ToArray(), + }) + .ToArray(); + + await newCurrencyCartAggregate.AddItemsAsync(newCartItems); + } + + // copy configured items + var configuredItems = currentCurrencyCartAggregate.LineItems + .Where(x => x.IsConfigured) + .ToArray(); + + await CopyConfiguredItems(newCurrencyCartAggregate, configuredItems); + } + + protected virtual async Task CopyConfiguredItems(CartAggregate newCurrencyCartAggregate, IList configuredItems) + { + if (configuredItems.Count == 0) + { + return; + } + + var configProductsIds = configuredItems + .Where(x => !x.ConfigurationItems.IsNullOrEmpty()) + .SelectMany(x => x.ConfigurationItems.Select(x => x.ProductId)) + .Distinct() + .ToList(); + + configProductsIds.AddRange(configuredItems.Select(x => x.ProductId)); + + var configProducts = await _cartProductService.GetCartProductsByIdsAsync(newCurrencyCartAggregate, configProductsIds); + + foreach (var configurationLineItem in configuredItems) + { + var contaner = AbstractTypeFactory.TryCreateInstance(); + contaner.Currency = newCurrencyCartAggregate.Currency; + contaner.Store = newCurrencyCartAggregate.Store; + + contaner.ConfigurableProduct = configProducts.FirstOrDefault(x => x.Product.Id == configurationLineItem.ProductId); + + foreach (var configurationItem in configurationLineItem.ConfigurationItems ?? []) + { + var product = configProducts.FirstOrDefault(x => x.Product.Id == configurationItem.ProductId); + if (product != null) + { + contaner.AddItem(product, configurationItem.Quantity, configurationItem.SectionId); + } + } + + var expItem = contaner.CreateConfiguredLineItem(configurationLineItem.Quantity); + + await newCurrencyCartAggregate.AddConfiguredItemAsync(new NewCartItem(configurationLineItem.ProductId, configurationLineItem.Quantity) + { + CartProduct = contaner.ConfigurableProduct, + IgnoreValidationErrors = true, + Comment = configurationLineItem.Note, + IsSelectedForCheckout = configurationLineItem.SelectedForCheckout, + DynamicProperties = configurationLineItem.DynamicProperties.SelectMany(x => x.Values.Select(y => new DynamicPropertyValue() + { + Name = x.Name, + Value = y.Value, + Locale = y.Locale, + })).ToArray(), + }, expItem.Item); + } + } + } +} diff --git a/src/VirtoCommerce.XCart.Data/Schemas/PurchaseSchema.cs b/src/VirtoCommerce.XCart.Data/Schemas/PurchaseSchema.cs index e8d4449..144dae8 100644 --- a/src/VirtoCommerce.XCart.Data/Schemas/PurchaseSchema.cs +++ b/src/VirtoCommerce.XCart.Data/Schemas/PurchaseSchema.cs @@ -848,6 +848,25 @@ public void Build(ISchema schema) schema.Mutation.AddField(margeCartField); + var changeCartCurrency = FieldBuilder.Create(GraphTypeExtenstionHelper.GetActualType()) + .Name("changeCartCurrency") + .Argument(GraphTypeExtenstionHelper.GetActualComplexType>(), SchemaConstants.CommandName) + .ResolveSynchronizedAsync(CartPrefix, "userId", _distributedLockService, async context => + { + var cartCommand = context.GetCartCommand(); + + await CheckAuthByCartCommandAsync(context, cartCommand); + + //We need to add cartAggregate to the context to be able use it on nested cart types resolvers (e.g for currency) + var cartAggregate = await _mediator.Send(cartCommand); + + //store cart aggregate in the user context for future usage in the graph types resolvers + context.SetExpandedObjectGraph(cartAggregate); + return cartAggregate; + }).FieldType; + + schema.Mutation.AddField(changeCartCurrency); + /// /// This is an example JSON request for a mutation /// {