Skip to content

Eternelle-180 Catalog Module#181

Merged
engrkwakwak merged 31 commits into
developfrom
eternelle-180
May 27, 2026
Merged

Eternelle-180 Catalog Module#181
engrkwakwak merged 31 commits into
developfrom
eternelle-180

Conversation

@engrkwakwak

@engrkwakwak engrkwakwak commented May 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added a Catalog module for managing wedding templates: create, update, list, and view template details (themes, supported sections, section variants).
    • New HTTP endpoints for template creation, updates, retrieval, theme/section/variant management; tiered pricing and upload limits supported.
    • Templates and lists are cached for performance; template manifest changes publish integration events.
  • Chores

    • Catalog database schema, background outbox processing, and authorization (catalog:manage) added.

Review Change Stack

…rations

Adds InternalsVisibleTo for the Infrastructure layer to access VO FromPersistence()
methods, plus EF Core entity type configurations for all Template aggregates.
Public read endpoints (GetTemplates, GetTemplateById, GetSectionVariants) with AllowAnonymous; admin write endpoints (CreateTemplate, UpdateTemplate, AddTemplateTheme, AddSupportedSection, AddSectionVariant) gated behind catalog:manage policy.
…igration

Registers CatalogModule in Program.cs (assembly, config, DI), adds catalog config
files, and generates the InitialCatalog EF migration with Kapilya seed data.
Adds a Users migration that inserts the catalog:manage permission and
assigns it to the Admin role, enabling authorized catalog management
operations.
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Walkthrough

Adds a new Catalog bounded context with domain model, application commands/queries, EF Core persistence and migrations, outbox processing, HTTP endpoints, integration events, API wiring/configuration, and a Users migration to seed the catalog:manage permission.

Changes

Catalog module introduction

Layer / File(s) Summary
Catalog Domain model (aggregate, VOs, events)
src/Modules/Catalog/Domain/*
Defines Template aggregate, value objects (names, descriptions, image URL, theme), IDs, enums, domain events, errors, and repository contract.
Application commands, queries, validators, handlers
src/Modules/Catalog/Application/*
Implements Create/Update/AddTheme/AddSupportedSection/AddSectionVariant commands with validators/handlers, queries with caching, cache keys, IUnitOfWork, and domain-event-to-integration-event handler.
Infrastructure: EF Core, DI wiring, repository
src/Modules/Catalog/Infrastructure/*
Registers Catalog module, CatalogDbContext, value converters, entity configurations, repository, schema constant, outbox options binding, and AddCatalogModule wiring.
Outbox processing and idempotent handlers
src/Modules/Catalog/Infrastructure/Outbox/*
Adds Quartz ProcessOutboxJob, IdempotentDomainEventHandler<T>, options configurator, and outbox consumer persistence helpers.
Catalog migrations (schema + seed)
src/Modules/Catalog/Infrastructure/Database/Migrations/*
Creates catalog schema/tables/indexes and seeds initial template/theme/section/variant data; snapshot included.
Entity mappings and converters
src/Modules/Catalog/Infrastructure/Templates/*, .../Database/Converters/*
EF Core configurations for Template/Theme/SectionVariant/SupportedSection and ID value converters and JSONB feature storage.
HTTP endpoints for Catalog
src/Modules/Catalog/Presentation/*
Maps endpoints for creating/updating templates, adding themes/sections/variants, and fetching templates/details/variants with tags and auth.
Integration event contracts
src/Modules/Catalog/IntegrationEvents/*
Adds CatalogTemplateManifestChangedIntegrationEvent and SectionVariantManifest DTOs for manifest publishing.
Solution, API wiring, and module config
Eternelle.slnx, src/API/Eternelle.Api/*
Adds Catalog projects to solution, updates API Program to register Catalog and load modules.catalog*.json, and adds module config with Catalog.Outbox settings.
Users migration: add catalog:manage permission
src/Modules/Users/.../Migrations/*
Seeds catalog:manage permission and associates it with Admin role.

Sequence Diagram

sequenceDiagram
  participant Client
  participant API as Catalog Endpoints
  participant App as MediatR Handlers
  participant Repo as ITemplateRepository
  participant UoW as IUnitOfWork (CatalogDbContext)
  participant Cache as ICacheService
  participant DB as PostgreSQL

  Client->>API: POST /catalog/templates (CreateTemplate)
  API->>App: Send(CreateTemplateCommand)
  App->>Repo: NameExistsAsync/Insert(template)
  App->>UoW: SaveChangesAsync()
  App->>Cache: Remove(AllTemplateListKeys)
  UoW->>DB: Commit

  Client->>API: GET /catalog/templates?tier=...
  API->>App: Send(GetTemplatesQuery)
  App->>Cache: Get(listKey)
  alt cache miss
    App->>DB: SELECT templates WHERE tier
    App->>Cache: Set(listKey, 30m)
  end
  App-->>API: Result<IReadOnlyList<TemplateSummaryResponse>>
  API-->>Client: 200 OK
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

new-module, domain, application, infrastructure, integration-events, migration

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch eternelle-180
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch eternelle-180
  • 🛠️ check conventions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSectionVariant/AddSectionVariantCommandValidator.cs`:
- Line 11: The RuleFor in AddSectionVariantCommandValidator currently uses a
magic number (MaximumLength(100)); update it to reference the domain constant
for the variant length (replace 100 with the domain constant such as
SectionVariant.MaxLength or the appropriate value-object constant that defines
the allowed variant length) so the validator uses the Application-domain
contract; modify the RuleFor(c => c.Variant).NotEmpty().MaximumLength(...) call
to use that domain constant and import the domain type if necessary.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddTemplateTheme/AddTemplateThemeCommandValidator.cs`:
- Around line 10-15: The validator AddTemplateThemeCommandValidator is missing a
conditional rule for the nullable Textures property; add a RuleFor for c =>
c.Textures and guard it with .When(c => c.Textures is not null) (e.g. RuleFor(c
=> c.Textures).NotEmpty().When(c => c.Textures is not null)) so nullable
Textures are only validated when present, following the module convention for
nullable command fields.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CreateTemplate/CreateTemplateCommandHandler.cs`:
- Around line 68-71: The cache eviction loop using
CatalogCacheKeys.AllTemplateListKeys() and cacheService.RemoveAsync(...) should
be treated as best-effort after SaveChangesAsync: wrap the foreach and await
cacheService.RemoveAsync calls in a try/catch so any exceptions do not fail the
command, log the failure (including the exception and which key failed) via the
current logger at warning/error level, and continue; ensure the try/catch is
placed after the call to SaveChangesAsync so persistence remains authoritative
and eviction failures are non-fatal.

In `@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs`:
- Around line 32-33: The constructor (Template) is storing the incoming
IReadOnlyList<string> features by reference which allows external mutation;
instead defensively copy the collection before assigning to the aggregate field
(e.g., create a new List<string> or call ToList/AsReadOnly) so the aggregate
owns its state; update the Template constructor and the other similar assignment
sites noted (the features assignments at the locations corresponding to lines
42-43, 57-58, 63-64) to copy the incoming IEnumerable/IReadOnlyList into a new
internal collection and expose only an IReadOnlyList to preserve aggregate
boundaries.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateId.cs`:
- Line 5: Replace the direct Guid.CreateVersion7() usage in TemplateId.New()
with the repo-standard UUIDv7 generator: call Uuid7.NewUuid7().ToGuid() (from
SourceFlow.Stores.EntityFramework) when constructing the TemplateId in the
TemplateId.New() factory; also scan the other Template*Id types in the same
folder and update their New() factory methods to use Uuid7.NewUuid7().ToGuid()
so all module ID factories use the same SourceFlow UUIDv7 generator.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs`:
- Around line 15-16: TemplateManifestChangedDomainEvent currently stores the
incoming lists by reference (SectionIds and Variants), so external mutations can
change the event payload; in the constructor for
TemplateManifestChangedDomainEvent, defensively copy the incoming collections
into immutable snapshots (e.g. convert to arrays or read-only lists) and assign
those copies to SectionIds and Variants (using null-safe conversion to an empty
array if needed) so the event carries stable data at raise-time.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs`:
- Line 12: The Variant property in TemplateSectionVariant currently suppresses
nullable analysis with "null!" without justification; either remove the
suppression by providing a proper non-null initialization (e.g., set in the
constructor or via a factory) or change the type to nullable (string?) if it can
be legitimately null, or keep the suppression but add a concise comment
explaining why it's safe (e.g., initialized via ORM/deserialization before use).
Update the TemplateSectionVariant class and the Variant property accordingly and
ensure the chosen approach is documented as required by project nullable policy.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariantId.cs`:
- Line 5: The New() factory in TemplateSectionVariantId (public static
TemplateSectionVariantId New()) currently calls Guid.CreateVersion7() directly;
change it to use the UUIDv7 generator provided by
SourceFlow.Stores.EntityFramework instead, adding the appropriate using/import
and NuGet/package reference if it’s not present; apply the same change to the
other Domain ID factories that call Guid.CreateVersion7() (e.g.,
TemplateId.New(), TemplateSupportedSectionId.New(), TemplateThemeId.New(),
UserId.New() and other *Id.cs files) so all Domain IDs are created via the
SourceFlow UUIDv7 helper API rather than Guid.CreateVersion7().

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSectionId.cs`:
- Line 5: Replace the direct Guid.CreateVersion7() call in
TemplateSupportedSectionId.New() with the UUIDv7 factory from
SourceFlow.Stores.EntityFramework: add a reference to the
SourceFlow.Stores.EntityFramework package in the relevant src projects, add the
appropriate using, and change the implementation of
TemplateSupportedSectionId.New() to call the library’s UUIDv7 generator (e.g.,
SourceFlow.Stores.EntityFramework.UuidV7.New() or the library’s documented
static factory) so domain IDs are created via SourceFlow.Stores.EntityFramework
rather than Guid.CreateVersion7().

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs`:
- Around line 12-14: The three domain properties TemplateThemeName Name,
ThemeColors Colors, and ThemeTypography Typography currently use null! without
explanation; add a concise inline comment on each property (or a single comment
above the group) stating why nullable suppression is safe, e.g., "set by
aggregate factory / EF materialization", to satisfy the nullable policy and
document intent in the TemplateTheme class.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ThemeStyling.cs`:
- Around line 5-9: Change the three public primary-record constructors into
sealed records with private constructors and add static Create(...) factory
methods that return a Result<T> to validate tokens before instantiation: make
ThemeColors, ThemeTypography, and ThemeTextures constructors private, implement
public static Result<ThemeColors> Create(IReadOnlyDictionary<string,string>
tokens) (and same for ThemeTypography/ThemeTextures) that enforces token
invariants and returns either a success with new ThemeColors(tokens) or a
failure Result with validation errors; ensure all callers use the Create(...)
factories rather than direct construction.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/CatalogModule.cs`:
- Around line 63-76: The code registers and decorates the concrete
domainEventHandler type, but IdempotentDomainEventHandler<TDomainEvent> expects
to decorate IDomainEventHandler<TDomainEvent>, causing a mismatch; in
AddDomainEventHandlers change the registration to register the service as the
interface type and decorate that interface: for each domainEventHandler resolve
the IDomainEventHandler<> service type using domainEvent (Type domainEvent =
...), call services.TryAddScoped(serviceType, domainEventHandler) to register
the implementation for IDomainEventHandler<domainEvent>, and then call
services.Decorate(serviceType, closedIdempotentHandler) where
closedIdempotentHandler =
typeof(IdempotentDomainEventHandler<>).MakeGenericType(domainEvent); this
ensures the decorator targets IDomainEventHandler<T> rather than the concrete
handler.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/20260527094230_InitialCatalog.cs`:
- Around line 185-186: In the InitialCatalog migration (the Up method of the
20260527094230_InitialCatalog migration) replace occurrences of "now() AT TIME
ZONE 'utc'" used as seed values for timestamptz columns with plain "now()" so
PostgreSQL inserts an actual timestamptz instant rather than interpreting a
timestamp in the session timezone; update the VALUES/INSERT seed statements that
set created_at/updated_at (or other timestamptz seed columns) to use now()
directly.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/IdempotentDomainEventHandler.cs`:
- Line 21: Replace the non-unique handler identity used when constructing
OutboxMessageConsumer in IdempotentDomainEventHandler: instead of
decorated.GetType().Name, use the fully-qualified type name
(decorated.GetType().FullName) with a safe fallback to Name (e.g.,
decorated.GetType().FullName ?? decorated.GetType().Name) so the idempotency key
uses a stable, namespace-qualified consumer identifier.
- Around line 23-30: Currently the code checks OutboxConsumerExistsAsync, then
runs decorated.Handle(domainEvent, ...), then InsertOutboxConsumerAsync, which
allows a race condition; instead perform an atomic claim before executing side
effects by attempting a transactional insert of the outbox consumer marker using
an "INSERT ... ON CONFLICT DO NOTHING" (or equivalent upsert) and check the
affected row count to determine success; only call decorated.Handle(domainEvent,
cancellationToken) when the insert returned 1 (claim succeeded); if the claim
returned 0 (already claimed) return immediately; implement this logic inside
IdempotentDomainEventHandler by replacing the OutboxConsumerExistsAsync +
InsertOutboxConsumerAsync sequence with a single TryClaimOutboxConsumerAsync (or
inline SQL) that returns a bool, then conditionally invoke decorated.Handle.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/ProcessOutboxJob.cs`:
- Line 33: The DB operations ignore Quartz's context.CancellationToken; update
ProcessOutboxJob.Execute to accept and pass context.CancellationToken (ct) into
GetOutboxMessagesAsync and UpdateOutboxMessageAsync, call
connection.BeginTransactionAsync(ct) and transaction.CommitAsync(ct), and change
Dapper calls inside GetOutboxMessagesAsync and UpdateOutboxMessageAsync to use
CommandDefinition with cancellationToken: ct for
QueryAsync<OutboxMessageResponse>(...) and both ExecuteAsync(...) calls so all
DB work respects the provided cancellation token.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSectionVariantConfiguration.cs`:
- Around line 16-21: Replace the magic numbers in HasMaxLength with the
corresponding domain VO constants: for builder.Property(v => v.SectionId) use
the SectionId.MaxLength constant from the domain/value-object that defines
SectionId, and for builder.Property(v => v.Variant) use the Variant (or
TemplateVariant/VariantName) VO MaxLength constant (e.g. Variant.MaxLength);
also update any other HasMaxLength(...) calls in this configuration (the other
occurrences around the builder.Property calls) to reference their owning domain
VO MaxLength constants so schema constraints come from the domain definitions.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSupportedSectionConfiguration.cs`:
- Around line 16-20: Replace the hardcoded .HasMaxLength(50) in
TemplateSupportedSectionConfiguration's builder.Property(s => s.SectionId) with
the domain value-object constant (e.g. .HasMaxLength(SectionId.MaxLength));
ensure you reference the VO class that defines the max length (add the
appropriate using/import for that VO if necessary) so the max length comes from
the domain-owned constant rather than a magic number.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/AddSectionVariantEndpoint.cs`:
- Line 15: The Request record in AddSectionVariantEndpoint.cs uses the domain
enum PriceClass; change the Request DTO to use a presentation-safe type (e.g., a
string or a Presentation enum like PriceClassDto) instead of the domain
PriceClass, then map/translate that DTO value to the domain PriceClass inside
the application boundary or in the endpoint handler before calling domain
services; update the Request declaration (the internal sealed record
Request(...) signature) and add a conversion step (e.g., a mapper or a
switch/Parse method) that converts Presentation value -> domain PriceClass where
AddSectionVariantEndpoint invokes the domain layer.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/AddSupportedSectionEndpoint.cs`:
- Line 15: The Request record currently exposes the domain type SectionId;
change the transport contract in AddSupportedSectionEndpoint.cs so Request uses
a primitive (e.g., Guid SectionId or string SectionId) along with int
DisplayOrder, and update the endpoint handler (the method that currently accepts
Request) to map that primitive to the domain SectionId (e.g.,
SectionId.From(Guid) or new SectionId(guid)) before passing into
application/domain logic; also update any callers/tests to send a Guid and add
validation for empty/invalid GUIDs in the handler.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/CreateTemplateEndpoint.cs`:
- Around line 15-20: The Request record currently exposes the domain enum
TemplateTier; change the Request contract to use a presentation-safe type (e.g.,
string or a local API enum like TemplateTierDto) instead of TemplateTier, update
any callers to populate that presentation type, and add explicit mapping in the
endpoint handler (where you construct the CreateTemplateCommand) to convert the
presentation value to the domain TemplateTier (validating/handling unknown
values) before creating the command; refer to the Request record and the place
that constructs CreateTemplateCommand for where to apply the change and mapping.

In
`@src/Modules/Users/Eternelle.Modules.Users.Infrastructure/Database/Migrations/20260527101020_AddCatalogManagePermission.cs`:
- Around line 28-32: The Down migration is currently destructive because it
unconditionally deletes the permission row; update the Down SQL in the
20260527101020_AddCatalogManagePermission migration (the Migration class's Down
block using migrationBuilder.Sql) to only remove the Admin role mapping (delete
from users.role_permissions where permission_code = 'catalog:manage' and role_id
corresponds to Admin) and do not delete the users.permissions row, or
alternatively add provenance checks before deleting (only remove the permission
row if it was created by this migration); in short, limit the rollback to
removing the Admin mapping via the existing migrationBuilder.Sql call and avoid
deleting pre-existing permission rows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97271b35-d012-454b-8ca5-eace24beb9b3

📥 Commits

Reviewing files that changed from the base of the PR and between 35a10a6 and 0d94173.

📒 Files selected for processing (98)
  • Eternelle.slnx
  • src/API/Eternelle.Api/Eternelle.Api.csproj
  • src/API/Eternelle.Api/Program.cs
  • src/API/Eternelle.Api/modules.catalog.Development.json
  • src/API/Eternelle.Api/modules.catalog.json
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Abstractions/Data/IUnitOfWork.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/AssemblyReference.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Eternelle.Modules.Catalog.Application.csproj
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSectionVariant/AddSectionVariantCommand.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSectionVariant/AddSectionVariantCommandHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSectionVariant/AddSectionVariantCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSupportedSection/AddSupportedSectionCommand.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSupportedSection/AddSupportedSectionCommandHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSupportedSection/AddSupportedSectionCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddTemplateTheme/AddTemplateThemeCommand.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddTemplateTheme/AddTemplateThemeCommandHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddTemplateTheme/AddTemplateThemeCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CatalogCacheKeys.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CreateTemplate/CreateTemplateCommand.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CreateTemplate/CreateTemplateCommandHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CreateTemplate/CreateTemplateCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSectionVariants/GetSectionVariantsQuery.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSectionVariants/GetSectionVariantsQueryHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSupportedSections/GetSupportedSectionsQuery.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSupportedSections/GetSupportedSectionsQueryHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplateById/GetTemplateByIdQuery.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplateById/GetTemplateByIdQueryHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplateById/TemplateDetailResponse.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplates/GetTemplatesQuery.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplates/GetTemplatesQueryHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplates/TemplateSummaryResponse.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/TemplateManifestChangedDomainEventHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/UpdateTemplate/UpdateTemplateCommand.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/UpdateTemplate/UpdateTemplateCommandHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/UpdateTemplate/UpdateTemplateCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/AssemblyInfo.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Eternelle.Modules.Catalog.Domain.csproj
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrl.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Shared/ImageUrlErrors.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ITemplateRepository.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/PriceClass.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateCreatedDomainEvent.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescription.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateDescriptionErrors.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateErrors.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateId.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestItem.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateName.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateNameErrors.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariantId.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSection.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSupportedSectionId.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeId.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeName.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateThemeNameErrors.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTier.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ThemeStyling.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/CatalogModule.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/CatalogDbContext.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Converters/TemplateIdConverter.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Converters/TemplateSectionVariantIdConverter.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Converters/TemplateSupportedSectionIdConverter.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Converters/TemplateThemeIdConverter.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/20260527094230_InitialCatalog.Designer.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/20260527094230_InitialCatalog.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/CatalogDbContextModelSnapshot.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Schemas.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Eternelle.Modules.Catalog.Infrastructure.csproj
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/ConfigureProcessOutboxJob.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/IdempotentDomainEventHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/OutboxOptions.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/ProcessOutboxJob.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateConfiguration.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateRepository.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSectionVariantConfiguration.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSupportedSectionConfiguration.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateThemeConfiguration.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/CatalogTemplateManifestChangedIntegrationEvent.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/Eternelle.Modules.Catalog.IntegrationEvents.csproj
  • src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/SectionVariantManifest.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/AssemblyReference.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Eternelle.Modules.Catalog.Presentation.csproj
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Tags.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/AddSectionVariantEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/AddSupportedSectionEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/AddTemplateThemeEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/CreateTemplateEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/GetSectionVariantsEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/GetTemplateByIdEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/GetTemplatesEndpoint.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Templates/UpdateTemplateEndpoint.cs
  • src/Modules/Users/Eternelle.Modules.Users.Infrastructure/Database/Migrations/20260527101020_AddCatalogManagePermission.Designer.cs
  • src/Modules/Users/Eternelle.Modules.Users.Infrastructure/Database/Migrations/20260527101020_AddCatalogManagePermission.cs

engrkwakwak and others added 3 commits May 27, 2026 18:45
Extended migration logic to include CatalogDbContext alongside WeddingsDbContext and UsersDbContext. Updated imports to reference the Catalog module's DbContext.
…validators

- Add MaxVariantLength constant to TemplateSectionVariant; reference it
  in AddSectionVariantCommandValidator and EF config (removes magic number)
- Add Textures validation rule in AddTemplateThemeCommandValidator (nullable guard)
- Wrap cache eviction loop in try/catch with ILogger in CreateTemplateCommandHandler
- Defensively copy features collection in Template.Create and UpdateDetails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updated TemplateManifestChangedDomainEvent to use collection expressions for property initialization. Added comments on null! usage in TemplateSectionVariant and TemplateTheme for clarity. Adjusted timestamp handling in InitialCatalog migration to use now(). Limited role_permissions deletion to 'Admin' in AddCatalogManagePermission Down migration.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSectionVariantConfiguration.cs (1)

16-19: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace remaining hardcoded max lengths with domain constants.

SectionId and PriceClass still use magic numbers, so aggregate/value-object constraints can drift from persistence mapping.

Suggested fix
         builder.Property(v => v.SectionId)
             .HasConversion<string>()
             .IsRequired()
-            .HasMaxLength(50);
+            .HasMaxLength(SectionId.MaxLength);
...
         builder.Property(v => v.PriceClass)
             .HasConversion<string>()
             .IsRequired()
-            .HasMaxLength(50);
+            .HasMaxLength(PriceClass.MaxLength);

As per coding guidelines: "String HasMaxLength() calls must reference the domain VO const (e.g. PersonName.MaxLength) — no magic numbers."

Also applies to: 25-28

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSectionVariantConfiguration.cs`
around lines 16 - 19, Replace the hardcoded HasMaxLength(50) on SectionId (in
TemplateSectionVariantConfiguration's builder.Property(v => v.SectionId)) and
the similar magic number used for PriceClass with the domain
value-object/constants; update the HasMaxLength calls to reference the
appropriate VO constant (e.g., SectionId.MaxLength and PriceClass.MaxLength or
the actual VO class/property used in the domain) so persistence mapping uses the
canonical domain constraints instead of literal numbers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSectionVariantConfiguration.cs`:
- Around line 16-19: Replace the hardcoded HasMaxLength(50) on SectionId (in
TemplateSectionVariantConfiguration's builder.Property(v => v.SectionId)) and
the similar magic number used for PriceClass with the domain
value-object/constants; update the HasMaxLength calls to reference the
appropriate VO constant (e.g., SectionId.MaxLength and PriceClass.MaxLength or
the actual VO class/property used in the domain) so persistence mapping uses the
canonical domain constraints instead of literal numbers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81c12999-660e-4cc0-a850-2c075687c20f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d94173 and 8169c19.

📒 Files selected for processing (11)
  • src/API/Eternelle.Api/Extensions/MigrationExtensions.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddSectionVariant/AddSectionVariantCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/AddTemplateTheme/AddTemplateThemeCommandValidator.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CreateTemplate/CreateTemplateCommandHandler.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateManifestChangedDomainEvent.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateSectionVariant.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateTheme.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/20260527094230_InitialCatalog.cs
  • src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateSectionVariantConfiguration.cs
  • src/Modules/Users/Eternelle.Modules.Users.Infrastructure/Database/Migrations/20260527101020_AddCatalogManagePermission.cs

@engrkwakwak engrkwakwak self-assigned this May 27, 2026
@engrkwakwak engrkwakwak merged commit 0108e90 into develop May 27, 2026
1 check passed
@engrkwakwak engrkwakwak deleted the eternelle-180 branch May 27, 2026 13:31
This was referenced May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Catalog] Implement Catalog module (Template aggregate, pricing + upload limits) — supersedes #82–#88

1 participant