Develop#182
Conversation
…it.yaml (#178) * Eternelle-176 Strengthen code review and DDD enforcement in .coderabbit.yaml - Refined tone and review instructions for Clean Architecture boundaries - Added granular PR labeling and path-based review rules per layer - Enforced DDD conventions: const MaxLength, event handler dependencies, IDOR guards - Introduced custom automated checks for architectural compliance - Disabled docstring warnings; set PR title requirements - Added C# XML doc comment generation guidance - Enabled additional security and quality tools - Excluded build/IDE artifacts from review; updated SQL schema rules * Update .coderabbit.yaml review checks and docstring mode Revised custom review instructions to enforce new .cs file conventions, focusing on aggregate behavior, domain error handling, handler return types, and cross-module reference types. Replaced previous checks and set docstring pre-merge check mode to warning.
…175) * Eternelle-174 Add Users module with CQRS, EF Core, and Keycloak auth - Introduce Users module (Application, Domain, Infrastructure, IntegrationEvents, Presentation) and register in solution/API - Implement user aggregate, value objects, roles, permissions, and domain events - Add EF Core config, migrations, and schema for users, roles, permissions, outbox/inbox - Provide CQRS handlers/validators for user registration, profile update, and queries - Integrate Users module into API DI, health checks, and endpoints - Add outbox/inbox processing jobs and idempotent event handlers - Update configs for Users module settings and logging - Add Keycloak service to docker-compose for authentication - Refactor common auth/authorization namespaces - Bump Microsoft.AspNetCore.Authentication.JwtBearer version * Eternelle-174 Refactor user module: UserId, EF Core, validation, updates - Switch UserRepository to use UserId value object for type safety - Add Update method to IUserRepository and implementation - Use CommandDefinition with cancellation tokens in Dapper queries - Add UserIdConverter and configure EF Core for UserId conversion - Move Permission/Role max length to constants and use in config - Remove manual UserId conversion from UserConfiguration - Validate required claims in RegisterUser endpoint - Require FirstName/LastName in UpdateUserProfile request model - Improve event handler registration logic for interface matching * Set default Permission value and improve claim validation Set Permission property default to empty string to avoid nulls. Use string.IsNullOrWhiteSpace for stricter identity claim validation in RegisterUser. * Refactor module config, improve message processing - Move module config loading before registration in Program.cs - Add health check updates - Initialize Role.Name to empty string in private ctor - Throw on null/whitespace identityId in User.Create - Remove unused usings in Schemas.cs - Use ON CONFLICT DO NOTHING for inbox inserts - Use FOR UPDATE SKIP LOCKED in inbox/outbox queries - Split processed/error update logic for messages - Register Outbox/Inbox options with validation in UsersModule * feat(weddings): add fail-fast validation for OutboxOptions and InboxOptions * fix(weddings): pin AddDomainEventHandlers and AddIntegrationEventHandlers LINQ predicates to exact generic types * fix(weddings): use handler short name for outbox consumer idempotency key * fix(weddings): use handler short name for inbox consumer idempotency key * Change ArgumentNullException to ArgumentException
…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.
…ixes to initial migration
Store TenantId on AlbumSettings so the upload-limit changed event (which carries TenantId) can locate the right settings row. Adds GetByTenantIdAsync to IAlbumSettingsRepository and a new UpdateUploadLimitCommand. Guest added/removed/lookup-code-regenerated events mirror the invitee list directly via repository injection.
…limit update Drop the IsUnique constraint on settings.tenant_id: a tenant can have multiple weddings, each with its own AlbumSettings row, so a unique index would reject the second seed. UpdateUploadLimitCommandHandler now updates all settings rows for the tenant rather than expecting exactly one.
… in Application and Presentation
Route changes from weddings/{id}/snap-share to weddings/{id}/social-share.
- Switch query handlers to Dapper with cancellation support - Ensure bulk photo commands process only distinct IDs - Add stricter validation for photo IDs in bulk commands - Validate and deduplicate SlotIds before upload - Set ReviewedAt for approved AlbumPhoto on creation - Simplify AlbumSettings EF config, enforce required fields - Pass cancellation tokens in all Dapper operations - Log and drop invalid lookup codes in integration handler - Remove unused GetByUploadTokenAsync from WeddingRepository
Removed explicit column names in InviteeConfiguration to use EF Core's default conventions. Simplified error logging in GuestLookupCodeRegeneratedIntegrationEventHandler by omitting the NewLookupCode value from log messages.
Eternelle-194
There was a problem hiding this comment.
Actionable comments posted: 24
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/API/Eternelle.Api/Extensions/MigrationExtensions.cs (1)
16-20:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd Album DbContext to migration bootstrap.
ApplyMigrations()now includes Rsvp but still skips Album. In Development, Album endpoints will run without schema migration.Suggested fix
+using Eternelle.Modules.Album.Infrastructure.Database; using Eternelle.Modules.Catalog.Infrastructure.Database; using Eternelle.Modules.Rsvp.Infrastructure.Database; using Eternelle.Modules.Tenancy.Infrastructure.Database; using Eternelle.Modules.Users.Infrastructure.Database; using Eternelle.Modules.Weddings.Infrastructure.Database; @@ ApplyMigration<CatalogDbContext>(scope); ApplyMigration<TenancyDbContext>(scope); ApplyMigration<RsvpDbContext>(scope); + ApplyMigration<AlbumDbContext>(scope);🤖 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/API/Eternelle.Api/Extensions/MigrationExtensions.cs` around lines 16 - 20, The migration bootstrap is missing the Album DbContext; update the ApplyMigrations call list to include ApplyMigration<AlbumDbContext>(scope) alongside ApplyMigration<WeddingsDbContext>, ApplyMigration<UsersDbContext>, ApplyMigration<CatalogDbContext>, ApplyMigration<TenancyDbContext>, and ApplyMigration<RsvpDbContext> so the Album schema is applied during startup (locate the ApplyMigration<T> invocations in MigrationExtensions.ApplyMigrations and add the AlbumDbContext entry).src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplates/GetTemplatesQueryHandler.cs (1)
39-40:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPass
Tieras numeric, not string.At Line 44,
query.Tier?.ToString()is passed tot.tier(integer), which can break the optional filter with a DB type mismatch.Proposed fix
- IEnumerable<TemplateSummaryResponse> rows = await connection.QueryAsync<TemplateSummaryResponse>( - new CommandDefinition(sql, new { Tier = query.Tier?.ToString() }, cancellationToken: cancellationToken)); + IEnumerable<TemplateSummaryResponse> rows = await connection.QueryAsync<TemplateSummaryResponse>( + new CommandDefinition( + sql, + new { Tier = query.Tier is null ? null : (int?)query.Tier }, + cancellationToken: cancellationToken));Also applies to: 44-44
🤖 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.Application/Templates/GetTemplates/GetTemplatesQueryHandler.cs` around lines 39 - 40, The SQL filter passes t.tier an incorrect string value — replace query.Tier?.ToString() with the numeric nullable (query.Tier) so the DB parameter matches the integer column; in GetTemplatesQueryHandler (GetTemplatesQuery) ensure the parameter bound to t.tier is the nullable int (query.Tier) rather than a string and, if your DB-binding requires, pass DBNull when null or the int value when present.src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSectionVariants/GetSectionVariantsQueryHandler.cs (1)
33-37:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse numeric
SectionIdfor the DB filter.At Line 36,
query.SectionId.ToString()sends a string whilesection_idis persisted asinteger; this can break filtering and return wrong/empty results. Pass the enum underlying int instead.Proposed fix
- IEnumerable<SectionVariantResponse> rows = await connection.QueryAsync<SectionVariantResponse>( - new CommandDefinition( - sql, - new { query.TemplateId, SectionId = query.SectionId.ToString() }, - cancellationToken: cancellationToken)); + IEnumerable<SectionVariantResponse> rows = await connection.QueryAsync<SectionVariantResponse>( + new CommandDefinition( + sql, + new { query.TemplateId, SectionId = (int)query.SectionId }, + cancellationToken: cancellationToken));🤖 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.Application/Templates/GetSectionVariants/GetSectionVariantsQueryHandler.cs` around lines 33 - 37, The DB filter is sending query.SectionId.ToString() which passes a string to the integer column; update the anonymous parameter passed to Dapper in GetSectionVariantsQueryHandler (the object used in connection.QueryAsync) to supply the enum's underlying numeric value (e.g. cast query.SectionId to int) for SectionId so the SQL binds against the integer section_id correctly and returns expected rows.src/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/IUploadSlotStore.cs (1)
20-27: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winClarify duplicate-slot behavior in
RedeemManyAsynccontract.Line 20-27 leaves duplicate
slotIdsundefined, but the return type is keyed byGuid, so duplicates are ambiguous. Explicitly define duplicates as invalid and returnnull(without consuming any slot) to preserve single-use semantics.♻️ Suggested contract clarification
/// Atomically validates and redeems all <paramref name="slotIds"/> in a single operation. + /// Duplicate slot IDs are invalid input and must fail atomically (no slots consumed). /// Returns a dictionary mapping each slot ID to its CDN URL if every slot is valid, /// or <see langword="null"/> if any slot is missing or already redeemed — in which case /// no slots are consumed.Based on learnings: duplicate slot IDs in a batch must be rejected before calling
RedeemManyAsync.🤖 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/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/IUploadSlotStore.cs` around lines 20 - 27, The XML doc for IUploadSlotStore.RedeemManyAsync currently omits behavior for duplicate slotIds; update the method's summary to explicitly state that duplicate GUIDs in the slotIds list are considered invalid and will cause the method to return null (and consume no slots) to preserve single-use semantics; mention this contract in the RedeemManyAsync documentation so callers and implementors (IUploadSlotStore and RedeemManyAsync) reject duplicate entries before attempting redemption.src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Storage/StubUploadSlotStore.cs (1)
32-60:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStub
RedeemManyAsyncshould reject duplicate slot IDs to match Redis contract.This implementation accepts duplicate IDs, while the Redis store returns
nullfor that case. Keep behavior identical across implementations to avoid environment-specific bugs.💡 Suggested fix
public Task<IReadOnlyDictionary<Guid, string>?> RedeemManyAsync( IReadOnlyList<Guid> slotIds, CancellationToken cancellationToken) { if (slotIds.Count == 0) { return Task.FromResult<IReadOnlyDictionary<Guid, string>?>(new Dictionary<Guid, string>(0)); } + + if (new HashSet<Guid>(slotIds).Count != slotIds.Count) + { + return Task.FromResult<IReadOnlyDictionary<Guid, string>?>(null); + } lock (_redeemLock) {Based on learnings: duplicate slot IDs in a batch must be rejected before calling
RedeemManyAsync.🤖 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/Album/Eternelle.Modules.Album.Infrastructure/Storage/StubUploadSlotStore.cs` around lines 32 - 60, RedeemManyAsync currently allows duplicate slotIds, but the Redis contract requires rejecting batches with duplicate IDs and returning null; update RedeemManyAsync (inside the method that locks _redeemLock and iterates slotIds) to first detect duplicates in the incoming slotIds (e.g., by comparing slotIds.Count to a HashSet count) and immediately return Task.FromResult<IReadOnlyDictionary<Guid,string>?>(null) when duplicates are present before any checks or removals against _slots; keep the existing all-or-nothing existence check and removal logic using _slots and _redeemLock unchanged otherwise.
♻️ Duplicate comments (3)
docs/architecture.md (1)
373-373:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the stray fenced-code marker at Line 373.
This dangling ``` triggers markdownlint (MD031/MD040) and breaks document formatting; delete it (or convert to a valid fenced block with language and surrounding blank lines).
🤖 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 `@docs/architecture.md` at line 373, Remove the stray fenced-code marker consisting of three backticks (```) that appears alone in the document and is triggering markdownlint MD031/MD040; either delete that lone ``` or convert it into a proper fenced code block by adding a language identifier and surrounding blank lines so it becomes a valid block (e.g., ```js with content and a closing ```).src/Common/Eternelle.Common.Domain/People/EmailAddressErrors.cs (1)
1-1:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winImport
Eternelle.Common.DomainsoErrorresolves.
Erroris referenced in this file without the corresponding namespace import, which can fail compilation.🔧 Proposed fix
+using Eternelle.Common.Domain; + namespace Eternelle.Common.Domain.People;#!/bin/bash set -euo pipefail # Verify whether Common.Domain is globally imported in this project rg -n --type=cs 'global\s+using\s+Eternelle\.Common\.Domain\s*;' src/Common/Eternelle.Common.Domain # Inspect imports/usages in the affected file sed -n '1,30p' src/Common/Eternelle.Common.Domain/People/EmailAddressErrors.cs🤖 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/Common/Eternelle.Common.Domain/People/EmailAddressErrors.cs` at line 1, Add the missing namespace import so the Error type resolves: in the Eternelle.Common.Domain.People file (namespace Eternelle.Common.Domain.People) add a using/import for Eternelle.Common.Domain so references to Error compile; locate the file where the Error type is used (e.g., in any EmailAddressErrors declarations) and add the single-line import for Eternelle.Common.Domain at the top of the file.src/Common/Eternelle.Common.Domain/People/EmailAddress.cs (1)
1-1:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winAdd the missing domain import for
Result<T>.
Resultis used in this value object butEternelle.Common.Domainis not imported in this file, which can break compilation.🔧 Proposed fix
+using Eternelle.Common.Domain; + namespace Eternelle.Common.Domain.People;#!/bin/bash set -euo pipefail # Verify whether Common.Domain is globally imported in this project rg -n --type=cs 'global\s+using\s+Eternelle\.Common\.Domain\s*;' src/Common/Eternelle.Common.Domain # Inspect imports/usages in the affected file sed -n '1,45p' src/Common/Eternelle.Common.Domain/People/EmailAddress.cs🤖 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/Common/Eternelle.Common.Domain/People/EmailAddress.cs` at line 1, The file EmailAddress.cs uses Result<T> but is missing the domain import; add the missing using for Eternelle.Common.Domain at the top of EmailAddress.cs (or replace usages with the fully-qualified type) so references to Result<T> in the EmailAddress value object resolve correctly; look for the EmailAddress type and any methods returning Result<T> to verify compilation.
🤖 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/API/Eternelle.Api/Eternelle.Api.csproj`:
- Line 30: Remove the direct ProjectReference to
Eternelle.Modules.Rsvp.Presentation.csproj from the API csproj and instead
reference the module's Infrastructure/bootstrap project used for host wiring;
specifically delete the ProjectReference line that includes
"Eternelle.Modules.Rsvp.Presentation.csproj" and add a ProjectReference to the
module's Infrastructure/bootstrap project (e.g., the
Eternelle.Modules.Rsvp.Infrastructure/bootstrap project used for module
registration) so the API only depends on Infrastructure-level assemblies.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Invitees/DeleteOwnPhoto/DeleteOwnPhotoCommandHandler.cs`:
- Line 17: Extract the repeated normalization logic into a shared extension
method and update handlers to use it: create an internal static class
LookupCodeExtensions with a Normalize(this string lookupCode) method that
returns lookupCode.Replace("-", string.Empty).Trim().ToUpperInvariant(), then
replace direct uses of .Replace(...).Trim().ToUpperInvariant() in
DeleteOwnPhotoCommandHandler, GenerateGuestUploadSlotsCommandHandler, and
GetUploadContextQueryHandler with call to LookupCodeExtensions.Normalize (e.g.,
command.LookupCode.Normalize()) to ensure consistent behavior and remove
duplication.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GenerateGuestUploadSlots/GenerateGuestUploadSlotsCommandValidator.cs`:
- Line 10: Replace the hardcoded upper bound 30 in
GenerateGuestUploadSlotsCommandValidator's RuleFor(c =>
c.Count).InclusiveBetween(1, 30) with a named domain constant; add a constant
such as AlbumConstants.MaxUploadSlotsPerRequest (or
AlbumUploadService.MaxSlotsPerRequest) in the Application/domain layer and
reference that constant in the validator so the rule becomes InclusiveBetween(1,
AlbumConstants.MaxUploadSlotsPerRequest), ensuring the single source of truth
for max slots.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkRejectPhotos/BulkRejectPhotosCommandValidator.cs`:
- Around line 10-11: The validator currently allows an unbounded PhotoIds list;
update BulkRejectPhotosCommandValidator to enforce a hard upper limit (e.g.
MAX_BATCH_SIZE = 100) by adding a check on PhotoIds length (for example via
RuleFor(c => c.PhotoIds).Must(list => list != null && list.Count <=
MAX_BATCH_SIZE).WithMessage(...)) while keeping the per-item RuleForEach(c =>
c.PhotoIds).NotEqual(Guid.Empty); declare the MAX_BATCH_SIZE constant inside the
validator and use it in the validation message so large requests are rejected
before handler DB work begins.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/GetFeedQuery.cs`:
- Around line 5-8: Add a FluentValidation validator class named
GetFeedQueryValidator for the GetFeedQuery record that enforces Page >= 1 and a
bounded PageSize (e.g., 1 <= PageSize <= MAX_PAGE_SIZE where MAX_PAGE_SIZE is a
defined constant); wire the validator into DI and ensure MediatR's validation
behavior (ValidationBehavior<TRequest,TResponse>) is registered so validators
run automatically for IQuery handlers. Locate GetFeedQuery (symbols:
GetFeedQuery, Page, PageSize) and implement the rules in GetFeedQueryValidator,
define or reuse a MAX_PAGE_SIZE constant, and confirm the application's service
registration includes the FluentValidation validators and MediatR validation
pipeline behavior.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/GetFeedQueryHandler.cs`:
- Around line 29-33: The SQL in GetFeedQueryHandler hardcodes photo status as
`status = 1`; change the query to use a bound parameter (e.g., `status =
`@Status``) and pass the enum-backed value `PhotoStatus.Approved` from the handler
when executing the query (update the parameter list used by the query execution
in GetFeedQueryHandler / GetFeedQuery to include Status). Ensure both places
noted (lines ~29-33 and ~37-43) use the same parameterized filter so the feed
respects the domain enum rather than a magic number.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetPhotos/GetPhotosQuery.cs`:
- Around line 7-12: GetPhotosQuery currently exposes Page and PageSize without
validation which can lead to invalid SQL paging or oversized reads; create a
GetPhotosQueryValidator (using FluentValidation) that enforces Page >= 1 and
PageSize within an allowed range (e.g. 1..100 or project-specific min/max) and
validates optional enums (Status, Source) as needed, then register the validator
with MediatR pipeline behaviors so queries are validated before reaching the
Dapper handler that returns IReadOnlyList<PhotoItem>.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Settings/GetSettings/GetSettingsQueryHandler.cs`:
- Line 27: Replace the hardcoded "album.settings" table reference in
GetSettingsQueryHandler with the module schema constant (Schemas.Album) so the
SQL builds the table name from the shared schema constant; locate the SQL string
in GetSettingsQueryHandler and change the table reference to use the
Schemas.Album prefix (e.g. construct the identifier via string interpolation or
concatenation using Schemas.Album + ".settings") so the module schema constant
is used instead of the literal "album".
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateSettings/UpdateSettingsCommandValidator.cs`:
- Line 9: UpdateSettingsCommandValidator currently only validates WeddingId but
allows unknown enum integers for ModerationMode; add an explicit enum validation
on the ModerationMode property (use RuleFor(c => c.ModerationMode).IsInEnum())
in the UpdateSettingsCommandValidator so requests with invalid ModerationMode
values are rejected before reaching the aggregate.
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateUploadLimit/UpdateUploadLimitCommand.cs`:
- Line 5: Create a FluentValidation AbstractValidator for
UpdateUploadLimitCommand (e.g., UpdateUploadLimitCommandValidator :
AbstractValidator<UpdateUploadLimitCommand>) and add rules to ensure TenantId is
not Guid.Empty (RuleFor(c => c.TenantId).NotEmpty().WithMessage(...)) and that
NewLimit, when present, is non-negative (RuleFor(c =>
c.NewLimit).GreaterThanOrEqualTo(0).When(c =>
c.NewLimit.HasValue).WithMessage(...)); place the validator alongside the
command so it follows the project convention for one-validator-per-command and
will be discovered by the DI/validation pipeline.
In `@src/Modules/Album/Eternelle.Modules.Album.Domain/Invitees/Invitee.cs`:
- Line 9: Add the explanatory comment to the private parameterless constructor
to mark it as the EF Core materialization constructor: locate the Invitee class
and its private parameterless constructor named Invitee(), and change the empty
constructor to include the comment "// EF constructor" so the pattern is
documented once instead of using = null! suppressions per-property.
In `@src/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhoto.cs`:
- Line 9: Add the conventional EF constructor comment to the private
parameterless constructor in the AlbumPhoto class: locate the private
AlbumPhoto() { } ctor in class AlbumPhoto and add a trailing comment "// EF
constructor" to document that it exists solely for EF Core materialization,
maintaining consistency with the property null-forgiving pattern used elsewhere.
In
`@src/Modules/Album/Eternelle.Modules.Album.Domain/Photos/IAlbumPhotoRepository.cs`:
- Line 8: Remove the persistence+business method InsertManyAndEnforceAsync from
IAlbumPhotoRepository and move quota enforcement into the application layer:
update the repository interface to only expose persistence operations (e.g.,
InsertMany/InsertManyAsync and CountActiveByWeddingIdAsync) and in the command
handler/aggregate that currently calls InsertManyAndEnforceAsync perform the
check using CountActiveByWeddingIdAsync(weddingId, cancellationToken) and
compare currentCount + photos.Count to the limit, returning
AlbumPhotoErrors.UploadLimitExceeded on violation before calling
InsertMany(photos) (or InsertManyAsync). Ensure all callers of
InsertManyAndEnforceAsync are refactored to this new two-step flow.
In
`@src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/IdempotentIntegrationEventHandler.cs`:
- Around line 37-58: The SQL strings hardcode the "album" schema; update both
SQL constants (the SELECT EXISTS used with ExecuteScalarAsync<bool> and the
INSERT in InsertInboxConsumerAsync) to use the module schema constant instead of
"album" by converting the raw string literals to interpolated raw strings
(prefix with $) and inject Schemas.Album (e.g. {Schemas.Album}) as the schema
prefix; ensure both the SELECT and INSERT statements reference the schema via
Schemas.Album and keep parameter names (`@InboxMessageId`, `@Name`) unchanged.
In
`@src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/IntegrationEventConsumer.cs`:
- Around line 30-35: The INSERT SQL uses a hardcoded "album" schema; update the
SQL in IntegrationEventConsumer (the const sql) to use the module schema
constant Schemas.Album instead of the literal "album" — e.g. make the string an
interpolated/raw string that inserts {Schemas.Album} as the schema prefix for
inbox_messages (ensure proper quoting/format for $"""...{Schemas.Album}...""" or
equivalent) so the query becomes INSERT INTO {Schemas.Album}.inbox_messages(...)
ON CONFLICT (id) DO NOTHING.
In
`@src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/ProcessInboxJob.cs`:
- Around line 77-87: Replace the hardcoded "album" schema literal in the SQL
strings with the module schema constant by using Schemas.Album; update the
SELECT query built in ProcessInboxJob (the local variable sql that selects into
InboxMessageResponse and uses inboxOptions.Value.BatchSize) and any other inbox
SQL statements referenced later in this file (the fetch and update statements
around the 104-124 region) so they use the Schemas.Album prefix instead of the
"album." literal; ensure you preserve the interpolated column aliases
(nameof(InboxMessageResponse.Id), nameof(InboxMessageResponse.Content)) and the
FOR UPDATE SKIP LOCKED / LIMIT clauses while replacing the schema prefix
consistently across all inbox queries in this class.
In
`@src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Migrations/20260531065202_InitialAlbum.cs`:
- Around line 159-166: Remove the inline pragmas around the CreateIndex call in
the InitialAlbum migration (the `#pragma` warning disable CA1861 / `#pragma` warning
restore CA1861 lines that wrap the migrationBuilder.CreateIndex for
idx_album_photos_wedding_status_uploaded) and instead add a rule to
.editorconfig to suppress this analyzer globally for the project (e.g. add
dotnet_diagnostic.CA1861.severity = none or desired severity under the
appropriate [*.cs] section). Ensure the migration source contains no `#pragma`
suppression and that the .editorconfig entry targets CA1861 so the build
behavior remains the same.
In
`@src/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/BulkDeletePhotosEndpoint.cs`:
- Around line 18-21: The MapDelete endpoint's complex parameter Request (record
Request(IReadOnlyList<Guid> PhotoIds)) is not explicitly bound from the request
body, so PhotoIds may not deserialize; update the endpoint signature in the
MapDelete call to bind the payload from the body (e.g., annotate the Request
parameter with [FromBody] or use the appropriate body-binding attribute) so that
the Request/PhotoIds are correctly read from the JSON body when calling the
endpoint.
In
`@src/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/GetFeedEndpoint.cs`:
- Around line 17-37: The GET feed endpoint (MapGet for
"weddings/{weddingId:guid}/album/feed" implemented in GetFeedEndpoint) is
currently AllowAnonymous and must be gated; remove or replace .AllowAnonymous()
and require authorization with the album management policy (e.g., call
RequireAuthorization with the "album:manage" policy) so only authorized users
can access feeds, or alternatively implement lookup-code gating by adding a
lookupCode parameter to the route and validating it inside the handler before
returning the feed (validate via existing auth/lookup services and return
unauthorized/forbidden on failure).
In
`@src/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestAddedIntegrationEventHandler.cs`:
- Around line 12-16: The handler GuestAddedIntegrationEventHandler currently
throws InvalidOperationException for deterministic payload validation failures
(the checks at the spots corresponding to lines ~36, ~42, ~48); instead of
throwing, replace those throws with structured logging of the validation failure
and an explicit non-retry/dead-letter path via the inbox pattern: log the
failure (include event id and validation details) and call the inbox/idempotency
API (e.g. use the existing unitOfWork inbox processing helper such as
ProcessInboxJob or the project’s method for marking an inbox message as
completed/dead-letter) so the event is recorded as processed/non-retriable and
the handler returns normally; do this inside
GuestAddedIntegrationEventHandler.Handle (or the handler method) for each
validation branch that currently throws.
In
`@src/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestLookupCodeRegeneratedIntegrationEventHandler.cs`:
- Around line 24-28: The handler in
GuestLookupCodeRegeneratedIntegrationEventHandler currently returns when the
local Invitee projection is null (variable invitee), which drops out-of-order
events; instead, make the case retryable or persist the event as a pending
update keyed by GuestId: in the Handle method, if invitee is null either throw a
retryable exception so the message infrastructure re-delivers, or call a
PendingGuestUpdateRepository/IGuestPendingUpdateStore.SavePending(GuestId,
eventPayload) to stage the update; ensure a reconciliation step (e.g., when
GuestAdded is applied) reads and applies pending updates and then clears them to
guarantee eventual convergence.
In
`@src/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestRemovedIntegrationEventHandler.cs`:
- Around line 19-27: The handler currently returns early when
inviteeRepository.GetByGuestIdAsync(...) returns null, which skips
photoRepository.NullifyGuestIdAsync(...) and leaves photos linked to removed
guests; change the flow so
photoRepository.NullifyGuestIdAsync(integrationEvent.GuestId, cancellationToken)
is always awaited regardless of invitee presence (e.g., call it before checking
invitee or remove the early return and ensure it runs), then if Invitee invitee
is found call inviteeRepository.Remove(invitee) and finally call
unitOfWork.SaveChangesAsync(cancellationToken) to persist photo nullification
and any invitee removal.
In `@src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cs`:
- Around line 17-18: The enum SectionId was modified by inserting SocialShare
before GuestUploads which changes numeric ordinals and can corrupt persisted
data; either move SocialShare to the end of the enum or freeze existing values
by adding explicit integer assignments for all members (e.g., give GuestUploads
and earlier members explicit numeric values) so existing persisted integers
retain the same meaning; refer to the SectionId enum and the SocialShare and
GuestUploads members when making the change.
In
`@src/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/GetGuest/GetGuestQuery.cs`:
- Line 5: Add a FluentValidation validator for the query by creating an
AbstractValidator<GetGuestQuery> that validates WeddingId and GuestId with
NotEmpty rules and an appropriate error message; name the class
GetGuestQueryValidator and place it alongside GetGuestQuery so it is discovered,
then ensure the validator is registered in the MediatR validation pipeline (or
with the DI container where other validators/behaviors are registered) so the
validator runs before handlers.
---
Outside diff comments:
In `@src/API/Eternelle.Api/Extensions/MigrationExtensions.cs`:
- Around line 16-20: The migration bootstrap is missing the Album DbContext;
update the ApplyMigrations call list to include
ApplyMigration<AlbumDbContext>(scope) alongside
ApplyMigration<WeddingsDbContext>, ApplyMigration<UsersDbContext>,
ApplyMigration<CatalogDbContext>, ApplyMigration<TenancyDbContext>, and
ApplyMigration<RsvpDbContext> so the Album schema is applied during startup
(locate the ApplyMigration<T> invocations in MigrationExtensions.ApplyMigrations
and add the AlbumDbContext entry).
In
`@src/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/IUploadSlotStore.cs`:
- Around line 20-27: The XML doc for IUploadSlotStore.RedeemManyAsync currently
omits behavior for duplicate slotIds; update the method's summary to explicitly
state that duplicate GUIDs in the slotIds list are considered invalid and will
cause the method to return null (and consume no slots) to preserve single-use
semantics; mention this contract in the RedeemManyAsync documentation so callers
and implementors (IUploadSlotStore and RedeemManyAsync) reject duplicate entries
before attempting redemption.
In
`@src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Storage/StubUploadSlotStore.cs`:
- Around line 32-60: RedeemManyAsync currently allows duplicate slotIds, but the
Redis contract requires rejecting batches with duplicate IDs and returning null;
update RedeemManyAsync (inside the method that locks _redeemLock and iterates
slotIds) to first detect duplicates in the incoming slotIds (e.g., by comparing
slotIds.Count to a HashSet count) and immediately return
Task.FromResult<IReadOnlyDictionary<Guid,string>?>(null) when duplicates are
present before any checks or removals against _slots; keep the existing
all-or-nothing existence check and removal logic using _slots and _redeemLock
unchanged otherwise.
In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSectionVariants/GetSectionVariantsQueryHandler.cs`:
- Around line 33-37: The DB filter is sending query.SectionId.ToString() which
passes a string to the integer column; update the anonymous parameter passed to
Dapper in GetSectionVariantsQueryHandler (the object used in
connection.QueryAsync) to supply the enum's underlying numeric value (e.g. cast
query.SectionId to int) for SectionId so the SQL binds against the integer
section_id correctly and returns expected rows.
In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplates/GetTemplatesQueryHandler.cs`:
- Around line 39-40: The SQL filter passes t.tier an incorrect string value —
replace query.Tier?.ToString() with the numeric nullable (query.Tier) so the DB
parameter matches the integer column; in GetTemplatesQueryHandler
(GetTemplatesQuery) ensure the parameter bound to t.tier is the nullable int
(query.Tier) rather than a string and, if your DB-binding requires, pass DBNull
when null or the int value when present.
---
Duplicate comments:
In `@docs/architecture.md`:
- Line 373: Remove the stray fenced-code marker consisting of three backticks
(```) that appears alone in the document and is triggering markdownlint
MD031/MD040; either delete that lone ``` or convert it into a proper fenced code
block by adding a language identifier and surrounding blank lines so it becomes
a valid block (e.g., ```js with content and a closing ```).
In `@src/Common/Eternelle.Common.Domain/People/EmailAddress.cs`:
- Line 1: The file EmailAddress.cs uses Result<T> but is missing the domain
import; add the missing using for Eternelle.Common.Domain at the top of
EmailAddress.cs (or replace usages with the fully-qualified type) so references
to Result<T> in the EmailAddress value object resolve correctly; look for the
EmailAddress type and any methods returning Result<T> to verify compilation.
In `@src/Common/Eternelle.Common.Domain/People/EmailAddressErrors.cs`:
- Line 1: Add the missing namespace import so the Error type resolves: in the
Eternelle.Common.Domain.People file (namespace Eternelle.Common.Domain.People)
add a using/import for Eternelle.Common.Domain so references to Error compile;
locate the file where the Error type is used (e.g., in any EmailAddressErrors
declarations) and add the single-line import for Eternelle.Common.Domain at the
top of the file.
🪄 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: 8a222f22-4739-41d0-95a7-58cbf3cfa920
📒 Files selected for processing (195)
CLAUDE.mdEternelle.slnxREADME.mddocs/architecture.mddocs/modules/README.mdsrc/API/Eternelle.Api/Eternelle.Api.csprojsrc/API/Eternelle.Api/Extensions/MigrationExtensions.cssrc/API/Eternelle.Api/Program.cssrc/API/Eternelle.Api/appsettings.Development.jsonsrc/API/Eternelle.Api/modules.album.Development.jsonsrc/API/Eternelle.Api/modules.album.jsonsrc/API/Eternelle.Api/modules.rsvp.Development.jsonsrc/API/Eternelle.Api/modules.rsvp.jsonsrc/API/Eternelle.Api/modules.weddings.Development.jsonsrc/API/Eternelle.Api/modules.weddings.jsonsrc/Common/Eternelle.Common.Application/Data/IDbConnectionFactory.cssrc/Common/Eternelle.Common.Domain/AssemblyInfo.cssrc/Common/Eternelle.Common.Domain/People/EmailAddress.cssrc/Common/Eternelle.Common.Domain/People/EmailAddressErrors.cssrc/Common/Eternelle.Common.Domain/People/PersonFirstName.cssrc/Common/Eternelle.Common.Domain/People/PersonFirstNameErrors.cssrc/Common/Eternelle.Common.Domain/People/PersonLastName.cssrc/Common/Eternelle.Common.Domain/People/PersonLastNameErrors.cssrc/Common/Eternelle.Common.Domain/People/PersonName.cssrc/Common/Eternelle.Common.Domain/People/PersonNameErrors.cssrc/Common/Eternelle.Common.Domain/People/PhoneNumber.cssrc/Common/Eternelle.Common.Domain/People/PhoneNumberErrors.cssrc/Common/Eternelle.Common.Domain/Text/GroupLabel.cssrc/Common/Eternelle.Common.Domain/Text/GroupLabelErrors.cssrc/Common/Eternelle.Common.Domain/Text/InternalNote.cssrc/Common/Eternelle.Common.Domain/Text/InternalNoteErrors.cssrc/Common/Eternelle.Common.Domain/Text/RichDescription.cssrc/Common/Eternelle.Common.Domain/Text/RichDescriptionErrors.cssrc/Common/Eternelle.Common.Infrastructure/Data/DbConnectionFactory.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Data/IUnitOfWork.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/IPhotoStorageService.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/IUploadSlotStore.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/PhotoStorageOptions.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Abstractions/Storage/PresignedUploadSlot.cssrc/Modules/Album/Eternelle.Modules.Album.Application/AssemblyInfo.cssrc/Modules/Album/Eternelle.Modules.Album.Application/AssemblyReference.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Eternelle.Modules.Album.Application.csprojsrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/DeleteOwnPhoto/DeleteOwnPhotoCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/DeleteOwnPhoto/DeleteOwnPhotoCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/DeleteOwnPhoto/DeleteOwnPhotoCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GenerateGuestUploadSlots/GenerateGuestUploadSlotsCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GenerateGuestUploadSlots/GenerateGuestUploadSlotsCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GenerateGuestUploadSlots/GenerateGuestUploadSlotsCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GetUploadContext/GetUploadContextQuery.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GetUploadContext/GetUploadContextQueryHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GetUploadContext/GetUploadContextResponse.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/RegisterGuestPhotos/RegisterGuestPhotosCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/RegisterGuestPhotos/RegisterGuestPhotosCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/RegisterGuestPhotos/RegisterGuestPhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/AlbumPhotoApprovedDomainEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/AlbumPhotoUploadedDomainEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/ApprovePhoto/ApprovePhotoCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/ApprovePhoto/ApprovePhotoCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/ApprovePhoto/ApprovePhotoCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkApprovePhotos/BulkApprovePhotosCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkApprovePhotos/BulkApprovePhotosCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkApprovePhotos/BulkApprovePhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkDeletePhotos/BulkDeletePhotosCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkDeletePhotos/BulkDeletePhotosCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkDeletePhotos/BulkDeletePhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkRejectPhotos/BulkRejectPhotosCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkRejectPhotos/BulkRejectPhotosCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkRejectPhotos/BulkRejectPhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/DeletePhoto/DeletePhotoCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/DeletePhoto/DeletePhotoCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/DeletePhoto/DeletePhotoCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GenerateHostUploadSlots/GenerateHostUploadSlotsCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GenerateHostUploadSlots/GenerateHostUploadSlotsCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GenerateHostUploadSlots/GenerateHostUploadSlotsCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/FeedItem.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/GetFeedQuery.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/GetFeedQueryHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetPhotos/GetPhotosQuery.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetPhotos/GetPhotosQueryHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetPhotos/PhotoItem.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/RegisterHostPhotos/RegisterHostPhotosCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/RegisterHostPhotos/RegisterHostPhotosCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/RegisterHostPhotos/RegisterHostPhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/RejectPhoto/RejectPhotoCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/RejectPhoto/RejectPhotoCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/RejectPhoto/RejectPhotoCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/GetSettings/AlbumSettingsResponse.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/GetSettings/GetSettingsQuery.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/GetSettings/GetSettingsQueryHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/SeedAlbumSettings/SeedAlbumSettingsCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/SeedAlbumSettings/SeedAlbumSettingsCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateSettings/UpdateSettingsCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateSettings/UpdateSettingsCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateSettings/UpdateSettingsCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateUploadLimit/UpdateUploadLimitCommand.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateUploadLimit/UpdateUploadLimitCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Uploads/AlbumUploadService.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Uploads/IAlbumUploadService.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/AssemblyInfo.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Eternelle.Modules.Album.Domain.csprojsrc/Modules/Album/Eternelle.Modules.Album.Domain/Invitees/IInviteeRepository.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Invitees/Invitee.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhoto.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhotoApprovedDomainEvent.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhotoErrors.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhotoId.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhotoRejectedDomainEvent.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/AlbumPhotoUploadedDomainEvent.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/IAlbumPhotoRepository.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/PhotoSource.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Photos/PhotoStatus.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Schemas.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Settings/AlbumSettings.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Settings/IAlbumSettingsRepository.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Settings/ModerationMode.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Shared/ImageUrl.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Shared/ImageUrlErrors.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Shared/LookupCode.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Shared/LookupCodeErrors.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/AlbumModule.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Database/AlbumDbContext.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Database/Converters/AlbumPhotoIdConverter.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Eternelle.Modules.Album.Infrastructure.csprojsrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/ConfigureProcessInboxJob.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/IdempotentIntegrationEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/InboxOptions.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/IntegrationEventConsumer.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Inbox/ProcessInboxJob.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Invitees/InviteeConfiguration.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Invitees/InviteeRepository.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Migrations/20260531065202_InitialAlbum.Designer.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Migrations/20260531065202_InitialAlbum.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Migrations/AlbumDbContextModelSnapshot.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Outbox/ConfigureProcessOutboxJob.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Outbox/IdempotentDomainEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Outbox/OutboxOptions.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Outbox/ProcessOutboxJob.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Photos/AlbumPhotoConfiguration.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Photos/AlbumPhotoRepository.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Settings/AlbumSettingsConfiguration.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Settings/AlbumSettingsRepository.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Storage/RedisUploadSlotStore.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Storage/S3PhotoStorageService.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Storage/StubPhotoStorageService.cssrc/Modules/Album/Eternelle.Modules.Album.Infrastructure/Storage/StubUploadSlotStore.cssrc/Modules/Album/Eternelle.Modules.Album.IntegrationEvents/Eternelle.Modules.Album.IntegrationEvents.csprojsrc/Modules/Album/Eternelle.Modules.Album.IntegrationEvents/Photos/AlbumPhotoApprovedIntegrationEvent.cssrc/Modules/Album/Eternelle.Modules.Album.IntegrationEvents/Photos/AlbumPhotoUploadedIntegrationEvent.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/AssemblyReference.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Eternelle.Modules.Album.Presentation.csprojsrc/Modules/Album/Eternelle.Modules.Album.Presentation/Permissions.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/ApprovePhotoEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/BulkApprovePhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/BulkDeletePhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/BulkRejectPhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/DeletePhotoEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/GenerateHostUploadSlotsEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/GetFeedEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/GetPhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/RegisterHostPhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/RejectPhotoEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Public/DeleteOwnPhotoEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Public/GenerateGuestUploadSlotsEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Public/GetUploadContextEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Public/RegisterGuestPhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/RateLimitingPolicies.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestAddedIntegrationEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestLookupCodeRegeneratedIntegrationEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestRemovedIntegrationEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Settings/GetSettingsEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Settings/UpdateSettingsEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Tags.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Tenancy/TenantUploadLimitChangedIntegrationEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Weddings/WeddingCreatedIntegrationEventHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSectionVariants/GetSectionVariantsQueryHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetSupportedSections/GetSupportedSectionsQueryHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplateById/GetTemplateByIdQueryHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/GetTemplates/GetTemplatesQueryHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/SectionId.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/20260529115411_InitialCatalog.Designer.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/20260529115411_InitialCatalog.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Database/Migrations/CatalogDbContextModelSnapshot.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Outbox/IdempotentDomainEventHandler.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Abstractions/Data/IUnitOfWork.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Abstractions/Security/ITokenGenerator.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/AssemblyReference.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Eternelle.Modules.Rsvp.Application.csprojsrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommand.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommandHandler.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommandValidator.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/ChangeGuestAllowance/ChangeGuestAllowanceCommand.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/ChangeGuestAllowance/ChangeGuestAllowanceCommandHandler.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/ChangeGuestAllowance/ChangeGuestAllowanceCommandValidator.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/GetGuest/GetGuestQuery.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/GetGuest/GetGuestQueryHandler.cs
💤 Files with no reviewable changes (2)
- src/API/Eternelle.Api/modules.weddings.Development.json
- src/API/Eternelle.Api/modules.weddings.json
CacheService now logs failures instead of throwing, using ILogger. Template repository and events use value objects for name and tier. Template section variants are trimmed before storing. Migration cleans up permissions on down. RegisterUser returns 201 Created with user ID.
Updated Dapper calls to use CommandDefinition with CancellationToken in IntegrationEventConsumer and ProcessInboxJob for better cancellation handling. Enhanced ReorderSectionsCommandValidator to ensure section keys are not null or whitespace.
- Rename ITokenGenerator to ILookupCodeGenerator and update usages - Retry guest lookup code generation up to 5 times to avoid collisions - Use LookupCode value object in GuestLookupCodeRegeneratedDomainEvent - Improve validation for guest listing and RSVP deadline commands - Update docs: add new modules, rename SnapShare to SocialShare, remove obsolete endpoints/plan limits - Make RichDescriptionErrors public - Fix RemoveGuestCommandHandler to use correct GuestId
- Centralize LookupCode normalization logic in a static method - Enforce max limits in bulk photo and upload slot validators - Add/extend validators for feed, guest, and settings commands - Use strongly-typed PhotoStatus in feed queries - Improve event handling for guest/invitee lifecycle - Annotate BulkDeletePhotosEndpoint request with [FromBody] - Introduce constants and clean up magic numbers and usings
Updated CacheService to avoid logging on cancellation exceptions. Improved AttendingState validation to only apply when not null.
Updated the .Must validation for PhotoIds in BulkApprovePhotosCommandValidator, BulkDeletePhotosCommandValidator, and BulkRejectPhotosCommandValidator to handle null values safely and prevent NullReferenceException. Now, PhotoIds can be null or have a count less than or equal to MaxBulkPhotoIds.
Eternelle-193
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/modules/catalog.md (1)
105-105:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the catalog migration ID in docs/modules/catalog.md.
docs/modules/catalog.mdlists20260527094230_InitialCatalog, but the shipped EF migration files are20260529115411_InitialCatalog.cs/20260529115411_InitialCatalog.Designer.cs—update the docs so it matches__EFMigrationsHistory.🤖 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 `@docs/modules/catalog.md` at line 105, Update the migration ID referenced in docs/modules/catalog.md so it matches the shipped EF migration files and the __EFMigrationsHistory entry: replace the incorrect `20260527094230_InitialCatalog` with `20260529115411_InitialCatalog` (the names of the actual files: `20260529115411_InitialCatalog.cs` / `20260529115411_InitialCatalog.Designer.cs`) so the docs and migration history are consistent.
♻️ Duplicate comments (2)
src/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommandHandler.cs (1)
75-90:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winCheck Result before accessing
.Valueon line 82.
LookupCode.Create(raw).Valueis accessed without verifying the Result succeeded. If the generator produces invalid input, this throwsInvalidOperationExceptioninstead of returning a domain error.🛡️ Proposed defensive fix
const int maxAttempts = 5; LookupCode? lookupCode = null; for (int attempt = 0; attempt < maxAttempts; attempt++) { string raw = tokenGenerator.GenerateLookupCode(); if (!await guestRepository.LookupCodeExistsAsync(raw, cancellationToken)) { - lookupCode = LookupCode.Create(raw).Value; - break; + Result<LookupCode> codeResult = LookupCode.Create(raw); + if (codeResult.IsFailure) + { + return Result.Failure<Guid>(codeResult.Error); + } + lookupCode = codeResult.Value; + break; } }🤖 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/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommandHandler.cs` around lines 75 - 90, The code calls LookupCode.Create(raw).Value without verifying the Result and may throw; change the loop to capture the creation Result (e.g. var createRes = LookupCode.Create(raw)), check createRes.IsSuccess before using .Value, and only assign lookupCode = createRes.Value when successful (otherwise continue to next attempt); if all attempts either collide or creation fails, return the existing failure path (Result.Failure<Guid>(GuestErrors.LookupCodeAlreadyTaken)) or propagate a specific domain error if creation fails consistently. Ensure you update the logic around tokenGenerator.GenerateLookupCode(), guestRepository.LookupCodeExistsAsync(...), and LookupCode.Create(...) accordingly.src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/UpdateTemplate/UpdateTemplateCommandHandler.cs (1)
69-81:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not fail a committed command when cache invalidation fails.
After the aggregate is persisted (line 67), cache invalidation is a secondary concern. Wrap the
InvalidateAsynccall in try/catch or add exception handling inside the helper so cache failures log warnings without propagating to the caller.Proposed fix
+using Microsoft.Extensions.Logging; + internal sealed class UpdateTemplateCommandHandler( ITemplateRepository templateRepository, IUnitOfWork unitOfWork, ICacheService cacheService, - IDateTimeProvider dateTimeProvider) : ICommandHandler<UpdateTemplateCommand> + IDateTimeProvider dateTimeProvider, + ILogger<UpdateTemplateCommandHandler> logger) : ICommandHandler<UpdateTemplateCommand> { public async Task<Result> Handle(UpdateTemplateCommand command, CancellationToken cancellationToken) { ... templateRepository.Update(template); await unitOfWork.SaveChangesAsync(cancellationToken); - await InvalidateAsync(cacheService, command.TemplateId, cancellationToken); + try + { + await InvalidateAsync(cacheService, command.TemplateId, cancellationToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogWarning(ex, "Catalog cache invalidation failed for TemplateId {TemplateId}", command.TemplateId); + } return Result.Success(); }🤖 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.Application/Templates/UpdateTemplate/UpdateTemplateCommandHandler.cs` around lines 69 - 81, After persisting the aggregate in UpdateTemplateCommandHandler, make cache invalidation non-fatal by catching exceptions from InvalidateAsync so failures don't fail the command; either wrap the call to InvalidateAsync(cacheService, command.TemplateId, cancellationToken) in a try/catch and log a warning, or add internal try/catch inside the private static InvalidateAsync method around each cacheService.RemoveAsync call (or the whole loop) to catch Exception and log a warning with the exception and the templateId; reference the InvalidateAsync method, cacheService.RemoveAsync, and CatalogCacheKeys.TemplateById/AllTemplateListKeys when applying the change.
🤖 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 `@README.md`:
- Line 138: The README is inconsistent with the configuration call
AddModuleConfiguration(["weddings", "users", "catalog", "tenancy", "rsvp",
"album"]); update the documentation to reflect the Album module: add an "Album"
row to the Modules table (include schema and current status) and adjust the
RSVP/Guestbook status entry so it matches the actual wiring, plus update the
architecture diagram and the "next planned module" note to remove or reconcile
Album/Rsvp discrepancies so the textual inventory matches
AddModuleConfiguration.
---
Outside diff comments:
In `@docs/modules/catalog.md`:
- Line 105: Update the migration ID referenced in docs/modules/catalog.md so it
matches the shipped EF migration files and the __EFMigrationsHistory entry:
replace the incorrect `20260527094230_InitialCatalog` with
`20260529115411_InitialCatalog` (the names of the actual files:
`20260529115411_InitialCatalog.cs` /
`20260529115411_InitialCatalog.Designer.cs`) so the docs and migration history
are consistent.
---
Duplicate comments:
In
`@src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/UpdateTemplate/UpdateTemplateCommandHandler.cs`:
- Around line 69-81: After persisting the aggregate in
UpdateTemplateCommandHandler, make cache invalidation non-fatal by catching
exceptions from InvalidateAsync so failures don't fail the command; either wrap
the call to InvalidateAsync(cacheService, command.TemplateId, cancellationToken)
in a try/catch and log a warning, or add internal try/catch inside the private
static InvalidateAsync method around each cacheService.RemoveAsync call (or the
whole loop) to catch Exception and log a warning with the exception and the
templateId; reference the InvalidateAsync method, cacheService.RemoveAsync, and
CatalogCacheKeys.TemplateById/AllTemplateListKeys when applying the change.
In
`@src/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommandHandler.cs`:
- Around line 75-90: The code calls LookupCode.Create(raw).Value without
verifying the Result and may throw; change the loop to capture the creation
Result (e.g. var createRes = LookupCode.Create(raw)), check createRes.IsSuccess
before using .Value, and only assign lookupCode = createRes.Value when
successful (otherwise continue to next attempt); if all attempts either collide
or creation fails, return the existing failure path
(Result.Failure<Guid>(GuestErrors.LookupCodeAlreadyTaken)) or propagate a
specific domain error if creation fails consistently. Ensure you update the
logic around tokenGenerator.GenerateLookupCode(),
guestRepository.LookupCodeExistsAsync(...), and LookupCode.Create(...)
accordingly.
🪄 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: 193155c9-343e-4fa1-954a-74bee0e37b0a
📒 Files selected for processing (31)
README.mddocs/modules/catalog.mddocs/modules/weddings.mdsrc/Common/Eternelle.Common.Domain/Text/RichDescriptionErrors.cssrc/Common/Eternelle.Common.Infrastructure/Caching/CacheService.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/DeleteOwnPhoto/DeleteOwnPhotoCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GenerateGuestUploadSlots/GenerateGuestUploadSlotsCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GenerateGuestUploadSlots/GenerateGuestUploadSlotsCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/GetUploadContext/GetUploadContextQueryHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Invitees/RegisterGuestPhotos/RegisterGuestPhotosCommandHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkApprovePhotos/BulkApprovePhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkDeletePhotos/BulkDeletePhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/BulkRejectPhotos/BulkRejectPhotosCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GenerateHostUploadSlots/GenerateHostUploadSlotsCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/GetFeedQueryHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Photos/GetFeed/GetFeedQueryValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateSettings/UpdateSettingsCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Settings/UpdateUploadLimit/UpdateUploadLimitCommandValidator.cssrc/Modules/Album/Eternelle.Modules.Album.Application/Uploads/IAlbumUploadService.cssrc/Modules/Album/Eternelle.Modules.Album.Domain/Shared/LookupCode.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Photos/BulkDeletePhotosEndpoint.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestLookupCodeRegeneratedIntegrationEventHandler.cssrc/Modules/Album/Eternelle.Modules.Album.Presentation/Rsvp/GuestRemovedIntegrationEventHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/CreateTemplate/CreateTemplateCommandHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Application/Templates/UpdateTemplate/UpdateTemplateCommandHandler.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/ITemplateRepository.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/Template.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Templates/TemplateCreatedDomainEvent.cssrc/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Templates/TemplateRepository.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Abstractions/Security/ILookupCodeGenerator.cssrc/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Guests/AddGuest/AddGuestCommandHandler.cs
Updated README to reflect RSVP and Album modules as complete, adjusted planned module to Guestbook, and revised module tables. Added AlbumDbContext to migration handling. Refactored LookupCodeGenerator to use RandomNumberGenerator.GetInt32 for code generation.
Added detailed per-module docs for Album and RSVP in /docs/modules (album.md, rsvp.md). Updated solution and README to reference new documentation. Docs cover aggregates, commands, queries, endpoints, integration events, and persistence for each module.
Eternelle-193
Summary by CodeRabbit
Release Notes
New Features
Infrastructure