Skip to content

Commit

Permalink
VCST-2092: add configurable products support (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksavosteev authored Nov 25, 2024
1 parent 8cfa797 commit a43f67b
Show file tree
Hide file tree
Showing 35 changed files with 1,084 additions and 16 deletions.
38 changes: 37 additions & 1 deletion src/VirtoCommerce.XCart.Core/CartAggregate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,42 @@ public virtual Task<CartAggregate> UpdateCartComment(string comment)
return Task.FromResult(this);
}

/// <summary>
/// Always add a new line item for a configured item.
/// </summary>
/// <param name="newCartItem"></param>
/// <param name="newConfiguredItem"></param>
/// <returns></returns>
public virtual async Task<CartAggregate> 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<CartAggregate> AddItemAsync(NewCartItem newCartItem)
{
EnsureCartExists();
Expand Down Expand Up @@ -956,7 +992,7 @@ protected virtual Task<CartAggregate> 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))
Expand Down
6 changes: 6 additions & 0 deletions src/VirtoCommerce.XCart.Core/Commands/AddCartItemCommand.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -23,5 +24,10 @@ public class AddCartItemCommand : CartCommand
/// Dynamic properties
/// </summary>
public IList<DynamicPropertyValue> DynamicProperties { get; set; }

/// <summary>
/// Configurable product sections
/// </summary>
public IList<ProductConfigurationSection> ConfigurationSections { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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<ExpConfigurationLineItem>, 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<ProductConfigurationSection> ConfigurationSections { get; set; } = [];
}
148 changes: 148 additions & 0 deletions src/VirtoCommerce.XCart.Core/ConfiguredLineItemContainer.cs
Original file line number Diff line number Diff line change
@@ -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<string> ProductsIncludeFields { get; set; }

public CartProduct ConfigurableProduct { get; set; }
public IList<LineItem> Items { get; set; } = new List<LineItem>();

public LineItem AddItem(CartProduct cartProduct, int quantity)
{
var lineItem = AbstractTypeFactory<LineItem>.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<LineItem>.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<ConfigurationItem>.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<CartProductsRequest>.TryCreateInstance();

request.Store = Store;
request.Currency = Currency;
request.CultureName = CultureName;
request.Member = Member;
request.UserId = UserId;

return request;
}

public object Clone()
{
return MemberwiseClone();
}
}
}
15 changes: 15 additions & 0 deletions src/VirtoCommerce.XCart.Core/ExpConfigurationLineItem.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
21 changes: 21 additions & 0 deletions src/VirtoCommerce.XCart.Core/Models/CartProductsRequest.cs
Original file line number Diff line number Diff line change
@@ -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<string> ProductIds { get; set; }
public IList<string> ProductsIncludeFields { get; set; }

public bool LoadPrice { get; set; } = true;
public bool LoadInventory { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace VirtoCommerce.XCart.Core.Models;

public class ConfigurableProductOption
{
public string ProductId { get; set; }

public int Quantity { get; set; } = 1;
}
19 changes: 19 additions & 0 deletions src/VirtoCommerce.XCart.Core/Models/ExpConfigurationSection.cs
Original file line number Diff line number Diff line change
@@ -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<ExpConfigurationLineItem> Options { get; set; } = new List<ExpConfigurationLineItem>();
}

public class ProductConfigurationQueryResponse
{
public IList<ExpProductConfigurationSection> ConfigurationSections { get; set; } = new List<ExpProductConfigurationSection>();
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace VirtoCommerce.XCart.Core.Models;

public class ProductConfigurationSection
{
public string SectionId { get; set; }

public ConfigurableProductOption Value { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<ProductConfigurationQueryResponse>, 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<string> IncludeFields { get; set; } = Array.Empty<string>();

public override IEnumerable<QueryArgument> GetArguments()
{
yield return Argument<NonNullGraphType<StringGraphType>>(nameof(ConfigurableProductId));

yield return Argument<NonNullGraphType<StringGraphType>>(nameof(StoreId), description: "Store Id");
yield return Argument<StringGraphType>(nameof(UserId), description: "User Id");
yield return Argument<StringGraphType>(nameof(CultureName), description: "Currency code (\"USD\")");
yield return Argument<StringGraphType>(nameof(CurrencyCode), description: "Culture name (\"en-US\")");
}

public override void Map(IResolveFieldContext context)
{
ConfigurableProductId = context.GetArgument<string>(nameof(ConfigurableProductId));

StoreId = context.GetArgument<string>(nameof(StoreId));
UserId = context.GetArgument<string>(nameof(UserId)) ?? context.GetCurrentUserId();
OrganizationId = context.GetCurrentOrganizationId();
CultureName = context.GetArgument<string>(nameof(CultureName));
CurrencyCode = context.GetArgument<string>(nameof(CurrencyCode));
}
}
14 changes: 14 additions & 0 deletions src/VirtoCommerce.XCart.Core/Schemas/CartConfigurationItemType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using VirtoCommerce.CartModule.Core.Model;
using VirtoCommerce.Xapi.Core.Schemas;

namespace VirtoCommerce.XCart.Core.Schemas
{
public class CartConfigurationItemType : ExtendableGraphType<ConfigurationItem>
{
public CartConfigurationItemType()
{
Field(x => x.Id, nullable: false).Description("Configuration item ID");
Field(x => x.Name, nullable: true).Description("Configuration item name");
}
}
}
Loading

0 comments on commit a43f67b

Please sign in to comment.