diff --git a/Directory.Packages.props b/Directory.Packages.props index ffce794..e8f0c78 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,8 +15,8 @@ - - + + diff --git a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs index a366e2a..65e5b58 100644 --- a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs +++ b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs @@ -74,7 +74,7 @@ public async Task Index(UpdateCartModel updateCartModel, CartOper }; var result = cartOperation == CartOperation.Remove - ? await shoppingService.RemoveCartItem(cartItemParams.MerchandiseID) + ? await shoppingService.RemoveProductVariantFromCart(cartItemParams.MerchandiseID) : await shoppingService.AddItemToCart(cartItemParams); TempData[ERROR_MESSAGES_KEY] = result.ErrorMessages.ToArray(); diff --git a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyShoppingCartController.cs b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyShoppingCartController.cs index 2a7b281..d5edf4f 100644 --- a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyShoppingCartController.cs +++ b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyShoppingCartController.cs @@ -76,18 +76,18 @@ public async Task Index() [HttpPost] [Route("/cart/update")] [ValidateAntiForgeryToken] - public async Task Update([FromForm] string variantGraphQLId, [FromForm] int quantity, [FromForm] string cartOperation) + public async Task Update([FromForm] string cartItemId, [FromForm] int quantity, [FromForm] string cartOperation) { var country = ShopifySharp.GraphQL.CountryCode.CZ; if (Enum.TryParse(cartOperation, out var operationEnum)) { var result = operationEnum == CartOperation.Remove - ? await shoppingService.RemoveCartItem(variantGraphQLId) + ? await shoppingService.RemoveCartItem(cartItemId) : await shoppingService.UpdateCartItem(new ShoppingCartItemParameters() { Country = country, Quantity = quantity, - MerchandiseID = variantGraphQLId + ShoppingCartItemID = cartItemId }); AddErrorsToTempData(result); diff --git a/examples/DancingGoat-Shopify/Models/WebPage/Shopify/ProductDetailPage/ProductDetailViewModel.cs b/examples/DancingGoat-Shopify/Models/WebPage/Shopify/ProductDetailPage/ProductDetailViewModel.cs index 57560d9..b08825b 100644 --- a/examples/DancingGoat-Shopify/Models/WebPage/Shopify/ProductDetailPage/ProductDetailViewModel.cs +++ b/examples/DancingGoat-Shopify/Models/WebPage/Shopify/ProductDetailPage/ProductDetailViewModel.cs @@ -31,7 +31,7 @@ public record ProductDetailViewModel public static ProductDetailViewModel GetViewModel(ProductDetailPage page, string selectedVariantID, string country, string currency, string[] errorMessages) { var product = page.Product.First(); - var selectedVariant = product.Variants.FirstOrDefault(x => x.ShopifyVariantID == selectedVariantID) ?? product.Variants.First(); + var selectedVariant = product.Variants.FirstOrDefault(x => x.ShopifyVariantID.Equals(selectedVariantID, StringComparison.Ordinal)) ?? product.Variants.First(); var allImages = product.Images.Concat(product.Variants.Select(x => x.Image.FirstOrDefault())) .Where(x => x != null) @@ -48,7 +48,7 @@ public static ProductDetailViewModel GetViewModel(ProductDetailPage page, string Images = allImages, DescriptionHTML = product.Description, ParametersSection = product.Parameters, - Variants = product.Variants.Select(x => new SelectListItem(x.Title, x.ShopifyVariantID, x.ShopifyVariantID == selectedVariant.ShopifyVariantID)).ToList(), + Variants = product.Variants.Select(x => new SelectListItem(x.Title, x.ShopifyVariantID, x.ShopifyVariantID.Equals(selectedVariant.ShopifyVariantID, StringComparison.Ordinal))).ToList(), SelectedShopifyVariantId = selectedVariant.ShopifyVariantID, ShopifyProductId = product.ShopifyProductID, CountryCode = country, diff --git a/examples/DancingGoat-Shopify/Views/ShopifyShoppingCart/ShopifyCartItemView.cshtml b/examples/DancingGoat-Shopify/Views/ShopifyShoppingCart/ShopifyCartItemView.cshtml index c121eb0..d86c692 100644 --- a/examples/DancingGoat-Shopify/Views/ShopifyShoppingCart/ShopifyCartItemView.cshtml +++ b/examples/DancingGoat-Shopify/Views/ShopifyShoppingCart/ShopifyCartItemView.cshtml @@ -13,7 +13,7 @@ @Html.Kentico().PageData()
@HtmlLocalizer["Qty"] - + diff --git a/examples/DancingGoat-Shopify/appsettings.json b/examples/DancingGoat-Shopify/appsettings.json index b3e6674..67e5137 100644 --- a/examples/DancingGoat-Shopify/appsettings.json +++ b/examples/DancingGoat-Shopify/appsettings.json @@ -13,5 +13,11 @@ } }, "AllowedHosts": "*", - "CMSHashStringSalt": "9f38667d-c99f-43bb-9a92-043ce36ecb5d" + "CMSHashStringSalt": "9f38667d-c99f-43bb-9a92-043ce36ecb5d", + "CMSShopifyConfig": { + "ShopifyUrl": "https://your-shopify-store-url.com/", + "AdminApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "StorefrontApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "StorefrontApiVersion": "YYYY-MM" + } } \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/packages.lock.json b/examples/DancingGoat-Shopify/packages.lock.json index bf120b3..1210aaa 100644 --- a/examples/DancingGoat-Shopify/packages.lock.json +++ b/examples/DancingGoat-Shopify/packages.lock.json @@ -55,9 +55,9 @@ }, "ShopifySharp": { "type": "Direct", - "requested": "[6.13.0, )", - "resolved": "6.13.0", - "contentHash": "GwM1K0FzTo9BrXSqG9hWxIOGGfhrbBfLXK+b6vMqaabmw0Uv4/n9gskQKSZ9CIB7vJksI4Grn+Tk9qW/dVqBJQ==", + "requested": "[6.17.0, )", + "resolved": "6.17.0", + "contentHash": "0yvFrbdvCaQyWHOXsd5Lp15mPpJDQ0G+PE8vJVRkMiH39nrEemI1i2UYrFTS8axgNndGJzB+E1XzctwELp7uug==", "dependencies": { "Microsoft.Extensions.Http": "2.1.0", "System.Text.Json": "7.0.3", @@ -66,11 +66,11 @@ }, "ShopifySharp.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "jpaU4WpqjAXm/Tm7vQwC/gBbsdIHa4taO/aAN6BZxT8t91f9+OHkWZzFOsvChVUNn61cShcTR84tUfrv6X2cVg==", + "requested": "[1.6.0, )", + "resolved": "1.6.0", + "contentHash": "Hf0MexgLD6FbTcm81lFMPsw5kWK4XNEl7FPmYuoE9kPlWhWwR/wUWMYUpn6uSBTHCE+62yI2l+i7lsOb0zASnA==", "dependencies": { - "ShopifySharp": "6.13.0", + "ShopifySharp": "6.16.0", "microsoft.extensions.dependencyinjection": "8.0.0" } }, @@ -1015,8 +1015,8 @@ "Kentico.Xperience.Admin": "[29.0.2, )", "Kentico.Xperience.Core": "[29.0.2, )", "Kentico.Xperience.Ecommerce.Common": "[1.0.0-prerelease-1, )", - "ShopifySharp": "[6.13.0, )", - "ShopifySharp.Extensions.DependencyInjection": "[1.4.0, )", + "ShopifySharp": "[6.17.0, )", + "ShopifySharp.Extensions.DependencyInjection": "[1.6.0, )", "System.Configuration.ConfigurationManager": "[8.0.0, )", "System.Linq.Async": "[6.0.1, )" } @@ -1024,7 +1024,7 @@ "kentico.xperience.shopify.rcl": { "type": "Project", "dependencies": { - "Kentico.Xperience.Shopify": "[1.0.0-prerelease-1, )" + "Kentico.Xperience.Shopify": "[0.1.0-prerelease-1, )" } }, "GraphQL": { diff --git a/src/Kentico.Xperience.Shopify.Rcl/packages.lock.json b/src/Kentico.Xperience.Shopify.Rcl/packages.lock.json index c5002eb..e69a230 100644 --- a/src/Kentico.Xperience.Shopify.Rcl/packages.lock.json +++ b/src/Kentico.Xperience.Shopify.Rcl/packages.lock.json @@ -885,8 +885,8 @@ "Kentico.Xperience.Admin": "[29.0.2, )", "Kentico.Xperience.Core": "[29.0.2, )", "Kentico.Xperience.Ecommerce.Common": "[1.0.0-prerelease-1, )", - "ShopifySharp": "[6.13.0, )", - "ShopifySharp.Extensions.DependencyInjection": "[1.4.0, )", + "ShopifySharp": "[6.17.0, )", + "ShopifySharp.Extensions.DependencyInjection": "[1.6.0, )", "System.Configuration.ConfigurationManager": "[8.0.0, )", "System.Linq.Async": "[6.0.1, )" } @@ -974,9 +974,9 @@ }, "ShopifySharp": { "type": "CentralTransitive", - "requested": "[6.13.0, )", - "resolved": "6.13.0", - "contentHash": "GwM1K0FzTo9BrXSqG9hWxIOGGfhrbBfLXK+b6vMqaabmw0Uv4/n9gskQKSZ9CIB7vJksI4Grn+Tk9qW/dVqBJQ==", + "requested": "[6.17.0, )", + "resolved": "6.17.0", + "contentHash": "0yvFrbdvCaQyWHOXsd5Lp15mPpJDQ0G+PE8vJVRkMiH39nrEemI1i2UYrFTS8axgNndGJzB+E1XzctwELp7uug==", "dependencies": { "Microsoft.Extensions.Http": "2.1.0", "System.Text.Json": "7.0.3", @@ -985,11 +985,11 @@ }, "ShopifySharp.Extensions.DependencyInjection": { "type": "CentralTransitive", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "jpaU4WpqjAXm/Tm7vQwC/gBbsdIHa4taO/aAN6BZxT8t91f9+OHkWZzFOsvChVUNn61cShcTR84tUfrv6X2cVg==", + "requested": "[1.6.0, )", + "resolved": "1.6.0", + "contentHash": "Hf0MexgLD6FbTcm81lFMPsw5kWK4XNEl7FPmYuoE9kPlWhWwR/wUWMYUpn6uSBTHCE+62yI2l+i7lsOb0zASnA==", "dependencies": { - "ShopifySharp": "6.13.0", + "ShopifySharp": "6.16.0", "microsoft.extensions.dependencyinjection": "8.0.0" } }, diff --git a/src/Kentico.Xperience.Shopify/Orders/ShopifyOrderService.cs b/src/Kentico.Xperience.Shopify/Orders/ShopifyOrderService.cs index 76d1656..3efc843 100644 --- a/src/Kentico.Xperience.Shopify/Orders/ShopifyOrderService.cs +++ b/src/Kentico.Xperience.Shopify/Orders/ShopifyOrderService.cs @@ -37,7 +37,7 @@ public ShopifyOrderService( var result = await orderService.ListAsync(filter); - return result.Items.FirstOrDefault(x => x.SourceIdentifier == sourceId); + return result.Items.FirstOrDefault(x => x.SourceIdentifier.Equals(sourceId, StringComparison.Ordinal)); } } } diff --git a/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs b/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs index 69e9eee..664b36e 100644 --- a/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs +++ b/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs @@ -45,7 +45,7 @@ private async Task> GetProductsPriceInter continue; } - var prices = product.Variants.Select(x => x.PresentmentPrices.FirstOrDefault(x => x.Price.CurrencyCode == currency)); + var prices = product.Variants.Select(x => x.PresentmentPrices.FirstOrDefault(x => x.Price.CurrencyCode.Equals(currency, StringComparison.Ordinal))); dict.TryAdd(product.Id.Value.ToString(), new ProductPriceModel() { diff --git a/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs b/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs index c9146b1..b4cc668 100644 --- a/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs +++ b/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs @@ -90,7 +90,7 @@ private async Task> GetProductVarian { foreach (var variant in variants.Values) { - var cartItem = cart.Items.FirstOrDefault(x => x.VariantGraphQLId == variant.MerchandiseID); + var cartItem = cart.Items.FirstOrDefault(x => x.VariantGraphQLId.Equals(variant.MerchandiseID, StringComparison.Ordinal)); variant.ItemsInCart = cartItem?.Quantity ?? 0; } } @@ -163,7 +163,7 @@ private ListResultWrapper CreateResultModel(ListResult< if (variants.Count() == 1) { var onlyVariant = variants.First(); - var currencyPrice = onlyVariant.PresentmentPrices?.FirstOrDefault(x => x.Price.CurrencyCode == currency); + var currencyPrice = onlyVariant.PresentmentPrices?.FirstOrDefault(x => x.Price.CurrencyCode.Equals(currency, StringComparison.Ordinal)); return currencyPrice is { Price: not null } ? (currencyPrice.Price.Amount, currencyPrice.CompareAtPrice?.Amount) : (null, null); @@ -173,7 +173,7 @@ private ListResultWrapper CreateResultModel(ListResult< foreach (var variant in variants) { - var currencyPrice = variant.PresentmentPrices?.FirstOrDefault(x => x.Price.CurrencyCode == currency); + var currencyPrice = variant.PresentmentPrices?.FirstOrDefault(x => x.Price.CurrencyCode.Equals(currency, StringComparison.Ordinal)); if (currencyPrice?.Price.Amount != null) { diff --git a/src/Kentico.Xperience.Shopify/ShopifyConstants.cs b/src/Kentico.Xperience.Shopify/ShopifyConstants.cs index 9d9a8ce..1c0cf10 100644 --- a/src/Kentico.Xperience.Shopify/ShopifyConstants.cs +++ b/src/Kentico.Xperience.Shopify/ShopifyConstants.cs @@ -21,5 +21,11 @@ public static class ShopifyConstants /// The default name for product variants. /// public const string DEFAULT_VARIANT_NAME = "default title"; + + + /// + /// The name of the header where buyer IP address should be added. + /// + public const string STOREFRONT_API_BUYER_IP_NAME = "Shopify-Storefront-Buyer-IP"; } } diff --git a/src/Kentico.Xperience.Shopify/ShoppingCart/GraphQLHttpClientFactory.cs b/src/Kentico.Xperience.Shopify/ShoppingCart/GraphQLHttpClientFactory.cs index d730ffb..141615d 100644 --- a/src/Kentico.Xperience.Shopify/ShoppingCart/GraphQLHttpClientFactory.cs +++ b/src/Kentico.Xperience.Shopify/ShoppingCart/GraphQLHttpClientFactory.cs @@ -2,15 +2,23 @@ using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; +using Microsoft.AspNetCore.Http; + namespace Kentico.Xperience.Shopify.ShoppingCart { internal class GraphQLHttpClientFactory : IGraphQLHttpClientFactory { private readonly HttpClient httpClient; - public GraphQLHttpClientFactory(IHttpClientFactory httpClientFactory) + public GraphQLHttpClientFactory(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor) { httpClient = httpClientFactory.CreateClient(ShopifyConstants.STOREFRONT_API_CLIENT_NAME); + string? buyerIP = httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString(); + + if (!string.IsNullOrEmpty(buyerIP)) + { + httpClient.DefaultRequestHeaders.Add(ShopifyConstants.STOREFRONT_API_BUYER_IP_NAME, buyerIP); + } } /// diff --git a/src/Kentico.Xperience.Shopify/ShoppingCart/IShoppingService.cs b/src/Kentico.Xperience.Shopify/ShoppingCart/IShoppingService.cs index 589cdfc..4cf6f1b 100644 --- a/src/Kentico.Xperience.Shopify/ShoppingCart/IShoppingService.cs +++ b/src/Kentico.Xperience.Shopify/ShoppingCart/IShoppingService.cs @@ -13,12 +13,20 @@ public interface IShoppingService Task UpdateCartItem(ShoppingCartItemParameters parameters); + /// + /// Remove all shopping cart items of specific product variant. + /// + /// Product variant graphQL ID. + /// with updated shopping cart if operation was successful. + Task RemoveProductVariantFromCart(string variantGraphQLId); + + /// /// Remove shopping cart item. /// - /// Shopify product variant ID + /// Shopify shopping cart item ID /// with updated shopping cart if operation was successful. - Task RemoveCartItem(string merchandiseId); + Task RemoveCartItem(string cartItemId); /// diff --git a/src/Kentico.Xperience.Shopify/ShoppingCart/Models/ShoppingCartItemParameters.cs b/src/Kentico.Xperience.Shopify/ShoppingCart/Models/ShoppingCartItemParameters.cs index debe4b6..4e6fbf0 100644 --- a/src/Kentico.Xperience.Shopify/ShoppingCart/Models/ShoppingCartItemParameters.cs +++ b/src/Kentico.Xperience.Shopify/ShoppingCart/Models/ShoppingCartItemParameters.cs @@ -19,6 +19,13 @@ public class ShoppingCartItemParameters public string MerchandiseID { get; set; } = string.Empty; + /// + /// ID of the shopping cart item if item was already in shopping cart. + /// If the cart item is new, the value will be empty string. + /// + public string ShoppingCartItemID { get; set; } = string.Empty; + + /// /// Country code for the item. /// diff --git a/src/Kentico.Xperience.Shopify/ShoppingCart/ShoppingService.cs b/src/Kentico.Xperience.Shopify/ShoppingCart/ShoppingService.cs index 7bb3110..8b0a87d 100644 --- a/src/Kentico.Xperience.Shopify/ShoppingCart/ShoppingService.cs +++ b/src/Kentico.Xperience.Shopify/ShoppingCart/ShoppingService.cs @@ -36,11 +36,15 @@ public async Task UpdateCartItem(ShoppingCartItemParameters { var cart = await GetCurrentShoppingCart(); - var cartItemToUpdate = cart?.Items.FirstOrDefault(x => x.ShopifyCartItemId == parameters.MerchandiseID); + var cartItemToUpdate = cart?.Items.FirstOrDefault(x => x.ShopifyCartItemId.Equals(parameters.ShoppingCartItemID, StringComparison.Ordinal)); if (cart == null || cartItemToUpdate == null) { return await AddItemToCart(parameters); } + if (cartItemToUpdate.Quantity == parameters.Quantity) + { + return new CartOperationResult(cart, true); + } int quantity = Math.Max(parameters.Quantity, 0); var result = await UpdateCartItemInternal(cart.CartId, cartItemToUpdate, quantity); @@ -62,7 +66,38 @@ public async Task UpdateCartItem(ShoppingCartItemParameters } - public async Task RemoveCartItem(string merchandiseId) + public async Task RemoveProductVariantFromCart(string variantGraphQLId) + { + var cart = await GetCurrentShoppingCart(); + if (cart == null) + { + return new CartOperationResult(null, true); + } + + var shopifyCartLines = cart.Items.Where(x => x.VariantGraphQLId.Equals(variantGraphQLId, StringComparison.Ordinal)); + bool success = true; + CartOperationResult? result = null; + + foreach (var shopifyCartLine in shopifyCartLines) + { + result = await RemoveCartItem(shopifyCartLine.ShopifyCartItemId); + if (!result.Success) + { + success = false; + } + } + + // There was no cart item with given variant graphQL ID + if (result == null) + { + return new CartOperationResult(cart, success); + } + + return new CartOperationResult(result.Cart, success); + } + + + public async Task RemoveCartItem(string cartItemId) { var cart = await GetCurrentShoppingCart(); if (cart == null) @@ -70,7 +105,7 @@ public async Task RemoveCartItem(string merchandiseId) return new CartOperationResult(null, true); } - var shopifyCartLine = cart.Items.FirstOrDefault(x => x.VariantGraphQLId == merchandiseId); + var shopifyCartLine = cart.Items.FirstOrDefault(x => x.ShopifyCartItemId.Equals(cartItemId, StringComparison.Ordinal)); if (shopifyCartLine == null) { return new CartOperationResult(cart, true); @@ -166,7 +201,7 @@ public async Task AddItemToCart(ShoppingCartItemParameters StoreCartToCookiesAndSession(cart.CartId); } - var addedItem = cart.Items.FirstOrDefault(x => x.VariantGraphQLId == parameters.MerchandiseID); + var addedItem = cart.Items.FirstOrDefault(x => x.VariantGraphQLId.Equals(parameters.MerchandiseID)); activityLogger.LogProductAddedToShoppingCartActivity(addedItem, parameters.Quantity); } return result; diff --git a/src/Kentico.Xperience.Shopify/Synchronization/SynchronizationServiceBase.cs b/src/Kentico.Xperience.Shopify/Synchronization/SynchronizationServiceBase.cs index 3f06934..dade9d4 100644 --- a/src/Kentico.Xperience.Shopify/Synchronization/SynchronizationServiceBase.cs +++ b/src/Kentico.Xperience.Shopify/Synchronization/SynchronizationServiceBase.cs @@ -50,7 +50,7 @@ protected IEnumerable OrderItemsByShopify(IEnumerable co { foreach (var shopifyObject in shopifyObjects.Where(x => x.Id.HasValue)) { - var contentItem = contentItems.FirstOrDefault(x => x.ShopifyObjectID == (shopifyObject.Id?.ToString() ?? string.Empty)); + var contentItem = contentItems.FirstOrDefault(x => x.ShopifyObjectID.Equals(shopifyObject.Id?.ToString() ?? string.Empty, StringComparison.Ordinal)); yield return contentItem?.SystemFields.ContentItemGUID ?? Guid.Empty; } } diff --git a/src/Kentico.Xperience.Shopify/packages.lock.json b/src/Kentico.Xperience.Shopify/packages.lock.json index ca5310e..faca169 100644 --- a/src/Kentico.Xperience.Shopify/packages.lock.json +++ b/src/Kentico.Xperience.Shopify/packages.lock.json @@ -69,9 +69,9 @@ }, "ShopifySharp": { "type": "Direct", - "requested": "[6.13.0, )", - "resolved": "6.13.0", - "contentHash": "GwM1K0FzTo9BrXSqG9hWxIOGGfhrbBfLXK+b6vMqaabmw0Uv4/n9gskQKSZ9CIB7vJksI4Grn+Tk9qW/dVqBJQ==", + "requested": "[6.17.0, )", + "resolved": "6.17.0", + "contentHash": "0yvFrbdvCaQyWHOXsd5Lp15mPpJDQ0G+PE8vJVRkMiH39nrEemI1i2UYrFTS8axgNndGJzB+E1XzctwELp7uug==", "dependencies": { "Microsoft.Extensions.Http": "2.1.0", "System.Text.Json": "7.0.3", @@ -80,11 +80,11 @@ }, "ShopifySharp.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "jpaU4WpqjAXm/Tm7vQwC/gBbsdIHa4taO/aAN6BZxT8t91f9+OHkWZzFOsvChVUNn61cShcTR84tUfrv6X2cVg==", + "requested": "[1.6.0, )", + "resolved": "1.6.0", + "contentHash": "Hf0MexgLD6FbTcm81lFMPsw5kWK4XNEl7FPmYuoE9kPlWhWwR/wUWMYUpn6uSBTHCE+62yI2l+i7lsOb0zASnA==", "dependencies": { - "ShopifySharp": "6.13.0", + "ShopifySharp": "6.16.0", "microsoft.extensions.dependencyinjection": "8.0.0" } }, diff --git a/test/Kentico.Xperience.Shopify.Tests/Mocks/GraphQLHttpClientMock.cs b/test/Kentico.Xperience.Shopify.Tests/Mocks/GraphQLHttpClientMock.cs index 82fd484..1e8dd71 100644 --- a/test/Kentico.Xperience.Shopify.Tests/Mocks/GraphQLHttpClientMock.cs +++ b/test/Kentico.Xperience.Shopify.Tests/Mocks/GraphQLHttpClientMock.cs @@ -9,12 +9,13 @@ namespace Kentico.Xperience.Shopify.Tests.Mocks { internal class GraphQLHttpClientMock : IGraphQLClient { - private ShoppingCartRepository CartRepository => new(); + private readonly IShoppingCartRepository cartRepository; + private DiscountCodesRepository DiscountCodesRepository => new(); - public GraphQLHttpClientMock() + public GraphQLHttpClientMock(IShoppingCartRepository cartRepository) { - + this.cartRepository = cartRepository; } public IObservable> CreateSubscriptionStream(GraphQLRequest request) => throw new NotImplementedException(); @@ -60,7 +61,7 @@ private UpdateDiscountCodesResponse HandleUpdateDiscountCodes(GraphQLRequest req string[]? discountCodes = request.GetProperty("discountCodes"); var availableDiscountCodes = DiscountCodesRepository.DiscountCodes; - var cart = CartRepository.Carts.FirstOrDefault(x => x.Id == cartId); + var cart = cartRepository.Carts.FirstOrDefault(x => x.Id == cartId); if (cart != null && discountCodes != null) { @@ -83,7 +84,7 @@ private UpdateDiscountCodesResponse HandleUpdateDiscountCodes(GraphQLRequest req private CreateCartResponse HandleCreateCart(GraphQLRequest request) { - var cart = CartRepository.Carts.First(); + var cart = cartRepository.Carts.First(); var createCartParams = request.GetProperty("CartInput"); if (cart.Lines?.Edges is not null && createCartParams?.Lines is not null) @@ -131,7 +132,7 @@ public Task> SendQueryAsync(GraphQLRequest CartLinesUpdate = new CartResponseBase() { UserErrors = [], - Cart = CartRepository.Carts.First() + Cart = cartRepository.Carts.First() } }; var cartLines = cartResponse.CartLinesUpdate.Cart.Lines?.Edges; @@ -159,7 +160,7 @@ public Task> SendQueryAsync(GraphQLRequest { string? cartId = request.GetProperty("cartId")?.ToString(); - var cart = CartRepository.Carts.FirstOrDefault(x => x.Id == cartId); + var cart = cartRepository.Carts.FirstOrDefault(x => x.Id == cartId); if (cart?.Id is null || cartId is null || cart.Id != cartId) { return null; @@ -176,7 +177,7 @@ private RemoveCartItemResponse HandleCartItemRemove(GraphQLRequest request) string? cartId = request.GetProperty("CartId")?.ToString(); string[]? cartLinesToRemove = (string[]?)Convert.ChangeType(request.GetProperty("LineIds"), typeof(string[])); - var cart = CartRepository.Carts.FirstOrDefault(cart => cart.Id == cartId); + var cart = cartRepository.Carts.FirstOrDefault(cart => cart.Id == cartId); var updatedCartLines = cart?.Lines?.Edges.Where(x => !cartLinesToRemove?.Contains(x.Node.Id) ?? false) ?? []; if (cart?.Lines is not null) @@ -231,7 +232,7 @@ private AddToCartResponse HandleAddToCart(GraphQLRequest request) } }; - var cart = CartRepository.Carts.FirstOrDefault(cart => cart.Id == cartId); + var cart = cartRepository.Carts.FirstOrDefault(cart => cart.Id == cartId); var updatedCartLines = cart?.Lines?.Edges.Append(newLineItem) ?? []; if (cart?.Lines is not null) { diff --git a/test/Kentico.Xperience.Shopify.Tests/Repositories/IShoppingCartRepository.cs b/test/Kentico.Xperience.Shopify.Tests/Repositories/IShoppingCartRepository.cs new file mode 100644 index 0000000..0d85595 --- /dev/null +++ b/test/Kentico.Xperience.Shopify.Tests/Repositories/IShoppingCartRepository.cs @@ -0,0 +1,13 @@ +using Kentico.Xperience.Shopify.ShoppingCart; + +namespace Kentico.Xperience.Shopify.Tests.Repositories +{ + internal interface IShoppingCartRepository + { + public IEnumerable Carts { get; set; } + + public CartObjectModel CartWithDiscountCode { get; set; } + + public CartObjectModel CartWithSameProductVariant { get; set; } + } +} diff --git a/test/Kentico.Xperience.Shopify.Tests/Repositories/ShoppingCartRepository.cs b/test/Kentico.Xperience.Shopify.Tests/Repositories/ShoppingCartRepository.cs index b4b31a2..297ba37 100644 --- a/test/Kentico.Xperience.Shopify.Tests/Repositories/ShoppingCartRepository.cs +++ b/test/Kentico.Xperience.Shopify.Tests/Repositories/ShoppingCartRepository.cs @@ -1,19 +1,24 @@ using Kentico.Xperience.Shopify.ShoppingCart; using Kentico.Xperience.Shopify.ShoppingCart.GraphQLModels; + using ShopifySharp.GraphQL; namespace Kentico.Xperience.Shopify.Tests.Repositories { - internal class ShoppingCartRepository + internal class ShoppingCartRepository : IShoppingCartRepository { private readonly DiscountCodesRepository discountCodesRepository = new(); public IEnumerable Carts { get; set; } - public CartObjectModel CartWithDiscountCode => GenerateCartWithDiscountCodes(); + public CartObjectModel CartWithDiscountCode { get; set; } + + public CartObjectModel CartWithSameProductVariant { get; set; } public ShoppingCartRepository() { + CartWithDiscountCode = GenerateCartWithDiscountCodes(); + CartWithSameProductVariant = GenerateCartWithSameProductVariant(); Carts = [ GenerateCart1(), @@ -21,10 +26,55 @@ public ShoppingCartRepository() GenerateCart3(), GenerateCart4(), GenerateCartWithoutLines(), - GenerateCartWithDiscountCodes() + GenerateCartWithDiscountCodes(), + GenerateCartWithSameProductVariant() ]; } + private CartObjectModel GenerateCartWithSameProductVariant() + { + // Create VariantProduct instances + var product1 = new VariantProduct { Title = "Panama Los Lajones Honey" }; + var product2 = new VariantProduct { Title = "Kenya Gakuyuni AA" }; + + // Create Merchandise instances + var merchandise1 = new Merchandise { Id = "gid://shopify/ProductVariant/47401942581544", Title = "2 lb", Product = product1 }; + var merchandise2 = new Merchandise { Id = "gid://shopify/ProductVariant/47401957785896", Title = "Default Title", Product = product2 }; + + // Create CartCost instances + var totalAmount1 = new PriceDto { Amount = 3267.0m, CurrencyCode = CurrencyCode.CZK }; + var totalAmount2 = new PriceDto { Amount = 1272.0m, CurrencyCode = CurrencyCode.CZK }; + var subtotalAmount = new PriceDto { Amount = 4539.0m, CurrencyCode = CurrencyCode.CZK }; + + var cost1 = new CartCost { TotalAmount = totalAmount1 }; + var cost2 = new CartCost { TotalAmount = totalAmount2 }; + + // Create CartLineNode instances + var node1 = new CartLineNode { Id = "gid://shopify/CartLine/244da069-6219-4943-8741-a0c8690d461c?cart=Z2NwLXVzLWVhc3QxOjAxSFM5NUFDWUI4OTQ0N05HUVdKU0hXVFlN", Quantity = 3, Cost = cost1, Merchandise = merchandise1 }; + var node2 = new CartLineNode { Id = "gid://shopify/CartLine/efb4b873-08b8-48dc-94d4-b026fdebfa07?cart=Z2NwLXVzLWVhc3QxOjAxSFM5NUFDWUI4OTQ0N05HUVdKU0hXVFlN", Quantity = 3, Cost = cost2, Merchandise = merchandise2 }; + var node3 = new CartLineNode { Id = "gid://shopify/CartLine/8rt7qwpv-z8mk-297g-8eq4g-b9eq7g8e96rn?cart=Z2NwLXVzLWVhc3QxOjAxSFM5NUFDWUI4OTQ0N05HUVdKU0hXVFlN", Quantity = 2, Cost = cost1, Merchandise = merchandise1 }; + + // Create CartLineEdge instances + var edge1 = new CartLineEdge { Node = node1 }; + var edge2 = new CartLineEdge { Node = node2 }; + var edge3 = new CartLineEdge { Node = node3 }; + + // Create CartLines instance + var lines = new CartLines { Edges = [edge1, edge2, edge3] }; + + // Create CartCost instance + var cost = new CartCost { TotalAmount = totalAmount1, SubtotalAmount = subtotalAmount }; + + // Create CartObjectModel instance + return new CartObjectModel + { + Id = "gid://shopify/Cart/Z2NwLWV1cm9wZS13ZXN0MTowMUhXUTE4TjNCV0FaTlNEMTdTVks2RVhUQQ", + TotalQuantity = 6, + CheckoutUrl = "https://quickstart-3b0f1a15.myshopify.com/cart/c/Z2NwLWV1cm9wZS13ZXN0MTowMUhXUTE4TjNCV0FaTlNEMTdTVks2RVhUQQ?key=ca0127810e60f064542c8a70ee304b76", + Cost = cost, + Lines = lines + }; + } private CartObjectModel GenerateCartWithDiscountCodes() { diff --git a/test/Kentico.Xperience.Shopify.Tests/ShoppingServiceTests.cs b/test/Kentico.Xperience.Shopify.Tests/ShoppingServiceTests.cs index 985fadb..8edc994 100644 --- a/test/Kentico.Xperience.Shopify.Tests/ShoppingServiceTests.cs +++ b/test/Kentico.Xperience.Shopify.Tests/ShoppingServiceTests.cs @@ -18,7 +18,7 @@ namespace Kentico.Xperience.Shopify.Tests public class ShoppingServiceTests { private readonly AutoMocker mocker; - private ShoppingCartRepository CartRepository { get; set; } + private IShoppingCartRepository CartRepository { get; set; } private DiscountCodesRepository DiscountCodesRepository { get; set; } @@ -37,7 +37,6 @@ public ShoppingServiceTests() [OneTimeSetUp] public void OneTimeSetUp() { - mocker.Setup(c => c.CreateGraphQLHttpClient()).Returns(new GraphQLHttpClientMock()); mocker.Use(new ShoppingCartCacheServiceMock()); mocker.Use(new EcommerceActivityLoggerMock()); } @@ -52,6 +51,8 @@ public void SetUp() { CartRepository = new ShoppingCartRepository(); DiscountCodesRepository = new DiscountCodesRepository(); + mocker.Use(CartRepository); + mocker.Setup(c => c.CreateGraphQLHttpClient()).Returns(new GraphQLHttpClientMock(CartRepository)); } @@ -115,7 +116,7 @@ public async Task UpdateCartItem_SetNegativeQuantity_ShouldRemoveCartItem() { Country = CountryCode.CZ, Quantity = -1, - MerchandiseID = itemToUpdate.ShopifyCartItemId + ShoppingCartItemID = itemToUpdate.ShopifyCartItemId })).Cart; Assert.Multiple(() => @@ -208,7 +209,7 @@ public async Task UpdateCartItem_UpdatedProductAlreadyInCart_ShouldUpdateExistin { Country = CountryCode.CZ, Quantity = newQuantity, - MerchandiseID = itemToUpdate.ShopifyCartItemId + ShoppingCartItemID = itemToUpdate.ShopifyCartItemId })).Cart; Assert.Multiple(() => @@ -237,7 +238,7 @@ public async Task RemoveCartItem_CartContainsItem_ShouldRemoveCartItem() var cartItemsList = shoppingCart.Items.ToList(); var itemToRemove = cartItemsList[0]; - var retrievedCart = (await shoppingService.RemoveCartItem(itemToRemove.VariantGraphQLId)).Cart; + var retrievedCart = (await shoppingService.RemoveCartItem(itemToRemove.ShopifyCartItemId)).Cart; cartItemsList.Remove(itemToRemove); Assert.Multiple(() => @@ -251,6 +252,32 @@ public async Task RemoveCartItem_CartContainsItem_ShouldRemoveCartItem() } + /// + /// Remove cart items with the same product variant GraphQL ID. + /// + [Test] + public async Task RemoveCartProductVariant_CartContainsVariant_ShouldRemoveCartItem() + { + var shoppingCart = new ShoppingCartInfo(CartRepository.CartWithSameProductVariant); + SetHttpContext(shoppingCart); + var shoppingService = mocker.CreateInstance(); + var cartItemsList = shoppingCart.Items.ToList(); + var duplicitItem = cartItemsList[0]; + + var retrievedCart = (await shoppingService.RemoveProductVariantFromCart(duplicitItem.VariantGraphQLId)).Cart; + cartItemsList.RemoveAll(x => x.VariantGraphQLId == duplicitItem.VariantGraphQLId); + + Assert.Multiple(() => + { + Assert.That(retrievedCart, Is.Not.Null); + + var retrievedCartItems = retrievedCart!.Items.ToList(); + Assert.That(retrievedCartItems, Has.Count.EqualTo(cartItemsList.Count)); + Assert.That(retrievedCartItems.Exists(x => x.VariantGraphQLId == duplicitItem.VariantGraphQLId), Is.False); + }); + } + + /// /// Remove non existing shopping cart item. /// diff --git a/test/Kentico.Xperience.Shopify.Tests/packages.lock.json b/test/Kentico.Xperience.Shopify.Tests/packages.lock.json index 682f27f..9869f01 100644 --- a/test/Kentico.Xperience.Shopify.Tests/packages.lock.json +++ b/test/Kentico.Xperience.Shopify.Tests/packages.lock.json @@ -986,8 +986,8 @@ "Kentico.Xperience.Admin": "[29.0.2, )", "Kentico.Xperience.Core": "[29.0.2, )", "Kentico.Xperience.Ecommerce.Common": "[1.0.0-prerelease-1, )", - "ShopifySharp": "[6.13.0, )", - "ShopifySharp.Extensions.DependencyInjection": "[1.4.0, )", + "ShopifySharp": "[6.17.0, )", + "ShopifySharp.Extensions.DependencyInjection": "[1.6.0, )", "System.Configuration.ConfigurationManager": "[8.0.0, )", "System.Linq.Async": "[6.0.1, )" } @@ -1075,9 +1075,9 @@ }, "ShopifySharp": { "type": "CentralTransitive", - "requested": "[6.13.0, )", - "resolved": "6.13.0", - "contentHash": "GwM1K0FzTo9BrXSqG9hWxIOGGfhrbBfLXK+b6vMqaabmw0Uv4/n9gskQKSZ9CIB7vJksI4Grn+Tk9qW/dVqBJQ==", + "requested": "[6.17.0, )", + "resolved": "6.17.0", + "contentHash": "0yvFrbdvCaQyWHOXsd5Lp15mPpJDQ0G+PE8vJVRkMiH39nrEemI1i2UYrFTS8axgNndGJzB+E1XzctwELp7uug==", "dependencies": { "Microsoft.Extensions.Http": "2.1.0", "System.Text.Json": "7.0.3", @@ -1086,11 +1086,11 @@ }, "ShopifySharp.Extensions.DependencyInjection": { "type": "CentralTransitive", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "jpaU4WpqjAXm/Tm7vQwC/gBbsdIHa4taO/aAN6BZxT8t91f9+OHkWZzFOsvChVUNn61cShcTR84tUfrv6X2cVg==", + "requested": "[1.6.0, )", + "resolved": "1.6.0", + "contentHash": "Hf0MexgLD6FbTcm81lFMPsw5kWK4XNEl7FPmYuoE9kPlWhWwR/wUWMYUpn6uSBTHCE+62yI2l+i7lsOb0zASnA==", "dependencies": { - "ShopifySharp": "6.13.0", + "ShopifySharp": "6.16.0", "microsoft.extensions.dependencyinjection": "8.0.0" } },