138 lines
3.9 KiB
C#
138 lines
3.9 KiB
C#
using Prefab.Catalog.Domain.Entities;
|
|
|
|
namespace Prefab.Catalog.Domain.Services;
|
|
|
|
/// <summary>
|
|
/// Result of variant lookup based on the shopper's selections.
|
|
/// </summary>
|
|
public sealed class VariantResolutionResult
|
|
{
|
|
public VariantResolutionResult(Product? variant, IReadOnlyCollection<string> errors)
|
|
{
|
|
Variant = variant;
|
|
Errors = errors;
|
|
}
|
|
|
|
public Product? Variant { get; }
|
|
|
|
public IReadOnlyCollection<string> Errors { get; }
|
|
|
|
public bool IsSuccess => Errors.Count == 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a model variant using axis selections or SKU.
|
|
/// </summary>
|
|
public sealed class VariantResolver
|
|
{
|
|
public VariantResolutionResult Resolve(Product model, IReadOnlyCollection<OptionSelection> selections, string? sku = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(model);
|
|
ArgumentNullException.ThrowIfNull(selections);
|
|
|
|
var errors = new List<string>();
|
|
|
|
if (!string.IsNullOrWhiteSpace(sku))
|
|
{
|
|
var variant = model.Variants
|
|
.Where(v => v.DeletedOn is null)
|
|
.FirstOrDefault(v => string.Equals(v.Sku, sku, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (variant is null)
|
|
{
|
|
errors.Add($"Variant with SKU '{sku}' was not found for product '{model.Name}'.");
|
|
}
|
|
|
|
return new VariantResolutionResult(variant, errors);
|
|
}
|
|
|
|
var axisDefinitions = model.Options
|
|
.Where(o => o.DeletedOn is null && o.IsVariantAxis)
|
|
.ToList();
|
|
|
|
if (axisDefinitions.Count == 0)
|
|
{
|
|
// No variant axes means the base model should be used.
|
|
return new VariantResolutionResult(null, errors);
|
|
}
|
|
|
|
var selectionMap = selections
|
|
.Where(s => s.OptionValueId.HasValue)
|
|
.ToDictionary(s => s.OptionDefinitionId, s => s.OptionValueId!.Value);
|
|
|
|
foreach (var axis in axisDefinitions)
|
|
{
|
|
if (!selectionMap.ContainsKey(axis.Id))
|
|
{
|
|
errors.Add($"Selection for variant axis '{axis.Name}' is required.");
|
|
}
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
return new VariantResolutionResult(null, errors);
|
|
}
|
|
|
|
var matches = new List<Product>();
|
|
|
|
foreach (var variant in model.Variants.Where(v => v.DeletedOn is null))
|
|
{
|
|
var axisValues = GetAxisValuesForVariant(model, variant);
|
|
|
|
if (axisValues.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var isMatch = true;
|
|
foreach (var axis in axisDefinitions)
|
|
{
|
|
var expectedValue = selectionMap[axis.Id];
|
|
if (!axisValues.TryGetValue(axis.Id, out var actualValue) || actualValue != expectedValue)
|
|
{
|
|
isMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isMatch)
|
|
{
|
|
matches.Add(variant);
|
|
}
|
|
}
|
|
|
|
if (matches.Count == 1)
|
|
{
|
|
return new VariantResolutionResult(matches[0], errors);
|
|
}
|
|
|
|
if (matches.Count == 0)
|
|
{
|
|
errors.Add("No variant matches the provided selections.");
|
|
}
|
|
else
|
|
{
|
|
errors.Add("Multiple variants match the provided selections.");
|
|
}
|
|
|
|
return new VariantResolutionResult(null, errors);
|
|
}
|
|
|
|
private static IDictionary<Guid, Guid> GetAxisValuesForVariant(Product model, Product variant)
|
|
{
|
|
var axisValues = variant.AxisValues?
|
|
.ToDictionary(av => av.OptionDefinitionId, av => av.OptionValueId);
|
|
|
|
if (axisValues is not null && axisValues.Count > 0)
|
|
{
|
|
return axisValues;
|
|
}
|
|
|
|
axisValues = model.AxisValues?
|
|
.Where(av => av.ProductVariantId == variant.Id)
|
|
.ToDictionary(av => av.OptionDefinitionId, av => av.OptionValueId);
|
|
|
|
return axisValues ?? new Dictionary<Guid, Guid>();
|
|
}
|
|
}
|