Adds the Offerings module and retargets the CI/CD pipeline to Api.SlpSoftware
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
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
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
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
||||
# 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`)
|
||||
- [x] `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`)
|
||||
- [x] `Data/OfferingsDbContext.cs` — `DbSet<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")
|
||||
- [x] Add `Microsoft.EntityFrameworkCore.Design`-driven initial migration (`dotnet ef migrations add InitialCreate`, run at Step 8 once the context compiles)
|
||||
|
||||
### Step 2 — Repository (`Repositories/`)
|
||||
- [x] `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)
|
||||
- [x] 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/`)
|
||||
- [x] `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`)
|
||||
- [x] `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/`)
|
||||
- [x] `OfferingDto`, `OfferingAdminDto` (records, per `domain-entities.md`'s exact field lists)
|
||||
- [x] `CreateOfferingRequest`, `UpdateOfferingRequest` — DataAnnotations per BR-OFF-04 (`[Required]`, `[MaxLength(100)]` etc.), plus the custom `FeaturesValidationAttribute` for per-item length/count
|
||||
- [x] `ReorderOfferingsRequest` (ordered `Guid[]`)
|
||||
|
||||
### Step 5 — Controller (`Controllers/OfferingsController.cs`)
|
||||
- [x] 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)
|
||||
- [x] `[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`)
|
||||
- [x] `RegisterServices`: `AddDbContext<OfferingsDbContext>` (MySQL, `NonLockingMySQLHistoryRepository`, matching `MasterModule`'s exact pattern), `AddScoped<IOfferingRepository, OfferingRepository>`, `AddScoped<IOfferingsService, OfferingsService>`, `AddHttpContextAccessor()` (for Step 3's caller-id resolution)
|
||||
- [x] `UseModule`: `db.Database.Migrate()`
|
||||
|
||||
### Step 7 — Rate Limiting Policy (Core, shared)
|
||||
- [x] `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)
|
||||
- [x] Add `RateLimiting:OfferingsPublic` section to `Api.SlpSoftware/appsettings.json` (and `.Development.json` if defaults should differ)
|
||||
|
||||
### Step 8 — Project Wiring
|
||||
- [x] `src/SlpModularCms.Modules.Offerings/SlpModularCms.Modules.Offerings.csproj` — mirrors `Modules.Master.csproj` shape (`InternalsVisibleTo` → `Modules.Offerings.Tests`, `ProjectReference` → `Core`)
|
||||
- [x] `src/SlpModularCms.Modules.Offerings.Tests/SlpModularCms.Modules.Offerings.Tests.csproj` — mirrors `Modules.Master.Tests.csproj` (xunit, FluentAssertions, NSubstitute, EF Core InMemory, coverlet)
|
||||
- [x] Add `<ProjectReference>` to `Modules.Offerings` in `SlpModularCms.Api.SlpSoftware.csproj` (this unit's responsibility per `unit-of-work.md`, not Unit 1's)
|
||||
- [x] `SlpModularCms.sln`: add both new projects (Modules solution folder for `Offerings`, Tests/Modules for `Offerings.Tests`, per `CLAUDE.md`'s structure rules), `ProjectConfigurationPlatforms`, `NestedProjects`
|
||||
- [x] Generate and apply the EF Core migration from Step 1 once the project compiles
|
||||
|
||||
### Step 9 — Backend Tests (≥80% coverage, NFR-OFF-04)
|
||||
- [x] `OfferingRepositoryTests` (EF Core InMemory) — `GetAllAsync` excludes soft-deleted, ordering by `DisplayOrder`, `GetFeaturedAsync`
|
||||
- [x] `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`)
|
||||
- [x] `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
|
||||
- [x] `frontend/package.json`: add `@dnd-kit/core` + `@dnd-kit/sortable` (Functional Design Q1 = A)
|
||||
- [x] `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/`)
|
||||
- [x] `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)
|
||||
- [x] `services/types.ts` — `OfferingAdminDto`, `CreateOfferingRequest`, `UpdateOfferingRequest` frontend-side types matching the backend DTOs field-for-field
|
||||
|
||||
### Step 12 — Frontend: Hooks (feature-local, `hooks/`)
|
||||
- [x] `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
|
||||
- [x] `components/OfferingsList.tsx`, `components/OfferingRow.tsx`, `components/DeleteOfferingDialog.tsx`, `components/OfferingForm.tsx`
|
||||
- [x] `pages/OfferingsListPage.tsx`, `pages/OfferingFormPage.tsx`
|
||||
- [x] All per `frontend-components.md`'s props/responsibilities/data-testid convention exactly
|
||||
|
||||
### Step 14 — Frontend: Routing, Navigation, i18n
|
||||
- [x] `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`
|
||||
- [x] `Sidebar.tsx`: new `NAV_ITEMS` entry (`to: '/offerings'`, `roles: ['Owner', 'Administrator']`, `requiredModule: 'Offerings'`, an appropriate `lucide-react` icon e.g. `Package`)
|
||||
- [x] `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
|
||||
- [x] 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
|
||||
- [x] `aidlc-docs/features/slpsoftware-api/construction/offerings/code/summary.md` — summarizing everything generated across Steps 1-15
|
||||
- [x] 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
|
||||
- [x] **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)
|
||||
- [x] 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
|
||||
- [x] Frontend: `pnpm test` (new Offerings tests + full suite regression), `pnpm build` (or equivalent typecheck/lint) to confirm no compile errors
|
||||
- [x] 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.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Functional Design Questions — Unit: Offerings
|
||||
|
||||
## Vraag 1 — Drag-and-drop is een nieuwe frontend-dependency
|
||||
`frontend/package.json` bevat vandaag geen enkele drag-and-drop-library (geen `@dnd-kit/*`, `react-beautiful-dnd`, of vergelijkbaar). US-08 (drag-and-drop herordenen) zou dus een nieuwe dependency betekenen, terwijl US-09 (omhoog/omlaag-knoppen) de functionele eis al volledig dekt zonder nieuwe dependency.
|
||||
|
||||
Hoe wil je dit voor de eerste versie aanpakken?
|
||||
|
||||
A) Beide bouwen zoals in de Inception-fase besloten — nieuwe dependency toevoegen (`@dnd-kit/core` + `@dnd-kit/sortable`, de huidige de-facto standaard voor React) voor drag-and-drop, plús de knoppen als toegankelijke fallback
|
||||
B) Nu alleen de omhoog/omlaag-knoppen bouwen (US-09) — geen nieuwe dependency; drag-and-drop (US-08) wordt een latere, aparte toevoeging
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
## Vraag 2 — Pagina-structuur admin-CRUD
|
||||
De bestaande `cms`-feature (Master-module) gebruikt het patroon: één lijstpagina (`CmsPage.tsx` + `CmsInstanceList.tsx`) met een modal-dialoog voor "toevoegen" (`AddCmsInstanceDialog.tsx`) en een aparte modal voor statuswijziging.
|
||||
|
||||
Moet de Offerings-admin-UI hetzelfde patroon volgen?
|
||||
|
||||
A) Ja — één lijstpagina met rij-acties (bewerken/verwijderen/omhoog/omlaag), en een modal-dialoog die zowel voor "aanmaken" als "bewerken" hergebruikt wordt (met de featured-toggle erin)
|
||||
B) Aparte pagina's/routes voor aanmaken en bewerken in plaats van modals
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: X, het hoeft niet hetzelfde patroon te zijn. Voor consistentie wel mooi, maar het CMS-stuk is vooral voor de master en zal niet bij andere websittes komen. Dus Optie B is prima
|
||||
|
||||
## Vraag 3 — Validatiegrenzen (SECURITY-05)
|
||||
Requirements.md vereist lengtebeperkingen op tekstvelden, maar noemt geen concrete getallen. Op basis van de bestaande content (langste titel "Landingspagina" = 14 tekens, langste description ≈ 70 tekens) stel ik ruime maar begrensde limieten voor.
|
||||
|
||||
Welke bovengrenzen wil je hanteren?
|
||||
|
||||
A) Title 100, Description 500, Price 50, PriceNote 100, CtaLabel 50 tekens; Features: min 1, max 10 items, elk item max 200 tekens — ruim genoeg voor toekomstig hergebruik (fotografie-pakketten etc.), maar begrensd tegen misbruik
|
||||
B) Strakkere limieten, dicht bij de huidige content (Title 50, Description 200, Features max 6 items van elk 100 tekens)
|
||||
X) Anders (geef zelf de getallen op na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
## Vraag 4 — Bevestiging bij verwijderen
|
||||
Verwijderen is een soft delete (D-Q5=B), maar er is geen "herstel"-functie in de admin-UI voorzien (buiten scope, FR-7 noemt alleen create/edit/delete/reorder). Vanuit het perspectief van de CMS Administrator is verwijderen dus onomkeerbaar.
|
||||
|
||||
Moet de admin-UI een bevestigingsdialoog tonen vóór het verwijderen van een offering?
|
||||
|
||||
A) Ja — een bevestigingsdialoog ("Weet je zeker dat je '[titel]' wilt verwijderen?") vóór de delete-aanroep
|
||||
B) Nee — direct verwijderen zonder bevestiging
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
## Vraag 5 — UI-trigger voor de featured-vlag
|
||||
US-10 beschrijft dat het systeem "precies 0 of 1 featured offering" afdwingt, maar niet hoe de CMS Administrator dat instelt in de UI.
|
||||
|
||||
A) Een checkbox/toggle "Meest gekozen" in het aanmaak-/bewerkformulier van elke offering — bij opslaan met deze toggle aan wordt de vorige featured-offering automatisch uitgezet
|
||||
B) Een aparte actie in de lijst-view zelf (bijv. een ster-icoon per rij om direct featured te maken, los van het bewerkformulier)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: kan A en B beide?
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Functional Design Plan — Unit: Offerings
|
||||
|
||||
Context loaded from `inception/application-design/unit-of-work.md` (unit scope: FR-4..FR-8, all 12 user stories) and `unit-of-work-story-map.md` (story assignment confirmation).
|
||||
|
||||
Investigated before drafting questions (not assumed):
|
||||
- `frontend/src/features/cms/` (the existing Master-module admin UI — `CmsPage.tsx`, `CmsInstanceList.tsx`, `AddCmsInstanceDialog.tsx`, `services/use*.ts`, `schemas/*.ts`) is the closest existing precedent for an admin list+CRUD screen in this codebase — used as the structural template below.
|
||||
- `frontend/package.json` has **no drag-and-drop library** (no `@dnd-kit/*`, no `react-beautiful-dnd`, nothing matching "dnd"/"sortable"/"drag"). US-08 (drag-and-drop reorder) would be a genuinely new frontend dependency, not something already available — see Vraag 1.
|
||||
|
||||
## Uitvoeringschecklist
|
||||
|
||||
- [x] Stap A — `business-logic-model.md`: procesflow voor create/update (met featured-exclusiviteit) en de twee reorder-interacties, als Mermaid-diagrammen
|
||||
- [x] Stap B — `business-rules.md`: gedetailleerde regels als Mermaid-beslisdiagrammen (featured-exclusiviteit, soft-delete-gedrag, reorder-grenzen, validatieregels)
|
||||
- [x] Stap C — `domain-entities.md`: `Offering`-entiteit met velden, types, en relatie tot de DTO's (publiek vs. admin) als Mermaid-diagram
|
||||
- [x] Stap D — `frontend-components.md`: componenthiërarchie voor de admin-CRUD-schermen (Mermaid `graph TD`), props/state per component, formuliervalidatie, welke backend-endpoints elk component aanroept
|
||||
- [x] Stap E — Consistentiecontrole tegen requirements.md, stories.md en application-design/component-methods.md — geen gaten gevonden; FR-8 (referentiecontent) blijft buiten deze unit's codegeneratie (D-5, handmatige invoer door gebruiker)
|
||||
@@ -0,0 +1,26 @@
|
||||
# NFR Design Plan — Unit: Offerings
|
||||
|
||||
**Categorieën die niet van toepassing zijn (met onderbouwing, niet zomaar overgeslagen)**:
|
||||
- **Scalability Patterns**: N/A — een freelancer-website met een handvol offerings (naar verwachting < 50 rijen); geen schaal-grens om voor te ontwerpen.
|
||||
- **Performance Patterns**: N/A buiten wat al besloten is — geen caching (NFR-OFF-02), geen zwaar rekenwerk; `GetAllAsync`/`GetPublicOfferingsAsync` zijn simpele, geïndexeerde queries.
|
||||
- **Security Patterns (rate limiting)**: geen losse vraag nodig — bestaand precedent in `AuthController` (`[EnableRateLimiting("login")]`/`[EnableRateLimiting("refresh")]`, per-actiemethode) wordt direct hergebruikt: `[EnableRateLimiting("offerings-public")]` komt op de publieke `GET`-actiemethode in `OfferingsController`, niet op de admin-mutatie-acties (die geen policy krijgen, per NFR-OFF-01 Q1=A).
|
||||
|
||||
**Wél een open ontwerpvraag gevonden**: `services.md` (Application Design) had de transactiegrens voor multi-row-operaties expliciet doorgeschoven naar Functional Design ("Exact transactional boundaries ... are a Functional Design decision for the Offerings unit, not decided here"), maar noch `business-logic-model.md` noch `business-rules.md` heeft dit vastgelegd. Dit raakt direct een NFR (data-integriteit), dus leg ik 'm hier alsnog voor in plaats van 'm stilzwijgend zelf te beslissen.
|
||||
|
||||
## Uitvoeringschecklist
|
||||
|
||||
- [x] Stap A — `nfr-design-patterns.md`: transactiepatroon, rate-limiting-toepassing, logging-velden vastleggen
|
||||
- [x] Stap B — `logical-components.md`: `OfferingsService`'s multi-row-operaties en de nieuwe rate-limiting-policy als logische componenten beschrijven
|
||||
|
||||
---
|
||||
|
||||
## Vragen
|
||||
|
||||
### Vraag 1 — Transactiegrens voor multi-row-operaties
|
||||
Drie operaties in `OfferingsService` raken meer dan één rij in dezelfde logische actie: de featured-exclusiviteitswissel (create/update met `Featured=true`, US-10), de volledige drag-and-drop-reorder (US-08), en de aangrenzende swap via knoppen (US-09). Moeten deze in één DB-transactie (`BeginTransaction`/`Commit`) of als opeenvolgende, niet-transactionele `SaveChangesAsync`-aanroepen?
|
||||
|
||||
A) Eén DB-transactie per operatie — garandeert dat bijv. een reorder van 10 rijen nooit half doorgevoerd raakt bij een crash/verbindingsfout; iets meer code (expliciet transactiebeheer), maar dit is precies waar transacties voor bestaan
|
||||
B) Opeenvolgende `SaveChangesAsync`-aanroepen zonder expliciete transactie — eenvoudiger; een falen halverwege laat de dataset in een inconsistente staat (bijv. twee offerings zonder featured, of dubbele `DisplayOrder`-waarden) tot een volgende succesvolle actie het herstelt; geaccepteerd risico gezien de kleine schaal (1 admin, handmatig direct zichtbaar/herstelbaar)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# NFR Requirements Plan — Unit: Offerings
|
||||
|
||||
Investigated before drafting questions:
|
||||
- `SlpModularCms.Core.Observability.SecurityEvents` (the existing structured security-alerting mechanism, `RateLimitTriggered` etc.) is purpose-built for **alertable anomalies** (brute force, forged tokens) — Warning-level, feeds Sentry alert rules (SECURITY-14). A routine "admin edited an offering" event is not an anomaly and would pollute that exact alerting mechanism if bolted on. SECURITY-13 (audit trail) is better served by plain `LogInformation`-level structured logging in `OfferingsService`, not by extending `SecurityEvents`.
|
||||
- `ServiceCollectionExtensions.AddCmsRateLimiting` already defines three named policies (`login`, `refresh`, `sentry-tunnel`) via `appsettings.json`'s `RateLimiting:*` section — adding an `offerings` (or `offerings-public`/`offerings-admin`) policy would follow the exact same established pattern.
|
||||
|
||||
## Uitvoeringschecklist
|
||||
|
||||
- [x] Stap A — `nfr-requirements.md`: NFR's voor deze unit vastleggen (rate limiting, caching, audit-trail-afronding, testdekking)
|
||||
- [x] Stap B — `tech-stack-decisions.md`: bevestigen dat geen nieuwe backend-technologie nodig is; vastleggen welke `RateLimiting`-policy(s) worden toegevoegd
|
||||
|
||||
---
|
||||
|
||||
## Vragen
|
||||
|
||||
### Vraag 1 — Rate limiting op de nieuwe endpoints
|
||||
De publieke `GET /api/v1/offerings` en de admin-CRUD-endpoints hebben nog geen rate-limiting-policy (in tegenstelling tot Login/Refresh/SentryTunnel).
|
||||
|
||||
Welke endpoints moeten een rate-limiting-policy krijgen?
|
||||
|
||||
A) Alleen de publieke `GET`-endpoint (tegen scraping/misbruik van een anonieme, veelgebruikte endpoint) — admin-endpoints zijn al achter authenticatie, dus lager risico
|
||||
B) Zowel de publieke `GET` als de admin-mutatie-endpoints — consistente verdediging in de diepte (SECURITY-11), ook al zijn admin-endpoints al geauthenticeerd
|
||||
C) Geen van beide nu — dit achterwege laten, eventueel later toevoegen
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Vraag 2 — HTTP-caching op de publieke endpoint
|
||||
De externe hand-off-doc noemt expliciet: "adding reasonable HTTP caching is welcome since this content changes rarely" voor `GET /api/v1/offerings`.
|
||||
|
||||
Wil je dit nu meenemen?
|
||||
|
||||
A) Ja — een simpele `Cache-Control: public, max-age=<N>` header op de publieke `GET`-response (bijv. 60-300 seconden); geen ETag/conditional-requests-complexiteit voor nu
|
||||
B) Nee — geen caching-headers in deze unit; de frontend gebruikt toch al TanStack Query zonder custom staleTime (ziet er functioneel niet uit als een probleem)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:B
|
||||
|
||||
### Vraag 3 — SECURITY-13-afronding: "wie" naast "wanneer"
|
||||
Functional Design voegde `CreatedAt`/`UpdatedAt` toe aan `Offering` (het "wanneer"-deel van SECURITY-13). Het "wie"-deel (welke admin de wijziging maakte) staat nog los.
|
||||
|
||||
Wil je dat ook vastleggen?
|
||||
|
||||
A) Ja — voeg `LastModifiedByUserId` (of vergelijkbaar) toe aan `Offering`, gevuld vanuit de geauthenticeerde admin-gebruiker bij elke create/update/delete
|
||||
B) Nee — `CreatedAt`/`UpdatedAt` is voldoende voor nu; "wie" blijft een bekend, geaccepteerd gat (net als de rest van SECURITY-13, al genoteerd als open item in requirements.md)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Vraag 4 — Testdekkingsnorm
|
||||
De `master-cms-module`-feature hanteerde een bestaande projectnorm van ≥80% testdekking voor nieuwe modules (NFR-MASTER-05).
|
||||
|
||||
Geldt dezelfde norm voor de Offerings-module?
|
||||
|
||||
A) Ja — zelfde ≥80%-norm aanhouden
|
||||
B) Nee — andere norm (geef aan welke na de [Answer]:-tag)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
Reference in New Issue
Block a user