using Prefab.Domain.Common; namespace Prefab.Catalog.Domain.Entities; /// /// Identifies what entity a rule set controls (option definition vs individual value). /// public enum OptionRuleTargetKind : byte { OptionDefinition = 0, OptionValue = 1 } /// /// Result to apply when rule conditions evaluate to true. /// public enum RuleEffect : byte { Show = 0, Enable = 1, Require = 2 } /// /// Defines whether all or any conditions must be satisfied. /// public enum RuleMode : byte { All = 0, Any = 1 } /// /// Operators supported when evaluating rule conditions against shopper selections. /// public enum RuleOperator : byte { Equal = 0, NotEqual = 1, InList = 2, NotInList = 3, GreaterThan = 4, GreaterThanOrEqual = 5, LessThan = 6, LessThanOrEqual = 7, Between = 8 } /// /// Groups conditions that control visibility, enablement, or requirement for an option or value. /// public class OptionRuleSet : EntityWithAuditAndStatus { 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 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."); } } } /// /// A single conditional expression applied to option selections. /// public class OptionRuleCondition : EntityWithAuditAndStatus { 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; } } /// /// Records which option value combination maps to an individual variant SKU. /// 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!; }