Files
slp-modular-cms/aidlc-docs/features/slpsoftware-api/construction/plans/offerings-code-generation-plan.md
T
SluijsensandClaude Sonnet 5 cfb06b28b6
Continuous Integration / config (pull_request) Successful in 12s
Continuous Integration / changes (pull_request) Successful in 22s
Continuous Integration / backend-build (pull_request) Successful in 5m53s
Continuous Integration / vulnerability-scan (pull_request) Successful in 5m46s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m54s
Continuous Integration / backend-test (pull_request) Successful in 7m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m59s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 7m34s
Continuous Integration / deploy-test (pull_request) Skipped
Adds the Offerings module and retargets the CI/CD pipeline to Api.SlpSoftware
Implements Unit 2 "Offerings" (backend module, admin CRUD UI with
drag-and-drop reordering, public GET /api/v1/offerings endpoint) and
executes the feature's D-15 CI/CD cutover, switching the deploy
pipeline's build/publish target from SlpModularCms.Api to
SlpModularCms.Api.SlpSoftware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWyStNL2ZsjrS7FLd7xvvN
2026-08-02 16:23:09 +02:00

14 KiB

Code Generation Plan — Unit: Offerings

Unit Context

Stories implemented: US-01 through US-12 (all 12). Functional requirements implemented: FR-4, FR-5, FR-6, FR-7, FR-8. Dependencies: Unit 1 "SlpSoftware Client Setup" (complete, approved, CI green) — SlpModularCms.Api.SlpSoftware already exists. Expected interfaces produced: IOfferingsService/OfferingsService, IOfferingRepository/OfferingRepository, OfferingsController, OfferingsModule, plus the full admin frontend feature. Database entities owned: Offering (new table, new OfferingsDbContext, own migration history — same MariaDB instance as every other module). Workspace root: K:\Development\Projects\SlpModularCms.

Investigation Findings That Shape This Plan

  • Table naming convention (read MasterDbContext.cs/AvailabilityDbContext.cs): every module prefixes its table name with the module name to avoid collisions in the shared MariaDB database (MasterCmsInstances, AvailabilityMasterRegistrations). Following this exactly: Offering → table OfferingsOfferings.
  • Validation mechanism (BR-OFF-04 requires field-level errors; no existing precedent for DataAnnotations or FluentValidation in this codebase — CmsInstanceService/UsersController do manual ArgumentException/flat-message checks instead): deciding directly, no question needed — DataAnnotations attributes ([Required], [MaxLength]) on CreateOfferingRequest/UpdateOfferingRequest, relying on [ApiController]'s automatic ValidationProblemDetails (RFC 9457-compatible, already how GlobalExceptionHandler frames every other error). This is the idiomatic ASP.NET Core mechanism and needs no new library. The Features list's per-item length/count bounds (1-10 items, each ≤200 chars) need a small custom ValidationAttribute since DataAnnotations doesn't validate collection-item length out of the box.
  • Rate limiting registration location: AddCmsRateLimiting lives in SlpModularCms.Core.Hosting.ServiceCollectionExtensions, shared by all Client projects via CmsHost. The new offerings-public policy is added there (Core), not per-module — harmless on Api/Api.Slave since nothing references that policy name without the OfferingsController action's attribute.
  • Frontend routing: this codebase uses TanStack Router (object-based routes in router.tsx), not React Router — frontend-components.md's "Admin Router" maps to new createRoute entries under authenticatedRoute, using the existing RoleGuard/ModuleGuard/lazyPage patterns from cmsRoute (Owner+Administrator roles, requiredModule="Offerings" so the nav item/route gracefully no-ops on Api/Api.Slave builds that never reference Modules.Offerings). Nav entry added to Sidebar.tsx's NAV_ITEMS.
  • Transactions (NFR Design Pattern 1): MasterDbContext/CmsInstanceRepository show no existing transaction-wrapping precedent in this codebase — this unit introduces the first one, via context.Database.BeginTransactionAsync() in the three OfferingsService methods identified in NFR Design.

Steps

Step 1 — Domain Layer (SlpModularCms.Modules.Offerings)

  • Data/Entities/Offering.cs — all 13 fields per domain-entities.md (Id, Title, Description, Price, PriceNote, Features (List<string>, stored as JSON column), CtaLabel, Featured, DisplayOrder, IsDeleted, DeletedAt, CreatedAt, UpdatedAt, LastModifiedByUserId)
  • Data/OfferingsDbContext.csDbSet<Offering> Offerings, OnModelCreating: ToTable("OfferingsOfferings"), field max-lengths matching domain-entities.md, Features mapped via a value converter (JSON string ⇄ List<string>), a global HasQueryFilter(o => !o.IsDeleted) so soft-deleted rows are never returned to any caller without every repository method needing its own .Where(!IsDeleted) (matches frontend-components.md/domain-entities.md's "soft-deleted rows are never returned to any caller")
  • Add Microsoft.EntityFrameworkCore.Design-driven initial migration (dotnet ef migrations add InitialCreate, run at Step 8 once the context compiles)

Step 2 — Repository (Repositories/)

  • IOfferingRepository + OfferingRepository per component-methods.md: GetAllAsync, GetByIdAsync, AddAsync, UpdateAsync, GetMaxDisplayOrderAsync, GetFeaturedAsync — plus GetByDisplayOrderNeighborAsync-style helpers as needed for MoveUpAsync/MoveDownAsync's adjacent-swap lookup (Code Generation detail, not previously specified at method-signature level)
  • Expose OfferingsDbContext.Database (or a thin BeginTransactionAsync/CommitAsync wrapper) so OfferingsService can own the transaction boundary without the repository leaking DbContext internals beyond what CmsInstanceRepository's existing SaveChangesAsync()-exposing pattern already does

Step 3 — Service (Services/)

  • IOfferingsService + OfferingsService per component-methods.md, implementing:
    • GetPublicOfferingsAsync / GetAllForAdminAsync — simple projections to OfferingDto/OfferingAdminDto
    • CreateAsync/UpdateAsync — BR-OFF-04 validation (via model binding, already enforced before the service runs), BR-OFF-01 featured exclusivity, wrapped in a transaction (NFR Design Pattern 1) when Featured transitions to true; sets LastModifiedByUserId from the authenticated caller (NFR-OFF-03) and CreatedAt/UpdatedAt
    • DeleteAsync — BR-OFF-02 (soft delete, never blocked), sets LastModifiedByUserId
    • ReorderAsync — BR-OFF-03, full-list resequence, transactional
    • MoveUpAsync/MoveDownAsync — BR-OFF-03 boundary no-op, transactional swap
    • Structured LogInformation call on every create/update/delete (NFR Design Pattern 3: OfferingId, Action, LastModifiedByUserId)
  • LastModifiedByUserId sourced the same way UsersController sources the caller's id today: ClaimTypes.NameIdentifier/"sub" claim, resolved via IHttpContextAccessor (mirroring MasterServiceDependencies's existing use of IHttpContextAccessor for a different purpose) — passed into the service from the controller, not read directly in the service, to keep the service HTTP-agnostic and unit-testable

Step 4 — Models (Models/)

  • OfferingDto, OfferingAdminDto (records, per domain-entities.md's exact field lists)
  • CreateOfferingRequest, UpdateOfferingRequest — DataAnnotations per BR-OFF-04 ([Required], [MaxLength(100)] etc.), plus the custom FeaturesValidationAttribute for per-item length/count
  • ReorderOfferingsRequest (ordered Guid[])

Step 5 — Controller (Controllers/OfferingsController.cs)

  • Per component-methods.md's route table exactly: GetOfferings ([AllowAnonymous], [EnableRateLimiting("offerings-public")]), GetAllForAdmin/Create/Update/Delete/Reorder/MoveUp/MoveDown ([Authorize(Policy = "AdminOnly")], no rate-limit attribute)
  • [ApiController] + [Route("offerings")] (matches CmsHost's ApiPrefixConvention("api/v1"), giving the final /api/v1/offerings route FR-6 requires)

Step 6 — Module Registration (OfferingsModule.cs)

  • RegisterServices: AddDbContext<OfferingsDbContext> (MySQL, NonLockingMySQLHistoryRepository, matching MasterModule's exact pattern), AddScoped<IOfferingRepository, OfferingRepository>, AddScoped<IOfferingsService, OfferingsService>, AddHttpContextAccessor() (for Step 3's caller-id resolution)
  • UseModule: db.Database.Migrate()

Step 7 — Rate Limiting Policy (Core, shared)

  • SlpModularCms.Core.Hosting.ServiceCollectionExtensions.AddCmsRateLimiting: add options.AddFixedWindowLimiter("offerings-public", opt => { ... RateLimiting:OfferingsPublic ... }), following the exact login/refresh/sentry-tunnel shape (default PermitLimit generous, e.g. 120/60s — final default decided at implementation time, config-overridable)
  • Add RateLimiting:OfferingsPublic section to Api.SlpSoftware/appsettings.json (and .Development.json if defaults should differ)

Step 8 — Project Wiring

  • src/SlpModularCms.Modules.Offerings/SlpModularCms.Modules.Offerings.csproj — mirrors Modules.Master.csproj shape (InternalsVisibleToModules.Offerings.Tests, ProjectReferenceCore)
  • src/SlpModularCms.Modules.Offerings.Tests/SlpModularCms.Modules.Offerings.Tests.csproj — mirrors Modules.Master.Tests.csproj (xunit, FluentAssertions, NSubstitute, EF Core InMemory, coverlet)
  • Add <ProjectReference> to Modules.Offerings in SlpModularCms.Api.SlpSoftware.csproj (this unit's responsibility per unit-of-work.md, not Unit 1's)
  • SlpModularCms.sln: add both new projects (Modules solution folder for Offerings, Tests/Modules for Offerings.Tests, per CLAUDE.md's structure rules), ProjectConfigurationPlatforms, NestedProjects
  • Generate and apply the EF Core migration from Step 1 once the project compiles

Step 9 — Backend Tests (≥80% coverage, NFR-OFF-04)

  • OfferingRepositoryTests (EF Core InMemory) — GetAllAsync excludes soft-deleted, ordering by DisplayOrder, GetFeaturedAsync
  • OfferingsServiceTests (NSubstitute repository) — BR-OFF-01 (featured exclusivity, both directions), BR-OFF-02 (delete always succeeds incl. last-remaining), BR-OFF-03 (reorder resequence, move-up/down boundary no-ops), BR-OFF-04 is exercised at the model-binding layer (controller test), audit fields set (LastModifiedByUserId/CreatedAt/UpdatedAt)
  • OfferingsControllerTests (WebApplicationFactory or direct controller instantiation with a substituted service, matching this codebase's existing test style) — route/auth assertions ([AllowAnonymous] on public GET, AdminOnly on the rest), validation-failure → 400 with field-level errors, rate limiter → 429 after the configured burst on the public GET only

Step 10 — Frontend: Dependency + Schema

  • frontend/package.json: add @dnd-kit/core + @dnd-kit/sortable (Functional Design Q1 = A)
  • frontend/src/features/offerings/schemas/offering.ts — zod schema mirroring BR-OFF-04 exactly (title ≤100, description ≤500, price ≤50, priceNote ≤100, ctaLabel ≤50, features 1-10 items each ≤200 chars, featured optional/defaults false)

Step 11 — Frontend: Services (API hooks, services/)

  • useOfferings(), useOffering(id), useCreateOffering(), useUpdateOffering(), useDeleteOffering(), useReorderOfferings(), useMoveOffering(direction) — per frontend-components.md's table, mirroring useCmsInstances.ts/useAddCmsInstance.ts's exact TanStack Query shape (query key ['offerings', 'admin'], invalidated by every mutating hook)
  • services/types.tsOfferingAdminDto, CreateOfferingRequest, UpdateOfferingRequest frontend-side types matching the backend DTOs field-for-field

Step 12 — Frontend: Hooks (feature-local, hooks/)

  • hooks/useOfferingsDnd.ts — dnd-kit sensor setup, drag-end order computation, optimistic local update, calls useReorderOfferings() (per frontend-components.md)

Step 13 — Frontend: Components and Pages

  • components/OfferingsList.tsx, components/OfferingRow.tsx, components/DeleteOfferingDialog.tsx, components/OfferingForm.tsx
  • pages/OfferingsListPage.tsx, pages/OfferingFormPage.tsx
  • All per frontend-components.md's props/responsibilities/data-testid convention exactly

Step 14 — Frontend: Routing, Navigation, i18n

  • router.tsx: offeringsRoute (/offerings), offeringsNewRoute (/offerings/new), offeringsEditRoute (/offerings/$id/edit) under authenticatedRoute, each wrapped in RoleGuard allowedRoles={['Owner', 'Administrator']} + ModuleGuard requiredModule="Offerings", using lazyPage
  • Sidebar.tsx: new NAV_ITEMS entry (to: '/offerings', roles: ['Owner', 'Administrator'], requiredModule: 'Offerings', an appropriate lucide-react icon e.g. Package)
  • i18n locale files (nl/en): nav.offerings + all new page/form/dialog copy (labels, validation messages, delete-confirmation text per BR-OFF-02's Dutch example already drafted in business-logic-model.md)

Step 15 — Frontend Tests

  • Component/page tests mirroring features/cms's existing .test.tsx/.test.ts coverage style (schema tests, hook tests with MSW-style mocks per mocks/handlers.ts pattern, component render/interaction tests)

Step 16 — Documentation

  • aidlc-docs/features/slpsoftware-api/construction/offerings/code/summary.md — summarizing everything generated across Steps 1-15
  • No root README.md change expected (Offerings is an internal module addition, not a structural/hosting change like Unit 1 was) — confirm at generation time whether the Projectstructuur section needs a one-line mention

Step 17 — Deployment Artifacts

  • N/A for this Construction stage — no new infrastructure (Infrastructure Design was skipped); the CI/CD cutover itself is Operations-phase work (D-7/D-15), untouched here

Step 18 — Build and Test Verification (automatic)

  • Backend: dotnet build full solution, dotnet test for Modules.Offerings.Tests (new) and the full solution (regression check), confirm ≥80% coverage on the new module
  • Frontend: pnpm test (new Offerings tests + full suite regression), pnpm build (or equivalent typecheck/lint) to confirm no compile errors
  • Fix and retry on any failure; only surface to the user if a fix requires a decision only they can make

Scope reminder: this plan implements Unit 2 "Offerings" — the last unit for this feature. Once approved and green, the feature moves to feature-wide Build and Test, then the Operations phase.