Init
This commit is contained in:
268
Prefab.Catalog/Domain/Entities/OptionRules.cs
Normal file
268
Prefab.Catalog/Domain/Entities/OptionRules.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using Prefab.Domain.Common;
|
||||
|
||||
namespace Prefab.Catalog.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies what entity a rule set controls (option definition vs individual value).
|
||||
/// </summary>
|
||||
public enum OptionRuleTargetKind : byte
|
||||
{
|
||||
OptionDefinition = 0,
|
||||
OptionValue = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result to apply when rule conditions evaluate to true.
|
||||
/// </summary>
|
||||
public enum RuleEffect : byte
|
||||
{
|
||||
Show = 0,
|
||||
Enable = 1,
|
||||
Require = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines whether all or any conditions must be satisfied.
|
||||
/// </summary>
|
||||
public enum RuleMode : byte
|
||||
{
|
||||
All = 0,
|
||||
Any = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operators supported when evaluating rule conditions against shopper selections.
|
||||
/// </summary>
|
||||
public enum RuleOperator : byte
|
||||
{
|
||||
Equal = 0,
|
||||
NotEqual = 1,
|
||||
InList = 2,
|
||||
NotInList = 3,
|
||||
GreaterThan = 4,
|
||||
GreaterThanOrEqual = 5,
|
||||
LessThan = 6,
|
||||
LessThanOrEqual = 7,
|
||||
Between = 8
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups conditions that control visibility, enablement, or requirement for an option or value.
|
||||
/// </summary>
|
||||
public class OptionRuleSet : EntityWithAuditAndStatus<Guid>
|
||||
{
|
||||
private OptionRuleSet()
|
||||
{
|
||||
}
|
||||
|
||||
public Guid ProductId { get; internal set; }
|
||||
|
||||
public Product Product { get; internal set; } = null!;
|
||||
|
||||
public OptionRuleTargetKind TargetKind { get; internal set; }
|
||||
|
||||
public Guid TargetId { get; internal set; }
|
||||
|
||||
public RuleEffect Effect { get; internal set; }
|
||||
|
||||
public RuleMode Mode { get; internal set; }
|
||||
|
||||
public List<OptionRuleCondition> Conditions { get; } = [];
|
||||
|
||||
public static OptionRuleSet Create(
|
||||
Product product,
|
||||
OptionRuleTargetKind targetKind,
|
||||
Guid targetId,
|
||||
RuleEffect effect,
|
||||
RuleMode mode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(product);
|
||||
|
||||
if (targetKind == OptionRuleTargetKind.OptionDefinition)
|
||||
{
|
||||
_ = product.Options.FirstOrDefault(o => o.Id == targetId)
|
||||
?? throw new ArgumentException($"Option definition '{targetId}' does not belong to product '{product.Id}'.", nameof(targetId));
|
||||
}
|
||||
else
|
||||
{
|
||||
var ownsValue = product.Options
|
||||
.SelectMany(o => o.Values)
|
||||
.Any(v => v.Id == targetId);
|
||||
if (!ownsValue)
|
||||
{
|
||||
throw new ArgumentException($"Option value '{targetId}' does not belong to product '{product.Id}'.", nameof(targetId));
|
||||
}
|
||||
}
|
||||
|
||||
var ruleSet = new OptionRuleSet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Product = product,
|
||||
ProductId = product.Id,
|
||||
TargetKind = targetKind,
|
||||
TargetId = targetId,
|
||||
Effect = effect,
|
||||
Mode = mode
|
||||
};
|
||||
|
||||
product.RuleSets.Add(ruleSet);
|
||||
return ruleSet;
|
||||
}
|
||||
|
||||
public OptionRuleCondition AddCondition(
|
||||
OptionDefinition leftOptionDefinition,
|
||||
RuleOperator @operator,
|
||||
Guid? rightOptionValueId = null,
|
||||
string? rightList = null,
|
||||
decimal? rightNumber = null,
|
||||
decimal? rightMin = null,
|
||||
decimal? rightMax = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(leftOptionDefinition);
|
||||
if (leftOptionDefinition.ProductId != ProductId)
|
||||
{
|
||||
throw new ArgumentException($"Option definition '{leftOptionDefinition.Id}' does not belong to product '{ProductId}'.", nameof(leftOptionDefinition));
|
||||
}
|
||||
|
||||
ValidateRightHandSide(leftOptionDefinition, @operator, rightOptionValueId, rightList, rightNumber, rightMin, rightMax);
|
||||
|
||||
OptionValue? rightOptionValue = null;
|
||||
if (rightOptionValueId.HasValue)
|
||||
{
|
||||
rightOptionValue = leftOptionDefinition.Values.FirstOrDefault(v => v.Id == rightOptionValueId.Value);
|
||||
if (rightOptionValue is null)
|
||||
{
|
||||
rightOptionValue = Product.Options
|
||||
.SelectMany(o => o.Values)
|
||||
.FirstOrDefault(v => v.Id == rightOptionValueId.Value)
|
||||
?? throw new ArgumentException($"Option value '{rightOptionValueId.Value}' was not found for product '{ProductId}'.", nameof(rightOptionValueId));
|
||||
}
|
||||
}
|
||||
|
||||
var condition = new OptionRuleCondition
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RuleSet = this,
|
||||
RuleSetId = Id,
|
||||
LeftOptionDefinition = leftOptionDefinition,
|
||||
LeftOptionDefinitionId = leftOptionDefinition.Id,
|
||||
Operator = @operator,
|
||||
RightOptionValue = rightOptionValue,
|
||||
RightOptionValueId = rightOptionValueId,
|
||||
RightList = string.IsNullOrWhiteSpace(rightList) ? null : rightList,
|
||||
RightNumber = rightNumber,
|
||||
RightMin = rightMin,
|
||||
RightMax = rightMax
|
||||
};
|
||||
|
||||
Conditions.Add(condition);
|
||||
return condition;
|
||||
}
|
||||
|
||||
private static void ValidateRightHandSide(
|
||||
OptionDefinition leftOptionDefinition,
|
||||
RuleOperator @operator,
|
||||
Guid? rightOptionValueId,
|
||||
string? rightList,
|
||||
decimal? rightNumber,
|
||||
decimal? rightMin,
|
||||
decimal? rightMax)
|
||||
{
|
||||
var isChoice = leftOptionDefinition.DataType == OptionDataType.Choice;
|
||||
|
||||
switch (@operator)
|
||||
{
|
||||
case RuleOperator.Equal:
|
||||
case RuleOperator.NotEqual:
|
||||
if (isChoice)
|
||||
{
|
||||
if (rightOptionValueId is null && string.IsNullOrWhiteSpace(rightList))
|
||||
{
|
||||
throw new ArgumentException("Choice comparisons require a right option value identifier or list.", nameof(rightOptionValueId));
|
||||
}
|
||||
}
|
||||
else if (rightNumber is null)
|
||||
{
|
||||
throw new ArgumentException("Numeric comparisons require a numeric value.", nameof(rightNumber));
|
||||
}
|
||||
break;
|
||||
|
||||
case RuleOperator.InList:
|
||||
case RuleOperator.NotInList:
|
||||
if (string.IsNullOrWhiteSpace(rightList))
|
||||
{
|
||||
throw new ArgumentException("List-based comparisons require a comma delimited list.", nameof(rightList));
|
||||
}
|
||||
break;
|
||||
|
||||
case RuleOperator.GreaterThan:
|
||||
case RuleOperator.GreaterThanOrEqual:
|
||||
case RuleOperator.LessThan:
|
||||
case RuleOperator.LessThanOrEqual:
|
||||
if (rightNumber is null)
|
||||
{
|
||||
throw new ArgumentException("Range comparisons require a numeric value.", nameof(rightNumber));
|
||||
}
|
||||
break;
|
||||
|
||||
case RuleOperator.Between:
|
||||
if (rightMin is null || rightMax is null)
|
||||
{
|
||||
throw new ArgumentException("Between comparisons require both minimum and maximum values.", nameof(rightMin));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(@operator), @operator, "Unsupported rule operator.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single conditional expression applied to option selections.
|
||||
/// </summary>
|
||||
public class OptionRuleCondition : EntityWithAuditAndStatus<Guid>
|
||||
{
|
||||
internal OptionRuleCondition()
|
||||
{
|
||||
}
|
||||
|
||||
public Guid RuleSetId { get; internal set; }
|
||||
|
||||
public OptionRuleSet RuleSet { get; internal set; } = null!;
|
||||
|
||||
public Guid LeftOptionDefinitionId { get; internal set; }
|
||||
|
||||
public OptionDefinition LeftOptionDefinition { get; internal set; } = null!;
|
||||
|
||||
public RuleOperator Operator { get; internal set; }
|
||||
|
||||
public Guid? RightOptionValueId { get; internal set; }
|
||||
|
||||
public OptionValue? RightOptionValue { get; internal set; }
|
||||
|
||||
public string? RightList { get; internal set; }
|
||||
|
||||
public decimal? RightNumber { get; internal set; }
|
||||
|
||||
public decimal? RightMin { get; internal set; }
|
||||
|
||||
public decimal? RightMax { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records which option value combination maps to an individual variant SKU.
|
||||
/// </summary>
|
||||
public class VariantAxisValue
|
||||
{
|
||||
public Guid ProductVariantId { get; set; }
|
||||
|
||||
public Product ProductVariant { get; set; } = null!;
|
||||
|
||||
public Guid OptionDefinitionId { get; set; }
|
||||
|
||||
public OptionDefinition OptionDefinition { get; set; } = null!;
|
||||
|
||||
public Guid OptionValueId { get; set; }
|
||||
|
||||
public OptionValue OptionValue { get; set; } = null!;
|
||||
}
|
||||
Reference in New Issue
Block a user