using System.Text.RegularExpressions; using Prefab.Base.Catalog.Categories; using Prefab.Catalog.Domain.Events; using Prefab.Catalog.Domain.Exceptions; using Prefab.Catalog.Domain.Services; using Prefab.Domain.Common; namespace Prefab.Catalog.Domain.Entities; public class Category : EntityWithAuditAndStatus { private static readonly Regex SlugRegex = new("^[a-z0-9]+(?:-[a-z0-9]+)*$", RegexOptions.Compiled); private Category() { } public Guid? ParentId { get; private set; } public string Name { get; private set; } = string.Empty; public string? Description { get; private set; } public string? Slug { get; private set; } public int DisplayOrder { get; private set; } public bool IsFeatured { get; private set; } public string? HeroImageUrl { get; private set; } public string? Icon { get; private set; } public static async Task Create(string name, string? description, IUniqueChecker check, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(check); var trimmedName = name.Trim(); var trimmedDescription = string.IsNullOrWhiteSpace(description) ? null : description.Trim(); if (!await check.CategoryNameIsUnique(trimmedName, cancellationToken)) { throw new DuplicateNameException(trimmedName, nameof(Category)); } var category = new Category { Id = Guid.NewGuid(), Name = trimmedName, Description = trimmedDescription }; category.AddEvent(new CategoryCreated(category.Id, category.Name)); return category; } public async Task Rename(string newName, IUniqueChecker check, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(newName); ArgumentNullException.ThrowIfNull(check); var trimmed = newName.Trim(); if (string.Equals(trimmed, Name, StringComparison.Ordinal)) { return; } if (!await check.CategoryNameIsUnique(trimmed, cancellationToken)) { throw new DuplicateNameException(trimmed, nameof(Category)); } var oldName = Name; Name = trimmed; AddEvent(new CategoryRenamed(Id, oldName, Name)); } public void ConfigureMetadata(string slug, int displayOrder, bool isFeatured, string? heroImageUrl, string? icon) { ArgumentException.ThrowIfNullOrWhiteSpace(slug); var trimmedSlug = slug.Trim(); if (trimmedSlug.Length > CategoryRules.SlugMaxLength) { throw new DomainValidationException($"Category slug cannot exceed {CategoryRules.SlugMaxLength} characters."); } if (!SlugRegex.IsMatch(trimmedSlug)) { throw new DomainValidationException("Category slug must use lowercase letters, numbers, and hyphens."); } var trimmedHeroImageUrl = string.IsNullOrWhiteSpace(heroImageUrl) ? null : heroImageUrl.Trim(); if (trimmedHeroImageUrl is not null && trimmedHeroImageUrl.Length > CategoryRules.HeroImageUrlMaxLength) { throw new DomainValidationException($"Hero image URL cannot exceed {CategoryRules.HeroImageUrlMaxLength} characters."); } var trimmedIcon = string.IsNullOrWhiteSpace(icon) ? null : icon.Trim(); if (trimmedIcon is not null && trimmedIcon.Length > CategoryRules.IconMaxLength) { throw new DomainValidationException($"Icon cannot exceed {CategoryRules.IconMaxLength} characters."); } Slug = trimmedSlug; DisplayOrder = displayOrder; IsFeatured = isFeatured; HeroImageUrl = trimmedHeroImageUrl; Icon = trimmedIcon; } }