From 1b819b6f593a5f834c6adf73cbbbc32fd26d197d Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 12:27:28 +0800 Subject: [PATCH 01/31] feat(catalog): scaffold five-project module structure --- Eternelle.slnx | 22 +++++++++++++++++++ .../AssemblyReference.cs | 8 +++++++ ...ernelle.Modules.Catalog.Application.csproj | 9 ++++++++ .../Eternelle.Modules.Catalog.Domain.csproj | 7 ++++++ .../Database/Schemas.cs | 6 +++++ ...elle.Modules.Catalog.Infrastructure.csproj | 16 ++++++++++++++ ...e.Modules.Catalog.IntegrationEvents.csproj | 7 ++++++ .../AssemblyReference.cs | 8 +++++++ ...rnelle.Modules.Catalog.Presentation.csproj | 8 +++++++ 9 files changed, 91 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Application/AssemblyReference.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Eternelle.Modules.Catalog.Application.csproj create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Eternelle.Modules.Catalog.Domain.csproj create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Schemas.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Eternelle.Modules.Catalog.Infrastructure.csproj create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/Eternelle.Modules.Catalog.IntegrationEvents.csproj create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/AssemblyReference.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Eternelle.Modules.Catalog.Presentation.csproj diff --git a/Eternelle.slnx b/Eternelle.slnx index b2c3b55..933c179 100644 --- a/Eternelle.slnx +++ b/Eternelle.slnx @@ -1,4 +1,19 @@ + + + + + + + + + + + + + + + @@ -15,6 +30,13 @@ + + + + + + + diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Application/AssemblyReference.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Application/AssemblyReference.cs new file mode 100644 index 0000000..5600467 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Application/AssemblyReference.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Eternelle.Modules.Catalog.Application; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Eternelle.Modules.Catalog.Application.csproj b/src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Eternelle.Modules.Catalog.Application.csproj new file mode 100644 index 0000000..a6c704f --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Eternelle.Modules.Catalog.Application.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Eternelle.Modules.Catalog.Domain.csproj b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Eternelle.Modules.Catalog.Domain.csproj new file mode 100644 index 0000000..06763b3 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Eternelle.Modules.Catalog.Domain.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Schemas.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Schemas.cs new file mode 100644 index 0000000..ee3561e --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Schemas.cs @@ -0,0 +1,6 @@ +namespace Eternelle.Modules.Catalog.Infrastructure.Database; + +internal static class Schemas +{ + internal const string Catalog = "catalog"; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Eternelle.Modules.Catalog.Infrastructure.csproj b/src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Eternelle.Modules.Catalog.Infrastructure.csproj new file mode 100644 index 0000000..6e1754e --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Eternelle.Modules.Catalog.Infrastructure.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/Eternelle.Modules.Catalog.IntegrationEvents.csproj b/src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/Eternelle.Modules.Catalog.IntegrationEvents.csproj new file mode 100644 index 0000000..b9e5a29 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/Eternelle.Modules.Catalog.IntegrationEvents.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/AssemblyReference.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/AssemblyReference.cs new file mode 100644 index 0000000..711bf77 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/AssemblyReference.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Eternelle.Modules.Catalog.Presentation; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Eternelle.Modules.Catalog.Presentation.csproj b/src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Eternelle.Modules.Catalog.Presentation.csproj new file mode 100644 index 0000000..297b70e --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Eternelle.Modules.Catalog.Presentation.csproj @@ -0,0 +1,8 @@ + + + + + + + + From 75139b7f873938bbfb6609450d3125897158dedb Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 12:43:49 +0800 Subject: [PATCH 02/31] feat(catalog): add TemplateTier, PriceClass, SectionId enums --- .../Templates/PriceClass.cs | 8 ++++++++ .../Templates/SectionId.cs | 20 +++++++++++++++++++ .../Templates/TemplateTier.cs | 7 +++++++ 3 files changed, 35 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/PriceClass.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTier.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/PriceClass.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/PriceClass.cs new file mode 100644 index 0000000..21ef21e --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/PriceClass.cs @@ -0,0 +1,8 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public enum PriceClass +{ + Free = 1, + Pro = 2, + Plus = 3 +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cs new file mode 100644 index 0000000..d87cdaa --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cs @@ -0,0 +1,20 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +// Persisted as the enum NAME (string) so the Tenancy module can match selections +// against this catalog by a stable identifier. Do not renumber/rename without a migration. +public enum SectionId +{ + Hero, + Story, + Entourage, + DressCode, + Gallery, + GiftGuide, + Guestbook, + Venue, + Reminders, + Rsvp, + VendorCredits, + SnapShare, + GuestUploads +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTier.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTier.cs new file mode 100644 index 0000000..1c94a31 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTier.cs @@ -0,0 +1,7 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public enum TemplateTier +{ + FirstClass = 1, + Signature = 2 +} From ab649c539670a632fb93ff0e94e0ccf19f4b8298 Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:02:47 +0800 Subject: [PATCH 03/31] feat(catalog): add strongly-typed ids for Template and children --- .../Templates/TemplateId.cs | 7 +++++++ .../Templates/TemplateSectionVariantId.cs | 7 +++++++ .../Templates/TemplateSupportedSectionId.cs | 7 +++++++ .../Templates/TemplateThemeId.cs | 7 +++++++ 4 files changed, 28 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateId.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariantId.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSectionId.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeId.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateId.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateId.cs new file mode 100644 index 0000000..619eb4c --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateId.cs @@ -0,0 +1,7 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public readonly record struct TemplateId(Guid Value) +{ + public static TemplateId New() => new(Guid.CreateVersion7()); + public override string ToString() => Value.ToString(); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariantId.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariantId.cs new file mode 100644 index 0000000..c585487 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariantId.cs @@ -0,0 +1,7 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public readonly record struct TemplateSectionVariantId(Guid Value) +{ + public static TemplateSectionVariantId New() => new(Guid.CreateVersion7()); + public override string ToString() => Value.ToString(); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSectionId.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSectionId.cs new file mode 100644 index 0000000..0f50248 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSectionId.cs @@ -0,0 +1,7 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public readonly record struct TemplateSupportedSectionId(Guid Value) +{ + public static TemplateSupportedSectionId New() => new(Guid.CreateVersion7()); + public override string ToString() => Value.ToString(); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeId.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeId.cs new file mode 100644 index 0000000..d0c1508 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeId.cs @@ -0,0 +1,7 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public readonly record struct TemplateThemeId(Guid Value) +{ + public static TemplateThemeId New() => new(Guid.CreateVersion7()); + public override string ToString() => Value.ToString(); +} From e88b1bdfd48a4e51abe275cb54e5853e59d879a4 Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:04:19 +0800 Subject: [PATCH 04/31] feat(catalog): add value objects and errors --- .../Shared/ImageUrl.cs | 38 +++++++++++++++++++ .../Shared/ImageUrlErrors.cs | 15 ++++++++ .../Templates/TemplateDescription.cs | 33 ++++++++++++++++ .../Templates/TemplateDescriptionErrors.cs | 12 ++++++ .../Templates/TemplateName.cs | 33 ++++++++++++++++ .../Templates/TemplateNameErrors.cs | 12 ++++++ .../Templates/TemplateThemeName.cs | 33 ++++++++++++++++ .../Templates/TemplateThemeNameErrors.cs | 12 ++++++ 8 files changed, 188 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrl.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrlErrors.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescription.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescriptionErrors.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateName.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateNameErrors.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeName.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeNameErrors.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrl.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrl.cs new file mode 100644 index 0000000..917e306 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrl.cs @@ -0,0 +1,38 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Shared; + +public sealed record ImageUrl +{ + public const int MaxLength = 2048; + + private ImageUrl(string value) => Value = value; + + public string Value { get; } + + public static Result Create(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return Result.Failure(ImageUrlErrors.Empty); + } + + string trimmed = raw.Trim(); + + if (trimmed.Length > MaxLength) + { + return Result.Failure(ImageUrlErrors.TooLong); + } + + if (!Uri.TryCreate(trimmed, UriKind.RelativeOrAbsolute, out _)) + { + return Result.Failure(ImageUrlErrors.InvalidFormat); + } + + return Result.Success(new ImageUrl(trimmed)); + } + + public override string ToString() => Value; + + internal static ImageUrl FromPersistence(string value) => new(value); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrlErrors.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrlErrors.cs new file mode 100644 index 0000000..df5ecce --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrlErrors.cs @@ -0,0 +1,15 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Shared; + +internal static class ImageUrlErrors +{ + public static readonly Error Empty = + Error.Problem("ImageUrl.Empty", "Image URL must not be empty"); + + public static readonly Error TooLong = + Error.Problem("ImageUrl.TooLong", $"Image URL must not exceed {ImageUrl.MaxLength} characters"); + + public static readonly Error InvalidFormat = + Error.Problem("ImageUrl.InvalidFormat", "Image URL must be a valid URI"); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescription.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescription.cs new file mode 100644 index 0000000..0956f2e --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescription.cs @@ -0,0 +1,33 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed record TemplateDescription +{ + public const int MaxLength = 1000; + + private TemplateDescription(string value) => Value = value; + + public string Value { get; } + + public static Result Create(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return Result.Failure(TemplateDescriptionErrors.Empty); + } + + string trimmed = raw.Trim(); + + if (trimmed.Length > MaxLength) + { + return Result.Failure(TemplateDescriptionErrors.TooLong); + } + + return Result.Success(new TemplateDescription(trimmed)); + } + + public override string ToString() => Value; + + internal static TemplateDescription FromPersistence(string value) => new(value); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescriptionErrors.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescriptionErrors.cs new file mode 100644 index 0000000..f10497f --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescriptionErrors.cs @@ -0,0 +1,12 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +internal static class TemplateDescriptionErrors +{ + public static readonly Error Empty = + Error.Problem("TemplateDescription.Empty", "Template description must not be empty when provided"); + + public static readonly Error TooLong = + Error.Problem("TemplateDescription.TooLong", $"Template description must not exceed {TemplateDescription.MaxLength} characters"); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateName.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateName.cs new file mode 100644 index 0000000..7f7a9c6 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateName.cs @@ -0,0 +1,33 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed record TemplateName +{ + public const int MaxLength = 150; + + private TemplateName(string value) => Value = value; + + public string Value { get; } + + public static Result Create(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return Result.Failure(TemplateNameErrors.Empty); + } + + string trimmed = raw.Trim(); + + if (trimmed.Length > MaxLength) + { + return Result.Failure(TemplateNameErrors.TooLong); + } + + return Result.Success(new TemplateName(trimmed)); + } + + public override string ToString() => Value; + + internal static TemplateName FromPersistence(string value) => new(value); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateNameErrors.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateNameErrors.cs new file mode 100644 index 0000000..cd2ef8b --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateNameErrors.cs @@ -0,0 +1,12 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +internal static class TemplateNameErrors +{ + public static readonly Error Empty = + Error.Problem("TemplateName.Empty", "Template name must not be empty"); + + public static readonly Error TooLong = + Error.Problem("TemplateName.TooLong", $"Template name must not exceed {TemplateName.MaxLength} characters"); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeName.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeName.cs new file mode 100644 index 0000000..6a14a29 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeName.cs @@ -0,0 +1,33 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed record TemplateThemeName +{ + public const int MaxLength = 100; + + private TemplateThemeName(string value) => Value = value; + + public string Value { get; } + + public static Result Create(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return Result.Failure(TemplateThemeNameErrors.Empty); + } + + string trimmed = raw.Trim(); + + if (trimmed.Length > MaxLength) + { + return Result.Failure(TemplateThemeNameErrors.TooLong); + } + + return Result.Success(new TemplateThemeName(trimmed)); + } + + public override string ToString() => Value; + + internal static TemplateThemeName FromPersistence(string value) => new(value); +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeNameErrors.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeNameErrors.cs new file mode 100644 index 0000000..2fcf177 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeNameErrors.cs @@ -0,0 +1,12 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +internal static class TemplateThemeNameErrors +{ + public static readonly Error Empty = + Error.Problem("TemplateThemeName.Empty", "Theme name must not be empty"); + + public static readonly Error TooLong = + Error.Problem("TemplateThemeName.TooLong", $"Theme name must not exceed {TemplateThemeName.MaxLength} characters"); +} From e159b63b6aa774e209e1e2a504be2d0fde7f8e84 Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:05:18 +0800 Subject: [PATCH 05/31] feat(catalog): add jsonb theme styling records --- .../Templates/ThemeStyling.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ThemeStyling.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ThemeStyling.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ThemeStyling.cs new file mode 100644 index 0000000..5301c1f --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ThemeStyling.cs @@ -0,0 +1,9 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +// Serialized to jsonb columns. Dictionaries keep the schema open for design tokens +// (e.g. { "primary": "#1a1a1a", "accent": "#c8a96a" }) without a migration per token. +public sealed record ThemeColors(IReadOnlyDictionary Tokens); + +public sealed record ThemeTypography(IReadOnlyDictionary Tokens); + +public sealed record ThemeTextures(IReadOnlyDictionary Tokens); From 5567a653858678d20c66b2496297dbf4816ace35 Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:06:15 +0800 Subject: [PATCH 06/31] feat(catalog): add Template child entities with variant upload limit --- .../Templates/TemplateSectionVariant.cs | 36 ++++++++++++++++++ .../Templates/TemplateSupportedSection.cs | 25 +++++++++++++ .../Templates/TemplateTheme.cs | 37 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSection.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs new file mode 100644 index 0000000..0f719e7 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs @@ -0,0 +1,36 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed class TemplateSectionVariant : Entity +{ + private TemplateSectionVariant() { } + + public TemplateSectionVariantId Id { get; private set; } + public TemplateId TemplateId { get; private set; } + public SectionId SectionId { get; private set; } + public string Variant { get; private set; } = null!; + public bool IsDefault { get; private set; } + public PriceClass PriceClass { get; private set; } + + // Upload cap for upload-capable sections (e.g. GuestUploads). null = unlimited / N/A. + public int? UploadLimit { get; private set; } + + internal static TemplateSectionVariant Create( + TemplateId templateId, + SectionId sectionId, + string variant, + bool isDefault, + PriceClass priceClass, + int? uploadLimit) => + new() + { + Id = TemplateSectionVariantId.New(), + TemplateId = templateId, + SectionId = sectionId, + Variant = variant, + IsDefault = isDefault, + PriceClass = priceClass, + UploadLimit = uploadLimit + }; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSection.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSection.cs new file mode 100644 index 0000000..d63c398 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSection.cs @@ -0,0 +1,25 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed class TemplateSupportedSection : Entity +{ + private TemplateSupportedSection() { } + + public TemplateSupportedSectionId Id { get; private set; } + public TemplateId TemplateId { get; private set; } + public SectionId SectionId { get; private set; } + public int DisplayOrder { get; private set; } + + internal static TemplateSupportedSection Create( + TemplateId templateId, + SectionId sectionId, + int displayOrder) => + new() + { + Id = TemplateSupportedSectionId.New(), + TemplateId = templateId, + SectionId = sectionId, + DisplayOrder = displayOrder + }; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs new file mode 100644 index 0000000..15c66df --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs @@ -0,0 +1,37 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed class TemplateTheme : Entity +{ + private TemplateTheme() { } + + public TemplateThemeId Id { get; private set; } + public TemplateId TemplateId { get; private set; } + public int ThemeIndex { get; private set; } + public TemplateThemeName Name { get; private set; } = null!; + public ThemeColors Colors { get; private set; } = null!; + public ThemeTypography Typography { get; private set; } = null!; + public ThemeTextures? Textures { get; private set; } + public bool IsDark { get; private set; } + + internal static TemplateTheme Create( + TemplateId templateId, + int themeIndex, + TemplateThemeName name, + ThemeColors colors, + ThemeTypography typography, + ThemeTextures? textures, + bool isDark) => + new() + { + Id = TemplateThemeId.New(), + TemplateId = templateId, + ThemeIndex = themeIndex, + Name = name, + Colors = colors, + Typography = typography, + Textures = textures, + IsDark = isDark + }; +} From d503809e03a13ad7edcdfc48a37f9a2ba9642599 Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:09:31 +0800 Subject: [PATCH 07/31] feat(catalog): add Template domain events and manifest payload --- .../Templates/TemplateCreatedDomainEvent.cs | 13 +++++++++++++ .../TemplateManifestChangedDomainEvent.cs | 17 +++++++++++++++++ .../Templates/TemplateManifestItem.cs | 8 ++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateCreatedDomainEvent.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestItem.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateCreatedDomainEvent.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateCreatedDomainEvent.cs new file mode 100644 index 0000000..f42b3e9 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateCreatedDomainEvent.cs @@ -0,0 +1,13 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed class TemplateCreatedDomainEvent( + TemplateId templateId, + string name, + string tier) : DomainEvent +{ + public TemplateId TemplateId { get; init; } = templateId; + public string Name { get; init; } = name; + public string Tier { get; init; } = tier; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs new file mode 100644 index 0000000..9e5d4fc --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs @@ -0,0 +1,17 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed class TemplateManifestChangedDomainEvent( + TemplateId templateId, + string name, + string tier, + IReadOnlyList sectionIds, + IReadOnlyList variants) : DomainEvent +{ + public TemplateId TemplateId { get; init; } = templateId; + public string Name { get; init; } = name; + public string Tier { get; init; } = tier; + public IReadOnlyList SectionIds { get; init; } = sectionIds; + public IReadOnlyList Variants { get; init; } = variants; +} diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestItem.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestItem.cs new file mode 100644 index 0000000..15f0c9b --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestItem.cs @@ -0,0 +1,8 @@ +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed record TemplateManifestItem( + string SectionId, + string Variant, + string PriceClass, + bool IsDefault, + int? UploadLimit); From f1ca0cead11432ba4f623d781c57091b3956cb10 Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:10:23 +0800 Subject: [PATCH 08/31] feat(catalog): add TemplateErrors --- .../Templates/TemplateErrors.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateErrors.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateErrors.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateErrors.cs new file mode 100644 index 0000000..f53d8ec --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateErrors.cs @@ -0,0 +1,27 @@ +using Eternelle.Common.Domain; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public static class TemplateErrors +{ + public static Error NotFound(TemplateId id) => + Error.NotFound("Catalog.Template.NotFound", $"Template '{id.Value}' was not found."); + + public static readonly Error NameTaken = + Error.Conflict("Catalog.Template.NameTaken", "A template with this name already exists."); + + public static readonly Error DuplicateThemeIndex = + Error.Conflict("Catalog.Template.DuplicateThemeIndex", "A theme with this index already exists for the template."); + + public static readonly Error SectionAlreadySupported = + Error.Conflict("Catalog.Template.SectionAlreadySupported", "This section is already supported by the template."); + + public static readonly Error SectionNotSupported = + Error.Problem("Catalog.Template.SectionNotSupported", "A variant cannot be added for a section the template does not support."); + + public static readonly Error DuplicateDefaultVariant = + Error.Conflict("Catalog.Template.DuplicateDefaultVariant", "Only one default variant is allowed per section."); + + public static readonly Error DuplicateVariant = + Error.Conflict("Catalog.Template.DuplicateVariant", "This variant already exists for the section."); +} From 8b245d68c1d3b257b9861d8fa959786f9dbf52fd Mon Sep 17 00:00:00 2001 From: Carl Marion Date: Wed, 27 May 2026 13:11:55 +0800 Subject: [PATCH 09/31] feat(catalog): add Template aggregate root --- .../Templates/Template.cs | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs diff --git a/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs new file mode 100644 index 0000000..8115272 --- /dev/null +++ b/src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs @@ -0,0 +1,152 @@ +using Eternelle.Common.Domain; +using Eternelle.Modules.Catalog.Domain.Shared; + +namespace Eternelle.Modules.Catalog.Domain.Templates; + +public sealed class Template : Entity +{ + private readonly List _themes = []; + private readonly List _supportedSections = []; + private readonly List _sectionVariants = []; + + private Template() { } + + public TemplateId Id { get; private set; } + public TemplateName Name { get; private set; } = null!; + public TemplateDescription? Description { get; private set; } + public TemplateTier Tier { get; private set; } + public ImageUrl? PreviewImageUrl { get; private set; } + public IReadOnlyList Features { get; private set; } = []; + public DateTime CreatedAtUtc { get; private set; } + public DateTime UpdatedAtUtc { get; private set; } + + public IReadOnlyCollection Themes => _themes.AsReadOnly(); + public IReadOnlyCollection SupportedSections => _supportedSections.AsReadOnly(); + public IReadOnlyCollection SectionVariants => _sectionVariants.AsReadOnly(); + + public static Result