using Prefab.Catalog.Domain.Entities;
namespace Prefab.Catalog.Domain.Services;
///
/// Result of variant lookup based on the shopper's selections.
///
public sealed class VariantResolutionResult
{
public VariantResolutionResult(Product? variant, IReadOnlyCollection errors)
{
Variant = variant;
Errors = errors;
}
public Product? Variant { get; }
public IReadOnlyCollection Errors { get; }
public bool IsSuccess => Errors.Count == 0;
}
///
/// Resolves a model variant using axis selections or SKU.
///
public sealed class VariantResolver
{
public VariantResolutionResult Resolve(Product model, IReadOnlyCollection selections, string? sku = null)
{
ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(selections);
var errors = new List();
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();
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 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();
}
}