Adds SlpModularCms.Api.SlpSoftware and extracts shared CmsHost composition
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Successful in 6m10s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m59s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m27s
Continuous Integration / backend-test (pull_request) Failing after 7m48s
Continuous Integration / frontend-build (pull_request) Successful in 2m5s
Continuous Integration / frontend-test (pull_request) Successful in 4m24s
Continuous Integration / frontend-lint (pull_request) Successful in 2m0s
Continuous Integration / publish-test (pull_request) Skipped
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Successful in 6m10s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m59s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m27s
Continuous Integration / backend-test (pull_request) Failing after 7m48s
Continuous Integration / frontend-build (pull_request) Successful in 2m5s
Continuous Integration / frontend-test (pull_request) Successful in 4m24s
Continuous Integration / frontend-lint (pull_request) Successful in 2m0s
Continuous Integration / publish-test (pull_request) Skipped
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Unit 1 of the slpsoftware-api feature (FR-1/FR-2/FR-3): a new Client project in the Clients solution folder, intended to eventually become the deployed API for test.slpsoftware.nl/slpsoftware.nl, hosting the same four modules as SlpModularCms.Api plus a future Offerings module. - Extracts SlpModularCms.Api/Program.cs's hosting-pipeline composition into SlpModularCms.Core.Hosting.CmsHost (ConfigureServices/ConfigurePipeline), shared by both Client projects so they cannot drift apart - Moves StaticContentExtensions.cs + WebsitePlaceholder.html from Api into Core, since CmsHost cannot live in Api but Core cannot depend on Api - Adds SlpModularCms.Api.SlpSoftware with its own isolated local dev database and dev ports (5286/7223, distinct from Api's and Api.Slave's) - Adds SlpModularCms.Api.Tests with WebApplicationFactory-based pipeline regression tests (security headers, health check, SPA fallback, rate limiting), scoped to Api per NFR Design - Adds a frontend dev:slpsoftware pnpm script mirroring dev:slave - Fixes GlobalExceptionHandler logging routine 401s (e.g. an expired/missing refresh token) as unhandled errors -- pre-existing, unrelated to this feature's own scope, found while testing the new instance Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWyStNL2ZsjrS7FLd7xvvN
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
# Application Design — SlpSoftware Production API
|
||||
|
||||
Consolidated summary. See the companion documents for full detail:
|
||||
- [components.md](components.md) — component identification and responsibilities
|
||||
- [component-methods.md](component-methods.md) — method signatures per component
|
||||
- [services.md](services.md) — service-layer orchestration patterns
|
||||
- [component-dependency.md](component-dependency.md) — dependency matrix and data-flow diagrams
|
||||
|
||||
## Design Decisions (traced to application-design-plan.md)
|
||||
|
||||
| # | Decision | Source |
|
||||
|---|---|---|
|
||||
| AD-1 | `CmsHost` exposes two granular methods (`ConfigureServices`, `ConfigurePipeline`) rather than one all-owning entrypoint — each Client project keeps a visible, thin `Program.cs`. | Q1 = B |
|
||||
| AD-2 | The `Offerings` module follows the Repository+Service pattern, consistent with `Modules.Master`, even though the module itself is simple CRUD. | Q2 = A |
|
||||
| AD-3 | `OfferingsController` is a single controller with per-action authorization (`[AllowAnonymous]` on the public `GET`, `AdminOnly` on everything else), not split into two controllers. | Q3 = B |
|
||||
| AD-4 | New offerings get a system-generated `Guid` `Id`, not an admin-provided slug. | Q4 = A |
|
||||
| AD-5 | Deleting an offering is a soft delete (`IsDeleted`/`DeletedAt`), filtered out of all reads — a partial, lightweight answer to the SECURITY-13 open item from requirements.md, without introducing a full audit-log mechanism. | Q5 = B |
|
||||
|
||||
## Component Overview
|
||||
|
||||
8 components: the shared `CmsHost` composition helper, the new `SlpModularCms.Api.SlpSoftware` Client project, and 6 components making up `SlpModularCms.Modules.Offerings` (`Offering` entity, `OfferingsDbContext`, `IOfferingRepository`/`OfferingRepository`, `IOfferingsService`/`OfferingsService`, `OfferingsController`, `OfferingsModule`).
|
||||
|
||||
## Consistency Check Against Requirements and Stories
|
||||
|
||||
| Requirement / Story | Covered By |
|
||||
|---|---|
|
||||
| FR-1 (new Client project) | `SlpModularCms.Api.SlpSoftware` component |
|
||||
| FR-2 (module composition) | Component-dependency.md — project-reference-driven module discovery |
|
||||
| FR-3 (shared hosting extraction) | `CmsHost` component + methods |
|
||||
| FR-4 (new module) | `OfferingsModule` + all Offerings sub-components |
|
||||
| FR-5 (`Offering` entity) | `Offering` entity component |
|
||||
| FR-6 (public endpoint) | `OfferingsController.GetOfferings`, `IOfferingsService.GetPublicOfferingsAsync` |
|
||||
| FR-7 (admin CRUD) | `OfferingsController` admin actions, `IOfferingsService` Create/Update/Delete/Reorder/MoveUp/MoveDown |
|
||||
| FR-8 (reference content) | No new component — content stays documented in requirements.md; entered manually by the user through the admin CRUD once built |
|
||||
| FR-9 (CI/CD retarget) | Out of scope for Application Design — Operations phase |
|
||||
| US-01/US-02/US-03 | `GetPublicOfferingsAsync` + `OfferingDto` shape (featured flag, empty-array-safe) |
|
||||
| US-04/US-05 | `CreateAsync`/`UpdateAsync` |
|
||||
| US-06/US-07 | `DeleteAsync` (soft delete, always allowed) |
|
||||
| US-08 | `ReorderAsync` |
|
||||
| US-09 | `MoveUpAsync`/`MoveDownAsync` |
|
||||
| US-10 | Featured-exclusivity orchestration in `IOfferingsService` (services.md pattern 1) |
|
||||
| US-11 | Validation is a `OfferingsController`/request-model concern (SECURITY-05) — detailed in Functional Design |
|
||||
| US-12 | `AdminOnly` policy on all mutating actions (AD-3) |
|
||||
|
||||
No gaps found: every functional requirement and user story maps to at least one component or method defined above. Detailed business-rule logic (exact exclusivity transaction handling, reorder boundary behavior, field-level validation rules) is intentionally deferred to Functional Design for the Offerings unit, per Application Design's scope.
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# Component Dependency — SlpSoftware Production API
|
||||
|
||||
## Dependency Matrix
|
||||
|
||||
| Component | Depends On | Communication Pattern |
|
||||
|---|---|---|
|
||||
| `SlpModularCms.Api.SlpSoftware` (`Program.cs`) | `CmsHost` (Core), `ModuleOrchestrator` (Core) | Direct method calls (`ConfigureServices`/`ConfigurePipeline`) at startup |
|
||||
| `SlpModularCms.Api` (`Program.cs`) | `CmsHost` (Core), `ModuleOrchestrator` (Core) | Same as above — unchanged behavior, now via the shared method instead of inline code |
|
||||
| `CmsHost` | Existing `Core.Hosting.*` extension methods (`AddCoreInfrastructure`, `AddCmsCors`, `AddCmsRateLimiting`, `AddCmsHealthChecks`, `AddCmsSecurityHeaders`, `AddCmsObservability`, `AddCmsDataProtection`, `AddCmsLogging`, `UseCmsSentry`, `UseCmsSecurityHeaders`, `UseCmsStaticContent`, `MapCmsHealthChecks`, `MapSentryTunnel`, `MapCmsSpaFallbacks`, `MigrateCoreDatabase`) | Direct method calls — no new dependencies introduced, purely re-composing existing ones |
|
||||
| `OfferingsController` | `IOfferingsService` | Constructor-injected interface (DI) |
|
||||
| `IOfferingsService` (`OfferingsService`) | `IOfferingRepository` | Constructor-injected interface (DI) |
|
||||
| `IOfferingRepository` (`OfferingRepository`) | `OfferingsDbContext` | Constructor-injected `DbContext` (DI, scoped) |
|
||||
| `OfferingsDbContext` | MariaDB (`DefaultConnection`) | EF Core, `UseMySQL`, TLS-enforced connection string (SECURITY-01) |
|
||||
| `OfferingsModule` | `OfferingsDbContext`, `IOfferingRepository`/`OfferingRepository`, `IOfferingsService`/`OfferingsService` | DI registration (`RegisterServices`) + migration application (`UseModule`) |
|
||||
| `SlpModularCms.Api.SlpSoftware` | `SlpModularCms.Modules.Offerings`, `.Identity`, `.Availability`, `.Master` (project references) | Module DLLs discovered dynamically by `ModuleOrchestrator` at runtime — **not** an explicit list in code |
|
||||
| `SlpModularCms.Api` | `SlpModularCms.Modules.Identity`, `.Availability`, `.Master` (project references — **no** `.Offerings` reference) | Same discovery mechanism; `Api` never loads `Offerings` because it never references that project |
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
box rgba(246,224,94,0.4) Website
|
||||
participant Visitor as Site Visitor
|
||||
end
|
||||
box rgba(159,122,234,0.4) External Frontend
|
||||
participant FE as React Frontend
|
||||
end
|
||||
box rgba(99,179,237,0.4) Api.SlpSoftware
|
||||
participant Ctrl as OfferingsController
|
||||
participant Svc as OfferingsService
|
||||
participant Repo as OfferingRepository
|
||||
participant DB as OfferingsDbContext
|
||||
end
|
||||
|
||||
Visitor->>FE: Loads website
|
||||
FE->>Ctrl: GET /api/v1/offerings
|
||||
Ctrl->>Svc: GetPublicOfferingsAsync()
|
||||
Svc->>Repo: GetAllAsync()
|
||||
Repo->>DB: query non-deleted, order by DisplayOrder
|
||||
DB-->>Repo: Offering rows
|
||||
Repo-->>Svc: List<Offering>
|
||||
Svc-->>Ctrl: List<OfferingDto>
|
||||
Ctrl-->>FE: 200 OK, JSON array
|
||||
FE-->>Visitor: Renders offering cards
|
||||
```
|
||||
|
||||
Text alternative: a Site Visitor's browser loads the frontend, which calls the public `GET /api/v1/offerings` endpoint; the request flows Controller → Service → Repository → DbContext and the resulting offerings flow back up the same chain to render as cards (yellow = visitor-facing website, purple = the external frontend app, blue = the new Api.SlpSoftware backend components).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
box rgba(246,224,94,0.4) Admin
|
||||
participant Admin as CMS Administrator
|
||||
end
|
||||
box rgba(99,179,237,0.4) Api.SlpSoftware
|
||||
participant Ctrl as OfferingsController
|
||||
participant Svc as OfferingsService
|
||||
participant Repo as OfferingRepository
|
||||
participant DB as OfferingsDbContext
|
||||
end
|
||||
|
||||
Admin->>Ctrl: POST /api/v1/offerings/admin (AdminOnly)
|
||||
Ctrl->>Svc: CreateAsync(request)
|
||||
Svc->>Repo: GetFeaturedAsync()
|
||||
Repo-->>Svc: currently-featured Offering (or none)
|
||||
Svc->>Repo: UpdateAsync(previous featured to unfeature, if any)
|
||||
Svc->>Repo: AddAsync(new Offering)
|
||||
Repo->>DB: persist changes
|
||||
DB-->>Repo: saved Offering
|
||||
Repo-->>Svc: Offering
|
||||
Svc-->>Ctrl: OfferingAdminDto
|
||||
Ctrl-->>Admin: 201 Created
|
||||
```
|
||||
|
||||
Text alternative: a CMS Administrator's create request flows through the same layered chain, with the Service first checking for and clearing any existing featured offering before persisting the new one, enforcing the exactly-0-or-1-featured rule from US-10 (yellow = the admin actor, blue = the new backend components).
|
||||
|
||||
## Notes
|
||||
- `SlpModularCms.Core` is the shared dependency for both Client projects (`CmsHost`) but has **no** dependency in the other direction — `Core` does not reference `Modules.Offerings` or any other module, preserving the existing module-isolation pattern.
|
||||
- The Gitea Actions pipeline (external to the application dependency graph) is not shown here — its retargeting (FR-9, D-15) is an Operations-phase, deployment-time concern, not an application-level dependency.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Component Methods — SlpSoftware Production API
|
||||
|
||||
**Note**: Signatures and high-level purpose only. Detailed business rules (exact exclusivity algorithm, validation rules, reorder edge cases) are defined in Functional Design for the relevant unit (CONSTRUCTION phase).
|
||||
|
||||
## `CmsHost` (static class, `SlpModularCms.Core.Hosting`)
|
||||
|
||||
| Method | Input | Output | Purpose |
|
||||
|---|---|---|---|
|
||||
| `ConfigureServices` | `WebApplicationBuilder builder` | `ModuleOrchestrator` | Runs logging/Sentry setup, discovers and registers module services, registers core infrastructure (CORS, rate limiting, health checks, security headers, observability, Data Protection), and configures MVC controllers. Returns the `ModuleOrchestrator` instance so the caller can pass it into `ConfigurePipeline` after `builder.Build()`. |
|
||||
| `ConfigurePipeline` | `WebApplication app`, `ModuleOrchestrator orchestrator` | `void` | Runs the Core DB migration, wires the exception handler, security headers, rate limiter, Development-only OpenAPI/Scalar, HTTPS redirection, static content + SPA fallbacks, CORS, module middleware (`orchestrator.UseModules`), authentication/authorization, controller mapping, health checks, and the Sentry tunnel — in the exact order `Api/Program.cs` uses today, since that order encodes real constraints (documented as code comments in the current `Program.cs`). |
|
||||
|
||||
Each project's `Program.cs` becomes:
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true);
|
||||
var orchestrator = CmsHost.ConfigureServices(builder);
|
||||
var app = builder.Build();
|
||||
CmsHost.ConfigurePipeline(app, orchestrator);
|
||||
app.Run();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `IOfferingRepository` (`SlpModularCms.Modules.Offerings.Repositories`)
|
||||
|
||||
| Method | Input | Output | Purpose |
|
||||
|---|---|---|---|
|
||||
| `GetAllAsync` | — | `IReadOnlyList<Offering>` | All non-deleted offerings, ordered by `DisplayOrder`. |
|
||||
| `GetByIdAsync` | `Guid id` | `Offering?` | Single non-deleted offering, or `null`. |
|
||||
| `AddAsync` | `Offering offering` | `Offering` | Persist a new offering. |
|
||||
| `UpdateAsync` | `Offering offering` | `Offering` | Persist changes to an existing offering. |
|
||||
| `GetMaxDisplayOrderAsync` | — | `int` | Used by the service to append new offerings at the end of the display order. |
|
||||
| `GetFeaturedAsync` | — | `Offering?` | The currently-featured offering (if any), used to enforce exclusivity. |
|
||||
|
||||
---
|
||||
|
||||
## `IOfferingsService` (`SlpModularCms.Modules.Offerings.Services`)
|
||||
|
||||
| Method | Input | Output | Purpose |
|
||||
|---|---|---|---|
|
||||
| `GetPublicOfferingsAsync` | — | `IReadOnlyList<OfferingDto>` | Backs FR-6 / US-01, US-02, US-03. |
|
||||
| `GetAllForAdminAsync` | — | `IReadOnlyList<OfferingAdminDto>` | Backs the admin list view. |
|
||||
| `CreateAsync` | `CreateOfferingRequest request` | `OfferingAdminDto` | Backs US-04. Assigns a new `Id` (Q4 = A) and appends to the end of `DisplayOrder`. If `request.Featured` is `true`, un-features the previously-featured offering (US-10). |
|
||||
| `UpdateAsync` | `Guid id`, `UpdateOfferingRequest request` | `OfferingAdminDto` | Backs US-05. Same featured-exclusivity handling as `CreateAsync` when `request.Featured` is `true`. |
|
||||
| `DeleteAsync` | `Guid id` | `void` | Backs US-06/US-07. Soft-delete (Q5 = B) — always allowed, including for the last remaining offering. |
|
||||
| `ReorderAsync` | `IReadOnlyList<Guid> orderedIds` | `void` | Backs US-08 (drag-and-drop). Full-list reorder — reassigns `DisplayOrder` to match the given sequence. |
|
||||
| `MoveUpAsync` | `Guid id` | `void` | Backs US-09. Swaps `DisplayOrder` with the immediately preceding offering. |
|
||||
| `MoveDownAsync` | `Guid id` | `void` | Backs US-09. Swaps `DisplayOrder` with the immediately following offering. |
|
||||
|
||||
---
|
||||
|
||||
## `OfferingsController` (`SlpModularCms.Modules.Offerings.Controllers`)
|
||||
|
||||
| Action | Route | Auth | Input | Output | Purpose |
|
||||
|---|---|---|---|---|---|
|
||||
| `GetOfferings` | `GET /api/v1/offerings` | `[AllowAnonymous]` | — | `200 OK`, `OfferingDto[]` | FR-6 |
|
||||
| `GetAllForAdmin` | `GET /api/v1/offerings/admin` | `AdminOnly` | — | `200 OK`, `OfferingAdminDto[]` | Admin list view |
|
||||
| `Create` | `POST /api/v1/offerings/admin` | `AdminOnly` | `CreateOfferingRequest` | `201 Created`, `OfferingAdminDto` | US-04 |
|
||||
| `Update` | `PUT /api/v1/offerings/admin/{id}` | `AdminOnly` | `UpdateOfferingRequest` | `200 OK`, `OfferingAdminDto` | US-05 |
|
||||
| `Delete` | `DELETE /api/v1/offerings/admin/{id}` | `AdminOnly` | — | `204 No Content` | US-06/US-07 |
|
||||
| `Reorder` | `PUT /api/v1/offerings/admin/reorder` | `AdminOnly` | `ReorderOfferingsRequest` (ordered `Guid[]`) | `204 No Content` | US-08 |
|
||||
| `MoveUp` | `POST /api/v1/offerings/admin/{id}/move-up` | `AdminOnly` | — | `204 No Content` | US-09 |
|
||||
| `MoveDown` | `POST /api/v1/offerings/admin/{id}/move-down` | `AdminOnly` | — | `204 No Content` | US-09 |
|
||||
|
||||
**Note**: Exact route naming (e.g. `/admin` suffix vs. a route-group prefix) may be refined in Functional Design or Code Generation Planning; the split shown here keeps the public route exactly as FR-6 specifies while keeping admin routes obviously distinct, consistent with decision Q3 (single controller, per-action authorization).
|
||||
|
||||
---
|
||||
|
||||
## `OfferingsModule` (`SlpModularCms.Modules.Offerings`)
|
||||
|
||||
| Method | Input | Output | Purpose |
|
||||
|---|---|---|---|
|
||||
| `RegisterServices` | `IServiceCollection services` | `void` | Registers `OfferingsDbContext` (MySQL), `IOfferingRepository`/`OfferingRepository`, `IOfferingsService`/`OfferingsService`. |
|
||||
| `UseModule` | `IApplicationBuilder app` | `void` | Applies pending `OfferingsDbContext` migrations. |
|
||||
@@ -0,0 +1,82 @@
|
||||
# Components — SlpSoftware Production API
|
||||
|
||||
## 1. `CmsHost` (new — `SlpModularCms.Core.Hosting`)
|
||||
|
||||
**Purpose**: Shared hosting-composition logic extracted from `SlpModularCms.Api/Program.cs` (FR-3), consumed by both `SlpModularCms.Api` and `SlpModularCms.Api.SlpSoftware`.
|
||||
|
||||
**Responsibilities**:
|
||||
- Compose the standard set of service registrations every Client project needs (logging, Sentry, module discovery, core infrastructure, CORS, rate limiting, health checks, security headers, observability, Data Protection, controllers).
|
||||
- Compose the standard HTTP pipeline every Client project needs (migrations, exception handling, security headers, rate limiting, OpenAPI/Scalar in Development, HTTPS redirection, static content, CORS, module middleware, authentication/authorization, controllers, health checks, Sentry tunnel, SPA fallbacks).
|
||||
- **Not** responsible for: bootstrapping the `WebApplicationBuilder` itself, or loading `appsettings.local.json` — those two lines stay in each project's own `Program.cs` (decision Q1 = B: two granular methods, not one entrypoint that owns everything).
|
||||
|
||||
**Interfaces**: `ConfigureServices(WebApplicationBuilder)`, `ConfigurePipeline(WebApplication, ModuleOrchestrator)` — see component-methods.md.
|
||||
|
||||
---
|
||||
|
||||
## 2. `SlpModularCms.Api.SlpSoftware` (new Client project)
|
||||
|
||||
**Purpose**: The new deployable Client (FR-1), first project in the `Clients` solution folder. Eventually the production host for `test.slpsoftware.nl` / `slpsoftware.nl` (Operations phase, D-15).
|
||||
|
||||
**Responsibilities**:
|
||||
- Reference `SlpModularCms.Core`, `SlpModularCms.Modules.Identity`, `SlpModularCms.Modules.Availability`, `SlpModularCms.Modules.Master`, and the new `SlpModularCms.Modules.Offerings` (FR-2) — module composition is driven entirely by which Module projects are referenced (`ModuleOrchestrator` discovers modules from `.dll` files on disk, not from any list in `Program.cs`).
|
||||
- Thin `Program.cs`: bootstrap the builder, load `appsettings.local.json`, call `CmsHost.ConfigureServices`/`ConfigurePipeline`.
|
||||
- Own its own `appsettings.json` / `appsettings.Development.json` / `appsettings.local.json` (per the `dotnet-appsettings` skill pattern already used by `Api`).
|
||||
|
||||
**Interfaces**: None beyond `Program.cs` itself — it's a composition root, not a library.
|
||||
|
||||
---
|
||||
|
||||
## 3. `Offering` (new entity — `SlpModularCms.Modules.Offerings.Data.Entities`)
|
||||
|
||||
**Purpose**: The persisted record behind both the public read contract (FR-6) and admin management (FR-7).
|
||||
|
||||
**Responsibilities**: Hold `Id` (Guid, per Q4 = A), `Title`, `Description`, `Price`, `PriceNote`, `Features` (ordered list), `CtaLabel`, `Featured`, `DisplayOrder`, `IsDeleted`, `DeletedAt` (per Q5 = B, soft delete).
|
||||
|
||||
---
|
||||
|
||||
## 4. `OfferingsDbContext` (new — `SlpModularCms.Modules.Offerings.Data`)
|
||||
|
||||
**Purpose**: Module-isolated EF Core context for the `Offering` entity, following the existing per-module `DbContext` pattern (`MasterDbContext`, `AvailabilityDbContext`) — MariaDB via `UseMySQL`, its own migrations assembly.
|
||||
|
||||
**Responsibilities**: `DbSet<Offering> Offerings`; model configuration (max lengths for `Title`/`Description`/`Price`/`PriceNote`/`CtaLabel` per SECURITY-05).
|
||||
|
||||
---
|
||||
|
||||
## 5. `IOfferingRepository` / `OfferingRepository` (new — `SlpModularCms.Modules.Offerings.Repositories`)
|
||||
|
||||
**Purpose**: Data-access layer between the service and `OfferingsDbContext`, mirroring `Modules.Master`'s `ICmsInstanceRepository`/`CmsInstanceRepository` (decision Q2 = A: keep the Repository+Service pattern consistent across modules, even though this module is individually simple).
|
||||
|
||||
**Responsibilities**: CRUD against `Offering` rows, always excluding soft-deleted rows except where the service explicitly needs them; ordering by `DisplayOrder`.
|
||||
|
||||
---
|
||||
|
||||
## 6. `IOfferingsService` / `OfferingsService` (new — `SlpModularCms.Modules.Offerings.Services`)
|
||||
|
||||
**Purpose**: Business orchestration layer — the one component that knows the rules from stories.md that a repository alone shouldn't own.
|
||||
|
||||
**Responsibilities**:
|
||||
- Enforce the "at most one featured offering" exclusivity rule (US-10) when creating/updating.
|
||||
- Own the `DisplayOrder` semantics for both reorder interactions: full-list reorder from drag-and-drop (US-08) and adjacent swap from the up/down buttons (US-09).
|
||||
- Assign `Id` (new `Guid`) and initial `DisplayOrder` (append to end) on creation.
|
||||
- Apply soft-delete (US-06/US-07 — deleting the last remaining offering is always allowed, decision Q4 in requirements.md).
|
||||
- Map between `Offering` entities and the DTOs used by the controller.
|
||||
|
||||
---
|
||||
|
||||
## 7. `OfferingsController` (new — `SlpModularCms.Modules.Offerings.Controllers`)
|
||||
|
||||
**Purpose**: Single HTTP-facing component for the module (decision Q3 = B: one controller, mixed authorization per action, rather than a public/admin split into two controllers).
|
||||
|
||||
**Responsibilities**: Expose `GET /api/v1/offerings` (`[AllowAnonymous]`, FR-6) and the admin CRUD + reorder actions (`[Authorize(Policy = "AdminOnly")]`, FR-7) on the same controller, delegating all logic to `IOfferingsService`.
|
||||
|
||||
---
|
||||
|
||||
## 8. `OfferingsModule` (new — `SlpModularCms.Modules.Offerings`)
|
||||
|
||||
**Purpose**: `IModule` implementation, following the exact pattern of `MasterModule`/`AvailabilityModule`.
|
||||
|
||||
**Responsibilities**: `RegisterServices` — register `OfferingsDbContext` (MySQL, non-locking history repository per the existing convention), `IOfferingRepository`/`OfferingRepository`, `IOfferingsService`/`OfferingsService`. `UseModule` — apply pending `OfferingsDbContext` migrations at startup.
|
||||
|
||||
---
|
||||
|
||||
**8 components**: 1 shared hosting component (`CmsHost`), 1 new Client project, and 6 components making up the `Offerings` module (entity, DbContext, repository, service, controller, module registration).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Services — SlpSoftware Production API
|
||||
|
||||
## `IOfferingsService`
|
||||
|
||||
**Layer**: Service (business orchestration), between `OfferingsController` and `IOfferingRepository`.
|
||||
|
||||
**Why this service exists** (per decision Q2 = A, mirroring `ICmsInstanceService` in `Modules.Master`): the repository alone can't own the rules that span more than a single row — the "at most one featured offering" exclusivity check (US-10) reads and writes two rows in one logical operation, and both reorder interactions (US-08 full reorder, US-09 adjacent swap) recompute `DisplayOrder` across multiple rows. A controller calling the repository directly would either duplicate this orchestration or risk skipping it.
|
||||
|
||||
**Orchestration patterns**:
|
||||
|
||||
1. **Create/Update with featured exclusivity** (US-10): when a request sets `Featured = true`, the service first calls `GetFeaturedAsync()`; if a different offering currently holds it, that offering is un-featured (via `UpdateAsync` on it) in the same logical operation before the requested offering is saved as featured. Exact transactional boundaries (single DB transaction vs. sequential saves) are a Functional Design decision for the Offerings unit, not decided here.
|
||||
2. **Full reorder** (US-08): `ReorderAsync` receives the complete ordered list of IDs from the drag-and-drop UI and reassigns `DisplayOrder` sequentially (0, 1, 2, ...) to match.
|
||||
3. **Adjacent swap** (US-09): `MoveUpAsync`/`MoveDownAsync` locate the neighboring offering by `DisplayOrder` and swap the two `DisplayOrder` values. A no-op (or a clearly-defined error) at the boundaries (first item moving up, last item moving down) — exact behavior for an out-of-bounds call is a Functional Design detail.
|
||||
4. **Soft delete** (US-06/US-07, Q5 = B): `DeleteAsync` sets `IsDeleted = true` / `DeletedAt = now` rather than removing the row. The repository's read methods (`GetAllAsync`, `GetByIdAsync`, `GetFeaturedAsync`) always exclude soft-deleted rows, so callers never need to remember to filter — deleting the last remaining offering (US-07) is unaffected by this and remains always allowed.
|
||||
5. **Public vs. admin projections**: `GetPublicOfferingsAsync` returns `OfferingDto` (the exact FR-6 contract shape: `id`, `title`, `description`, `price`, `priceNote`, `features`, `ctaLabel`, `featured`). `GetAllForAdminAsync` returns `OfferingAdminDto`, which additionally exposes `DisplayOrder` (and, if useful in the admin UI, `IsDeleted`/`DeletedAt` are **not** exposed since deleted rows are never returned to any caller).
|
||||
|
||||
**No other services are introduced by this feature.** `CmsHost` (components.md #1) is a static composition helper, not a service in the DI/business-orchestration sense — it has no business rules, only infrastructure wiring, so it is documented under Components/Component Methods rather than here.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Unit of Work Dependency — SlpSoftware Production API
|
||||
|
||||
## Dependency Matrix
|
||||
|
||||
| Unit | Depends On | Nature of Dependency | Blocking? |
|
||||
|---|---|---|---|
|
||||
| 1. SlpSoftware Client Setup | Existing `SlpModularCms.Api` (must not regress), existing `Core.Hosting.*` extension methods | Behavior-preservation constraint: the extraction must reproduce `Api`'s current pipeline exactly | N/A (this unit is the starting point) |
|
||||
| 2. Offerings | Unit 1 (`SlpModularCms.Api.SlpSoftware` project must exist) | Structural: Unit 2 adds its own `<ProjectReference>` into `Api.SlpSoftware.csproj`, which requires that project to already exist | **Yes** — Unit 2 cannot start its Code Generation until Unit 1's `Api.SlpSoftware` project shell exists |
|
||||
|
||||
## Sequencing
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
U1["Unit 1: SlpSoftware Client Setup<br/>(FR-1, FR-2, FR-3)"]
|
||||
U2["Unit 2: Offerings<br/>(FR-4..FR-8, US-01..US-12)"]
|
||||
U1 -->|"Api.SlpSoftware project must exist first"| U2
|
||||
|
||||
classDef foundation fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef feature fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
|
||||
class U1 foundation;
|
||||
class U2 feature;
|
||||
```
|
||||
|
||||
Text alternative: Unit 1 (SlpSoftware Client Setup, blue) must complete before Unit 2 (Offerings, yellow) can start, because Unit 2's project reference requires Unit 1's `Api.SlpSoftware` project to already exist.
|
||||
|
||||
## Shared / Cross-Cutting Concerns
|
||||
|
||||
- **`SlpModularCms.Core`**: modified only by Unit 1 (the `CmsHost` addition). Unit 2 does not modify `Core`.
|
||||
- **Security Baseline compliance**: both units must satisfy their applicable rules from requirements.md's Security Compliance table independently — Unit 1 for the hosting/pipeline rules (SECURITY-03, 04, 09, 10, 14, 15 continuity), Unit 2 for the new-surface rules (SECURITY-05, 06, 08, 11, 13).
|
||||
- **No shared mutable state or runtime coupling** between the two units beyond the one-time structural dependency above — at runtime, `Offerings` is just another module discovered by `ModuleOrchestrator` inside the process Unit 1 built.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Unit of Work Story Map — SlpSoftware Production API
|
||||
|
||||
## Unit 1: SlpSoftware Client Setup
|
||||
|
||||
| User Story | Assigned? |
|
||||
|---|---|
|
||||
| — | None (decision Q3 = A — this unit is a purely technical enabling unit) |
|
||||
|
||||
| Functional Requirement | Assigned |
|
||||
|---|---|
|
||||
| FR-1 (new Client project) | ✅ |
|
||||
| FR-2 (module composition) | ✅ |
|
||||
| FR-3 (shared hosting extraction) | ✅ |
|
||||
|
||||
## Unit 2: Offerings
|
||||
|
||||
| User Story | Assigned |
|
||||
|---|---|
|
||||
| US-01 — View the list of available offerings | ✅ |
|
||||
| US-02 — See the recommended offering highlighted | ✅ |
|
||||
| US-03 — Website stays functional with zero offerings | ✅ |
|
||||
| US-04 — Create a new offering | ✅ |
|
||||
| US-05 — Edit an existing offering | ✅ |
|
||||
| US-06 — Delete an offering | ✅ |
|
||||
| US-07 — Delete the last remaining offering | ✅ |
|
||||
| US-08 — Reorder offerings via drag-and-drop | ✅ |
|
||||
| US-09 — Reorder offerings via up/down buttons | ✅ |
|
||||
| US-10 — Mark an offering as featured (system-enforced exclusivity) | ✅ |
|
||||
| US-11 — Receive validation feedback on invalid input | ✅ |
|
||||
| US-12 — Admin actions require the Administrator role | ✅ |
|
||||
|
||||
| Functional Requirement | Assigned |
|
||||
|---|---|
|
||||
| FR-4 (new module) | ✅ |
|
||||
| FR-5 (`Offering` entity) | ✅ |
|
||||
| FR-6 (public endpoint) | ✅ |
|
||||
| FR-7 (admin CRUD) | ✅ |
|
||||
| FR-8 (reference content, documentation only — no code) | ✅ |
|
||||
|
||||
## Coverage Check
|
||||
|
||||
- **All 12 user stories** assigned to exactly one unit (Offerings). ✅
|
||||
- **All 9 functional requirements** (FR-1 through FR-9) assigned, except **FR-9** (CI/CD retarget) — correctly **not** assigned to either Construction unit, since it belongs to the Operations phase, not Construction (per D-7/D-15 and the execution plan). ✅
|
||||
- No story or FR is assigned to more than one unit, and no story is left unassigned. ✅
|
||||
@@ -0,0 +1,49 @@
|
||||
# Unit of Work — SlpSoftware Production API
|
||||
|
||||
2 units (decision Q1 = A), named per Q2 = B.
|
||||
|
||||
---
|
||||
|
||||
## Unit 1: SlpSoftware Client Setup
|
||||
|
||||
**Scope**: FR-1, FR-2, FR-3. No user stories are directly attributed to this unit (decision Q3 = A) — it is a purely technical enabling unit with no persona-facing behavior of its own.
|
||||
|
||||
**Responsibilities**:
|
||||
- Extract the composed hosting pipeline from `SlpModularCms.Api/Program.cs` into `SlpModularCms.Core.Hosting.CmsHost`, as two methods: `ConfigureServices(WebApplicationBuilder)` and `ConfigurePipeline(WebApplication, ModuleOrchestrator)` (AD-1).
|
||||
- Repoint `SlpModularCms.Api/Program.cs` at the new shared methods, with **no behavior change** — its existing test suite must remain green.
|
||||
- Create the new `SlpModularCms.Api.SlpSoftware` project under the `Clients` solution folder, referencing `SlpModularCms.Core`, `SlpModularCms.Modules.Identity`, `SlpModularCms.Modules.Availability`, and `SlpModularCms.Modules.Master` (per FR-2). **Does not yet reference `SlpModularCms.Modules.Offerings`** — that reference is added by Unit 2, since the Offerings project doesn't exist until Unit 2 creates it.
|
||||
- Give `Api.SlpSoftware` its own `appsettings.json` / `appsettings.Development.json` / `appsettings.local.json` per the `dotnet-appsettings` skill's pattern (same as `Api`).
|
||||
|
||||
**Components owned** (from application-design/components.md): `CmsHost`, the `SlpModularCms.Api.SlpSoftware` project shell (its `Program.cs` and `.csproj`).
|
||||
|
||||
**Preliminary per-unit Construction assessment** (confirmed at each stage when reached, not decided here):
|
||||
- Functional Design: likely **SKIP** — no new data model or business rule, purely a hosting-composition refactor.
|
||||
- NFR Requirements / NFR Design: likely **EXECUTE** — the extraction must provably preserve `Api`'s existing security headers, rate limiting, Sentry, and Data Protection behavior (Security Baseline, D-11); this is exactly an NFR concern.
|
||||
- Infrastructure Design: likely **EXECUTE** — `Api.SlpSoftware` becoming a deployment target is new for this unit even though the underlying hosting infrastructure already exists (per the "when in doubt, execute" rule).
|
||||
- Code Generation, Build and Test: **ALWAYS**.
|
||||
|
||||
---
|
||||
|
||||
## Unit 2: Offerings
|
||||
|
||||
**Scope**: FR-4, FR-5, FR-6, FR-7, FR-8. All 12 user stories (US-01 through US-12).
|
||||
|
||||
**Responsibilities**:
|
||||
- Create `SlpModularCms.Modules.Offerings` (+ `SlpModularCms.Modules.Offerings.Tests`) following the existing module pattern: `Offering` entity, `OfferingsDbContext`, `IOfferingRepository`/`OfferingRepository`, `IOfferingsService`/`OfferingsService`, `OfferingsController`, `OfferingsModule` (per application-design/components.md and component-methods.md).
|
||||
- Add the `<ProjectReference>` to `SlpModularCms.Modules.Offerings` in `SlpModularCms.Api.SlpSoftware.csproj` (this unit's responsibility, not Unit 1's — see plan rationale).
|
||||
- Implement the public `GET /api/v1/offerings` and the admin CRUD + reorder actions, enforcing the featured-exclusivity rule (US-10) and soft-delete semantics (AD-5).
|
||||
- No code changes for FR-8 (reference content) — the three current package values are already documented in requirements.md; the user enters them manually via the admin CRUD once built (decision D-5).
|
||||
|
||||
**Components owned**: `Offering`, `OfferingsDbContext`, `IOfferingRepository`/`OfferingRepository`, `IOfferingsService`/`OfferingsService`, `OfferingsController`, `OfferingsModule`.
|
||||
|
||||
**Preliminary per-unit Construction assessment**:
|
||||
- Functional Design: likely **EXECUTE** — new data model (`Offering`) and non-trivial business rules (featured exclusivity, two reorder interactions, soft delete).
|
||||
- NFR Requirements / NFR Design: likely **EXECUTE** — SECURITY-05 (input validation specifics) and the SECURITY-13 partial mitigation (soft delete) both need concrete design here.
|
||||
- Infrastructure Design: likely **SKIP** — reuses the existing MariaDB / per-module-migration infrastructure with nothing new to map (per requirements.md's Security Compliance table, SECURITY-01 is "pre-existing, unchanged" for this module).
|
||||
- Code Generation, Build and Test: **ALWAYS**.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Summary
|
||||
|
||||
Unit 2 (Offerings) depends on Unit 1 (SlpSoftware Client Setup) — the `Api.SlpSoftware` project must exist before Unit 2 can add its project reference into it. Unit 1 must be built and verified (existing `Api` test suite green after the extraction) before Unit 2 starts, per the Module Update Strategy in `inception/plans/execution-plan.md`. See `unit-of-work-dependency.md` for the full matrix.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Application Design Plan — SlpSoftware Production API
|
||||
|
||||
Dit plan beschrijft hoe de high-level applicatie-ontwerp-artefacten voor deze feature worden opgesteld. Beantwoord eerst de vragen hieronder; na jouw goedkeuring wordt het plan uitgevoerd.
|
||||
|
||||
Context die ik al heb geverifieerd in de code (niet aangenomen):
|
||||
- `ModuleOrchestrator` (`SlpModularCms.Core/Hosting/ModuleOrchestrator.cs`) ontdekt modules **dynamisch** door `.dll`-bestanden op schijf te scannen — er is nergens een expliciete "welke modules host ik"-lijst in `Program.cs`. Dat betekent: welke modules een Client-project host, wordt volledig bepaald door welke Module-projecten dat `.csproj` referenceert, niet door code in `Program.cs` zelf.
|
||||
- `SlpModularCms.Api/Program.cs` bevat, op de bootstrap-regels na (`WebApplication.CreateBuilder`, `appsettings.local.json`), **geen enkele project-specifieke branch** — alles is generiek/config-gedreven. Dat maakt een verregaande extractie (FR-3) haalbaar.
|
||||
- `SlpModularCms.Modules.Master` volgt het patroon: `Controllers/` → `Services/` (`I{X}Service`/`{X}Service`) → `Repositories/` (`I{X}Repository`/`{X}Repository`) → `Data/{X}DbContext.cs`, plus `Models/` voor DTO's/requests en `{Module}Module.cs` (`IModule`-implementatie).
|
||||
|
||||
## Uitvoeringschecklist
|
||||
|
||||
- [x] Stap A — `components.md`: componenten identificeren (CmsHost-extractie, Offerings-module met sub-componenten) met verantwoordelijkheden
|
||||
- [x] Stap B — `component-methods.md`: methode-signaturen per component (geen gedetailleerde business rules — dat komt in Functional Design)
|
||||
- [x] Stap C — `services.md`: servicedefinities en orkestratiepatronen (o.a. featured-exclusiviteit, reorder-logica uit de user stories)
|
||||
- [x] Stap D — `component-dependency.md`: afhankelijkheidsmatrix + datastroom (Core ↔ Api ↔ Api.SlpSoftware ↔ Offerings)
|
||||
- [x] Stap E — `application-design.md`: consolidatie van bovenstaande in één document
|
||||
- [x] Stap F — Consistentiecontrole: komt het ontwerp overeen met requirements.md (FR-1..FR-9) en stories.md (US-01..US-12)?
|
||||
|
||||
---
|
||||
|
||||
## Vragen
|
||||
|
||||
### Vraag 1 — Vorm van de `CmsHost`-extractie (FR-3)
|
||||
`Api/Program.cs` bevat, buiten de bootstrap-regels, geen project-specifieke logica. Dat maakt twee uitersten mogelijk voor de extractie.
|
||||
|
||||
Hoe ver moet de extractie naar `SlpModularCms.Core` gaan?
|
||||
|
||||
A) **Eén volledig entrypoint** — `CmsHost.RunAsync(string[] args)` bevat de hele samenstelling (services + pipeline + `app.Run()`); beide `Program.cs`-bestanden worden dan letterlijk een paar regels (`return CmsHost.RunAsync(args);` + evt. bootstrap-overrides). Minimaliseert duplicatie/drift maximaal, maar geeft een individueel project weinig ruimte om ooit af te wijken zonder de gedeelde methode te wijzigen.
|
||||
B) **Twee gedeelde methodes** — `CmsHost.ConfigureServices(WebApplicationBuilder)` en `CmsHost.ConfigurePipeline(WebApplication)`, die elk project vanuit zijn eigen dunne `Program.cs` aanroept (zoals vandaag al met `AddCoreInfrastructure` etc. gebeurt, maar dan als één samengestelde aanroep per fase). Iets meer code per project, maar elk project behoudt een zichtbaar `Program.cs` waarin het makkelijk is om ooit één stap toe te voegen/over te slaan zonder `Core` te wijzigen.
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
### Vraag 2 — Architectuurpatroon voor de Offerings-module
|
||||
`Modules.Master` gebruikt een Repository+Service-laag (`ICmsInstanceRepository` → `ICmsInstanceService` → Controller). Bij Requirements Analysis heb je de Property-Based Testing-extensie overgeslagen met als reden dat dit een eenvoudige CRUD-achtige module is zonder significante bedrijfslogica.
|
||||
|
||||
Moet de Offerings-module hetzelfde Repository+Service-patroon volgen (consistent met Master), of is dat voor deze module onnodige indirectie?
|
||||
|
||||
A) Repository+Service (consistent met Master) — `IOfferingRepository` + `IOfferingsService`, ook al is de module zelf simpel
|
||||
B) Alleen Service, geen Repository — `IOfferingsService` praat direct met `OfferingsDbContext` (minder indirectie voor een module die je zelf als eenvoudige CRUD hebt gekarakteriseerd)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Vraag 3 — Controllersplitsing publiek vs. admin
|
||||
Requirements.md scheidt al duidelijk het publieke `GET /api/v1/offerings` (FR-6, anoniem) van de admin-CRUD (FR-7, `AdminOnly`).
|
||||
|
||||
Moet dit ook twee aparte controllers worden, of één controller met gemengde autorisatie per actie?
|
||||
|
||||
A) Twee controllers — `OfferingsController` (publiek, alleen `GET`) en `OfferingsAdminController` (CRUD + reorder, `AdminOnly`) — duidelijke scheiding, moeilijker om per ongeluk een admin-actie anoniem te laten
|
||||
B) Eén controller — `OfferingsController` met `[AllowAnonymous]` op de publieke `GET` en `[Authorize(Policy = "AdminOnly")]` op de rest
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
### Vraag 4 — Id-strategie voor nieuw aangemaakte offerings
|
||||
De drie bestaande referentiewaarden (FR-8) gebruiken leesbare slugs (`pakket_01`, `pakket_02`, `pakket_03`). Het publieke contract (FR-6) verwacht een `string`-veld `id`, dus zowel een GUID als een handmatige slug is technisch mogelijk.
|
||||
|
||||
Hoe moet de `Id` van een **nieuw** aangemaakte offering tot stand komen?
|
||||
|
||||
A) Automatisch gegenereerd (GUID as string) — simpel, geen validatie op uniekheid/formaat nodig, consistent met andere entiteiten in dit systeem (bijv. `CmsInstance.Id`)
|
||||
B) Door de CMS Administrator zelf opgegeven als leesbare slug — consistent met de bestaande `pakket_XX`-stijl, vereist wel validatie (uniek, toegestane tekens)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Vraag 5 — Verwijdersemantiek
|
||||
Requirements.md signaleert een nog open punt bij SECURITY-13 (audit-trail op content-mutaties). US-06/US-07 beschrijven "verwijderen" zonder te specificeren of dat een echte database-delete is of een soft-delete.
|
||||
|
||||
Hoe moet "een offering verwijderen" op databaseniveau werken?
|
||||
|
||||
A) Hard delete — de rij wordt echt verwijderd uit `OfferingsDbContext`; simpelst, maar draagt niet bij aan het SECURITY-13-openpunt
|
||||
B) Soft delete — een `IsDeleted`/`DeletedAt`-veld, verwijderde offerings worden uit alle queries gefilterd maar blijven in de database staan; simpele, gedeeltelijke invulling van het SECURITY-13-openpunt (geen volledige audit trail, maar wel behoud van de laatste staat vóór verwijdering)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
@@ -0,0 +1,214 @@
|
||||
# Execution Plan — SlpSoftware Production API
|
||||
|
||||
## Detailed Analysis Summary
|
||||
|
||||
### Transformation Scope (Brownfield)
|
||||
- **Transformation Type**: Architectural addition, not a rewrite — a new deployable Client project is added alongside the existing one, a piece of existing hosting logic is extracted into a shared location, and one new module is added. No existing deployment model changes (still a single self-hosted ASP.NET Core process per environment).
|
||||
- **Primary Changes**: (1) Extract `SlpModularCms.Api/Program.cs`'s composed hosting pipeline into `SlpModularCms.Core` (`CmsHost.Configure(...)`); (2) new `SlpModularCms.Api.SlpSoftware` Client project consuming that shared method; (3) new `SlpModularCms.Modules.Offerings` module (entity, `DbContext`, public endpoint, admin CRUD); (4) Operations-phase CI/CD cutover of the existing pipeline from `Api` to `Api.SlpSoftware`.
|
||||
- **Related Components**: `SlpModularCms.Core` (extraction target + hosts the `Offering`-adjacent shared conventions), `SlpModularCms.Api` (must keep working identically after the extraction — it is not itself changing behavior), the existing Gitea Actions pipeline (`gitea-deployment-workflow` feature's artifacts).
|
||||
|
||||
### Change Impact Assessment
|
||||
- **User-facing changes**: Yes — new admin CRUD screens for the CMS Administrator persona, and new dynamic (CMS-managed) content on the live website for the Site Visitor persona (requirements.md FR-6, FR-7; stories.md US-01..US-12).
|
||||
- **Structural changes**: Yes — first-ever project in the `Clients` solution folder; new shared hosting-composition method in `Core`; new module following the existing `IModule` pattern.
|
||||
- **Data model changes**: Yes — new `Offering` entity + `OfferingsDbContext` (FR-5), isolated per the existing per-module migration pattern.
|
||||
- **API changes**: Yes — new public `GET /api/v1/offerings` (FR-6) and new authenticated admin endpoints (FR-7).
|
||||
- **NFR impact**: Yes — Security Baseline extension is enabled and blocking (D-11); the hosting-pipeline extraction (FR-3) must preserve `Api`'s existing security headers/rate limiting/Sentry/Data Protection behavior exactly, so it doesn't regress the already-hardened dev host while building the new one on the same foundation.
|
||||
|
||||
### Component Relationships (Brownfield)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
core["SlpModularCms.Core<br/>(hosting composition, Identity, Availability entities)"]
|
||||
api["SlpModularCms.Api<br/>(existing dev host)"]
|
||||
apiSlp["SlpModularCms.Api.SlpSoftware<br/>(new Client, eventual prod host)"]
|
||||
offerings["SlpModularCms.Modules.Offerings<br/>(new module)"]
|
||||
identity["SlpModularCms.Modules.Identity"]
|
||||
availability["SlpModularCms.Modules.Availability"]
|
||||
master["SlpModularCms.Modules.Master"]
|
||||
pipeline["Gitea Actions Pipeline<br/>(owned by gitea-deployment-workflow)"]
|
||||
|
||||
core -->|"CmsHost.Configure(...)<br/>consumed by both"| api
|
||||
core -->|"CmsHost.Configure(...)"| apiSlp
|
||||
apiSlp -->|"hosts"| identity
|
||||
apiSlp -->|"hosts"| availability
|
||||
apiSlp -->|"hosts"| master
|
||||
apiSlp -->|"hosts"| offerings
|
||||
api -->|"hosts (unchanged)"| identity
|
||||
api -->|"hosts (unchanged)"| availability
|
||||
api -->|"hosts (unchanged)"| master
|
||||
pipeline -.->|"retargeted (D-15 cutover)<br/>Operations phase"| apiSlp
|
||||
|
||||
classDef core fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef existing fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef new fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef external fill:#d6bcfa,stroke:#553c9a,stroke-width:1px,color:#000;
|
||||
|
||||
class core core;
|
||||
class api,identity,availability,master existing;
|
||||
class apiSlp,offerings new;
|
||||
class pipeline external;
|
||||
```
|
||||
|
||||
Text alternative: `Core` provides the shared `CmsHost.Configure` method to both the existing `Api` (unchanged behavior) and the new `Api.SlpSoftware` (blue = shared foundation, green = existing/unchanged components, yellow = new components, purple = the externally-owned CI/CD pipeline that gets retargeted in the Operations phase).
|
||||
|
||||
- **Primary Component**: `SlpModularCms.Modules.Offerings` (new) and `SlpModularCms.Api.SlpSoftware` (new)
|
||||
- **Infrastructure Components**: `.gitea/workflows/continuous_integration.yaml`, `.gitea/workflows/deploy-scp.yaml`, `operations/deployment/deployment-instructions.md` (all owned by `gitea-deployment-workflow`; extended, not duplicated, per D-7/D-15)
|
||||
- **Shared Components**: `SlpModularCms.Core` (new `CmsHost.Configure(...)`), `SlpModularCms.Modules.Identity`, `SlpModularCms.Modules.Availability`, `SlpModularCms.Modules.Master` (all hosted, unchanged)
|
||||
- **Dependent Components**: `SlpModularCms.Api` — does not change behavior, but depends on the FR-3 extraction being behavior-preserving
|
||||
- **Supporting Components**: Existing Sentry-based logging/alerting, existing `HierarchicalRoleHandler`/`AdminOnly` policy
|
||||
|
||||
| Related Component | Change Type | Change Reason | Change Priority |
|
||||
|---|---|---|---|
|
||||
| `SlpModularCms.Core` | Minor (additive extraction) | FR-3 shared hosting composition | Critical (blocks both Client projects) |
|
||||
| `SlpModularCms.Api` | Configuration-only (calls the new shared method instead of inline code) | FR-3 | Critical (regression risk if behavior changes) |
|
||||
| `SlpModularCms.Api.SlpSoftware` | Major (new project) | FR-1, FR-2 | Critical |
|
||||
| `SlpModularCms.Modules.Offerings` (+ Tests) | Major (new module) | FR-4, FR-5 | Critical |
|
||||
| Gitea Actions pipeline | Minor (retarget existing jobs) | FR-9, D-15 | Important (Operations phase only, not blocking Construction) |
|
||||
|
||||
### Risk Assessment
|
||||
- **Risk Level**: **Medium** — multiple components change, but each is independently testable (the `Core` extraction can be verified against `Api`'s existing test suite before `Api.SlpSoftware` is even built on top of it), and the highest-risk step (the CI/CD cutover, D-15) is isolated to the Operations phase, coordinated with the feature that already owns that pipeline rather than a fresh, unreviewed change.
|
||||
- **Rollback Complexity**: Moderate — the `Core` extraction is a straightforward revert if `Api`'s behavior regresses (git revert, `Api.SlpSoftware` didn't exist to depend on it yet at that point in the sequence). The pipeline cutover (Operations) is a config change to Gitea Actions YAML, revertible the same way.
|
||||
- **Testing Complexity**: Moderate — needs before/after regression coverage on `Api` for the extraction (NFR impact on security headers/rate limiting/Sentry/Data Protection continuity), plus new unit/integration tests for the `Offerings` module.
|
||||
|
||||
---
|
||||
|
||||
## Module Update Strategy
|
||||
|
||||
- **Update Approach**: Sequential where dependencies require it, then parallel-capable.
|
||||
1. **Foundation first**: Extract `CmsHost.Configure(...)` into `Core` and repoint `SlpModularCms.Api/Program.cs` at it, **verifying `Api`'s existing behavior and test suite are unaffected** before building anything new on top of the shared method.
|
||||
2. **Then, in parallel**: create `SlpModularCms.Api.SlpSoftware` (consuming the now-shared method + existing modules) and build out `SlpModularCms.Modules.Offerings` — these two do not depend on each other's internals, only on the foundation from step 1 and on `Api.SlpSoftware` existing as *a* host by the time `Offerings` needs to be wired in.
|
||||
3. **Operations last**: CI/CD cutover (D-15) only after Construction (Code Generation + Build and Test) has proven both the extraction and the new module.
|
||||
- **Critical Path**: The `Core` extraction (step 1) — both the new Client project and the continued correctness of the existing dev host depend on it.
|
||||
- **Coordination Points**: The shared `CmsHost.Configure(...)` signature (must accommodate `Api`'s and `Api.SlpSoftware`'s differing module lists); the CI/CD pipeline hand-off with `gitea-deployment-workflow` (extend existing jobs, don't fork them).
|
||||
- **Testing Checkpoints**: (a) After the `Core` extraction — full existing `Api` test suite + a manual/automated smoke check that `Api` still serves `/admin`, static content, health checks, and security headers identically. (b) After `Offerings` module code generation — its own unit/integration tests (NFR-2). (c) After both units — full Build and Test phase covering `Api.SlpSoftware` end-to-end. (d) Before the Operations cutover — confirm `Api.SlpSoftware` has been running successfully (e.g. against `test.slpsoftware.nl`) prior to repointing production.
|
||||
|
||||
**Exact unit boundaries and naming are finalized in the Units Generation stage** (next after Application Design); this section states the intended dependency order that Units Generation should respect, not the final unit list.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Visualization
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["User Request"])
|
||||
|
||||
subgraph INCEPTION["🔵 INCEPTION PHASE"]
|
||||
WD["Workspace Detection<br/><b>COMPLETED</b>"]
|
||||
RE["Reverse Engineering<br/><b>COMPLETED (reused)</b>"]
|
||||
RA["Requirements Analysis<br/><b>COMPLETED</b>"]
|
||||
US["User Stories<br/><b>COMPLETED</b>"]
|
||||
WP["Workflow Planning<br/><b>COMPLETED</b>"]
|
||||
AD["Application Design<br/><b>EXECUTE</b>"]
|
||||
UP["Units Planning<br/><b>EXECUTE</b>"]
|
||||
UG["Units Generation<br/><b>EXECUTE</b>"]
|
||||
end
|
||||
|
||||
subgraph CONSTRUCTION["🟢 CONSTRUCTION PHASE"]
|
||||
FD["Functional Design (per unit)<br/><b>EXECUTE</b>"]
|
||||
NFRA["NFR Requirements (per unit)<br/><b>EXECUTE</b>"]
|
||||
NFRD["NFR Design (per unit)<br/><b>EXECUTE</b>"]
|
||||
ID["Infrastructure Design (per unit)<br/><b>EXECUTE</b>"]
|
||||
CG["Code Generation<br/>(Planning + Generation)<br/><b>EXECUTE</b>"]
|
||||
BT["Build and Test<br/><b>EXECUTE</b>"]
|
||||
end
|
||||
|
||||
subgraph OPERATIONS["🟡 OPERATIONS PHASE"]
|
||||
DS["Deployment Setup<br/><b>EXECUTE</b>"]
|
||||
MS["Monitoring Setup<br/><b>EXECUTE</b>"]
|
||||
PRV["Production Readiness Validation<br/><b>EXECUTE</b>"]
|
||||
end
|
||||
|
||||
Start --> WD --> RE --> RA --> US --> WP --> AD --> UP --> UG --> FD --> NFRA --> NFRD --> ID --> CG --> BT --> DS --> MS --> PRV --> End(["Complete"])
|
||||
|
||||
style WD fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style RE fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style RA fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style US fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style WP fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style AD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style UP fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style UG fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style FD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style NFRA fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style NFRD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style ID fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style CG fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style BT fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style DS fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style MS fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000
|
||||
style PRV fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style Start fill:#CE93D8,stroke:#6A1B9A,stroke-width:3px,color:#000
|
||||
style End fill:#CE93D8,stroke:#6A1B9A,stroke-width:3px,color:#000
|
||||
|
||||
style INCEPTION fill:#BBDEFB,color:#000
|
||||
style CONSTRUCTION fill:#C8E6C9,color:#000
|
||||
style OPERATIONS fill:#FFF59D,color:#000
|
||||
|
||||
linkStyle default stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
Text alternative: all Inception stages up to and including Workflow Planning are completed (solid green). Application Design, Units Planning, Units Generation, and all four per-unit Construction design stages (Functional Design, NFR Requirements, NFR Design, Infrastructure Design) are planned to execute (dashed orange). Code Generation and Build and Test always execute (solid green). In Operations, Deployment Setup and Monitoring Setup are planned to execute (dashed orange, each asks its own inclusion question when reached), and Production Readiness Validation always runs once the phase is reached (solid green).
|
||||
|
||||
---
|
||||
|
||||
## Phases to Execute
|
||||
|
||||
### 🔵 INCEPTION PHASE
|
||||
- [x] Workspace Detection (COMPLETED)
|
||||
- [x] Reverse Engineering (COMPLETED — reused existing `_shared/reverse-engineering/` artifacts, no rerun)
|
||||
- [x] Requirements Analysis (COMPLETED)
|
||||
- [x] User Stories (COMPLETED)
|
||||
- [x] Workflow Planning / Execution Plan (COMPLETED — this document)
|
||||
- [ ] Application Design — **EXECUTE**
|
||||
- **Rationale**: New components are introduced (`Offering` entity, `OfferingsDbContext`, admin CRUD service layer, the shared `CmsHost.Configure(...)` method) whose methods, business rules (featured exclusivity, reorder persistence), and dependencies need definition before units can be planned.
|
||||
- [ ] Units Planning — **EXECUTE**
|
||||
- **Rationale**: Multiple modules/projects are involved (Core extraction, new Client project, new module) with a real dependency order (Module Update Strategy above) — this needs explicit unit boundaries, not an implicit single unit.
|
||||
- [ ] Units Generation — **EXECUTE**
|
||||
- **Rationale**: Same as Units Planning — this is a multi-unit change, not a single simple unit.
|
||||
|
||||
### 🟢 CONSTRUCTION PHASE
|
||||
*(Assessed per unit once Units Generation defines them; overall expectation below.)*
|
||||
- [ ] Functional Design — **EXECUTE** (primarily for the Offerings unit: new data model + business rules; likely minimal/skippable for a pure hosting-extraction unit — confirmed per-unit)
|
||||
- **Rationale**: New data model (`Offering`) and non-trivial business rules (exactly-one-featured, reorder semantics) need detailed design.
|
||||
- [ ] NFR Requirements — **EXECUTE**
|
||||
- **Rationale**: Security Baseline extension is enabled and blocking (D-11); the hosting-extraction unit specifically carries NFR risk (must not regress `Api`'s existing security headers/rate limiting/Sentry/Data Protection).
|
||||
- [ ] NFR Design — **EXECUTE**
|
||||
- **Rationale**: Follows directly from NFR Requirements being executed.
|
||||
- [ ] Infrastructure Design — **EXECUTE** (primarily for the Client/hosting unit: `Api.SlpSoftware` is a new deployment target; likely skippable for the Offerings unit, which reuses the existing MariaDB/module-migration infrastructure with nothing new to map)
|
||||
- **Rationale**: `Api.SlpSoftware` becoming a deployment target is new for this specific unit, even though the underlying hosting infrastructure (Pi, Gitea) already exists — per the "when in doubt, execute" rule for infra that's new to *this* unit.
|
||||
- [ ] Code Generation — **EXECUTE (ALWAYS)**
|
||||
- **Rationale**: Implementation planning and code generation needed for every unit.
|
||||
- [ ] Build and Test — **EXECUTE (ALWAYS)**
|
||||
- **Rationale**: Full build across units together, plus integration testing between the new module, the new Client project, and the unchanged `Api`.
|
||||
|
||||
### 🟡 OPERATIONS PHASE
|
||||
- [ ] Deployment Setup — **EXECUTE** (asks its own inclusion question when reached, per the workflow's standard pattern)
|
||||
- **Rationale**: D-7/D-15 — the CI/CD pipeline cutover from `Api` to `Api.SlpSoftware` is explicitly in scope for this feature's Operations phase.
|
||||
- [ ] Monitoring Setup — **EXECUTE** (asks its own inclusion question when reached)
|
||||
- **Rationale**: The new public endpoint and admin CRUD are new surfaces on what will become the production API; worth confirming the existing Sentry-based monitoring (inherited via FR-3) covers them, or whether anything additional is needed.
|
||||
- [ ] Production Readiness Validation — **EXECUTE (ALWAYS, once Operations phase is reached)**
|
||||
- **Rationale**: Standard wrap-up gate, including the Security Baseline final check and (per this repo's convention) the `dotnet-appsettings` compliance check for the new Client project.
|
||||
|
||||
---
|
||||
|
||||
## Package Change Sequence (Brownfield)
|
||||
|
||||
1. **`SlpModularCms.Core`** — add `CmsHost.Configure(...)` (or equivalent), extracted from `SlpModularCms.Api/Program.cs`. *Must land first; blocks everything else.*
|
||||
2. **`SlpModularCms.Api`** — repoint `Program.cs` at the new shared method. *No behavior change; verify via existing tests before proceeding.*
|
||||
3. **`SlpModularCms.Api.SlpSoftware`** (new) and **`SlpModularCms.Modules.Offerings`** (+ `.Tests`, new) — can proceed once steps 1-2 are verified; independent of each other internally, both needed before Build and Test can exercise the full stack.
|
||||
4. **Gitea Actions pipeline** (`continuous_integration.yaml`, `deploy-scp.yaml`, `deployment-instructions.md`) — retargeted in the Operations phase only, after Construction has proven steps 1-3.
|
||||
|
||||
*(Final unit grouping is confirmed in Units Generation — this is the dependency-respecting order that stage should produce.)*
|
||||
|
||||
---
|
||||
|
||||
## Estimated Timeline
|
||||
- **Total Phases**: 3 (Inception remainder, Construction, Operations)
|
||||
- **Estimated Duration**: Not tracked in calendar time for this workflow — driven by stage-by-stage approval, not a schedule.
|
||||
|
||||
## Success Criteria
|
||||
- **Primary Goal**: `SlpModularCms.Api.SlpSoftware` exists, hosts Core/Identity/Availability/Master/Offerings, serves the public `GET /api/v1/offerings` and authenticated admin CRUD, without regressing `SlpModularCms.Api`.
|
||||
- **Key Deliverables**: Shared `CmsHost.Configure(...)` in `Core`; `SlpModularCms.Api.SlpSoftware` project in `Clients`; `SlpModularCms.Modules.Offerings` (+ `.Tests`) in `Application/Modules` / `Tests/Modules`; documented reference content (FR-8); retargeted CI/CD pipeline (Operations).
|
||||
- **Quality Gates**: Full Security Baseline compliance (per requirements.md); `Api`'s existing test suite green after the extraction; new module's own test coverage (NFR-2); Build and Test phase integration checks.
|
||||
- **Integration Testing**: `Api.SlpSoftware` serving all five modules together, same-origin site + `/admin` + `/api/v1`, matches `Api`'s existing behavior for the four pre-existing modules.
|
||||
- **Operational Readiness**: CI/CD pipeline successfully building/deploying `Api.SlpSoftware`; monitoring/alerting confirmed to cover the new surfaces.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Story Generation Plan — SlpSoftware Production API
|
||||
|
||||
Dit plan beschrijft hoe de user stories en persona's voor deze feature worden opgesteld. Beantwoord eerst de vragen hieronder; na jouw goedkeuring wordt dit plan stap voor stap uitgevoerd.
|
||||
|
||||
## Uitvoeringschecklist
|
||||
|
||||
- [x] Stap A — Persona's definiëren (`personas.md`): Site Visitor (anonieme bezoeker marketingsite) en CMS Administrator (Administrator-rol, beheert offerings via `/admin`)
|
||||
- [x] Stap B — Stories voor de Site Visitor-persona (consumptie van `GET /api/v1/offerings`, incl. leeg-resultaat-scenario)
|
||||
- [x] Stap C — Stories voor de CMS Administrator-persona (aanmaken, bewerken, verwijderen, herordenen van offerings, incl. de "featured"-regel)
|
||||
- [x] Stap D — Acceptatiecriteria per story toevoegen (Given/When/Then, zie Vraag 2)
|
||||
- [x] Stap E — Persona's koppelen aan bijbehorende stories
|
||||
- [x] Stap F — Zelfcontrole: elke story voldoet aan INVEST (Independent, Negotiable, Valuable, Estimable, Small, Testable)
|
||||
- [x] Stap G — `stories.md` en `personas.md` opslaan onder `aidlc-docs/features/slpsoftware-api/inception/user-stories/`
|
||||
|
||||
## Aanpak-opties voor storyopbouw
|
||||
|
||||
- **Persona-based** (aanbevolen): stories gegroepeerd per persona (Site Visitor / CMS Administrator) — sluit direct aan op de twee duidelijk verschillende gebruikersrollen uit requirements.md.
|
||||
- **Feature-based**: stories gegroepeerd per capability (lezen, aanmaken, bewerken, verwijderen, herordenen) ongeacht wie de actor is.
|
||||
- **Hybride**: epics per persona, met feature-based sub-stories eronder.
|
||||
|
||||
---
|
||||
|
||||
## Vragen
|
||||
|
||||
### Vraag 1 — Storyopbouw
|
||||
Welke aanpak voor het groeperen van de stories heeft je voorkeur?
|
||||
|
||||
A) Persona-based (aanbevolen) — twee groepen: Site Visitor en CMS Administrator
|
||||
B) Feature-based — gegroepeerd per capability (lezen/aanmaken/bewerken/verwijderen/herordenen)
|
||||
C) Hybride — epics per persona met feature-based sub-stories
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: a
|
||||
|
||||
### Vraag 2 — Detailniveau acceptatiecriteria
|
||||
Welk format voor acceptatiecriteria per story?
|
||||
|
||||
A) Given/When/Then (aanbevolen — direct bruikbaar als testscenario in latere fases)
|
||||
B) Simpele bullet-checklist per story (sneller te lezen, minder gestructureerd)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Vraag 3 — "Exactly one featured" regel
|
||||
De externe hand-off-doc noemt: "Exactly one package in the list should have `featured: true`" — maar de frontend handhaaft dit niet zelf. Moet de admin-CRUD (bij het aanmaken/bewerken) dit afdwingen?
|
||||
|
||||
A) Ja — bij het instellen van `featured` op een offering wordt automatisch de vorige featured-offering ontfeatured (systeem garandeert altijd precies 0 of 1 featured item)
|
||||
B) Nee — geen afdwinging; de admin is zelf verantwoordelijk, het systeem staat 0, 1 of meerdere featured offerings toe
|
||||
C) Waarschuwen, niet blokkeren — het systeem staat meerdere featured offerings toe maar toont een duidelijke waarschuwing in de admin-UI
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Vraag 4 — Lege lijst op de publieke endpoint
|
||||
Wat moet er gebeuren als de admin alle offerings verwijdert, zodat `GET /api/v1/offerings` een lege array `[]` teruggeeft?
|
||||
|
||||
A) Toestaan — een lege array is een geldige response; de marketingsite toont dan geen pakket-cards (frontend-verantwoordelijkheid, niet iets wat de API moet voorkomen)
|
||||
B) Voorkomen — de admin kan de laatste overgebleven offering niet verwijderen (systeem blokkeert dit met een duidelijke foutmelding)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Vraag 5 — Herordenen (reorder)
|
||||
Hoe moet de admin de volgorde van offerings (het `DisplayOrder`-veld uit FR-5) kunnen aanpassen?
|
||||
|
||||
A) Drag-and-drop in de lijst-view van de admin-UI
|
||||
B) Expliciete "omhoog"/"omlaag"-knoppen per rij
|
||||
C) Een numeriek volgorde-veld dat de admin direct invult bij het aanmaken/bewerken
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A en B
|
||||
|
||||
### Vraag 6 — Persona-naam voor de beheerder
|
||||
Welke naam/omschrijving past het best bij de admin-persona, gezien de `AdminOnly`-policy (Administrator-rol) uit requirements.md?
|
||||
|
||||
A) "CMS Administrator" — generieke, rol-neutrale naam
|
||||
B) "Site Owner" — benadrukt dat het (voorlopig) waarschijnlijk de eigenaar zelf is die dit gebruikt
|
||||
X) Anders (geef zelf een naam op na de [Answer]:-tag)
|
||||
|
||||
[Answer]: CMS Beheerder, als we het nederlands willen houden
|
||||
@@ -0,0 +1,52 @@
|
||||
# Unit of Work Plan — SlpSoftware Production API
|
||||
|
||||
Dit plan beschrijft hoe het systeem wordt opgedeeld in units of work voor de Construction-fase. Beantwoord eerst de vragen; na goedkeuring wordt het plan uitgevoerd.
|
||||
|
||||
**Al besliste punten, hier niet opnieuw bevraagd** (met onderbouwing waarom een vraag overbodig zou zijn):
|
||||
- **Volgorde tussen units**: al vastgelegd in `inception/plans/execution-plan.md` (Module Update Strategy) — de Foundation-unit (Core-extractie + `Api.SlpSoftware`-skelet) moet eerst landen en geverifieerd worden tegen `Api`'s bestaande gedrag, vóórdat de Offerings-unit erbovenop gebouwd wordt. Geen nieuwe ambiguïteit sinds die analyse.
|
||||
- **Wie voegt de project-reference naar `Modules.Offerings` toe aan `Api.SlpSoftware.csproj`**: dit moet de Offerings-unit zelf doen (niet de Foundation-unit), simpelweg omdat die referentie niet kan compileren vóórdat het Offerings-project bestaat. Geen keuzevraag, een logische noodzaak.
|
||||
- **Teamafstemming (Team Alignment-categorie)**: N/A — dit is een solo-project (jij bent de enige ontwikkelaar/reviewer), er zijn geen team-ownership-grenzen te bepalen.
|
||||
- **Code-organisatiestrategie (greenfield-only categorie)**: N/A — dit is een brownfield-feature; de mapstructuur ligt al vast via `CLAUDE.md`/`AGENTS.md` (Application/Modules, Tests/Modules, Clients).
|
||||
|
||||
## Uitvoeringschecklist
|
||||
|
||||
- [x] Stap A — `unit-of-work.md`: unit-definities en verantwoordelijkheden
|
||||
- [x] Stap B — `unit-of-work-dependency.md`: afhankelijkheidsmatrix tussen units
|
||||
- [x] Stap C — `unit-of-work-story-map.md`: koppeling van elke user story (US-01..US-12) en relevante FR's aan een unit
|
||||
- [x] Stap D — Valideren: zijn alle stories toegewezen, kloppen de grenzen met application-design.md?
|
||||
|
||||
---
|
||||
|
||||
## Vragen
|
||||
|
||||
### Vraag 1 — Unit-indeling
|
||||
Op basis van requirements.md en application-design.md stel ik twee units voor: **Foundation** (`CmsHost`-extractie in Core + het `Api.SlpSoftware`-projectskelet, FR-1/FR-2/FR-3) en **Offerings** (de volledige nieuwe module, FR-4 t/m FR-8, alle 12 user stories). Dit sluit aan bij de Module Update Strategy uit Workflow Planning: Foundation moet eerst en heeft het meeste regressierisico op de bestaande `Api`; Offerings is de nieuwe, op zichzelf staande module.
|
||||
|
||||
Welke indeling heeft je voorkeur?
|
||||
|
||||
A) Twee units (aanbevolen) — Foundation en Offerings, zoals hierboven beschreven
|
||||
B) Eén gecombineerde unit — alles in één keer (Core-extractie, nieuw project, nieuwe module) als één ontwerp/codegeneratie-traject
|
||||
C) Drie units — Foundation opsplitsen in "Core-extractie" en "Api.SlpSoftware-projectskelet" als aparte units
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Vraag 2 — Naamgeving van de units
|
||||
Bij optie A of C hierboven, welke namen passen het best?
|
||||
|
||||
A) "Client Hosting Foundation" en "Offerings Module" (technisch, beschrijft wat de unit doet)
|
||||
B) "SlpSoftware Client Setup" en "Offerings" (korter, gekoppeld aan het eindresultaat)
|
||||
X) Anders (geef zelf namen op na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
### Vraag 3 — Unit zonder eigen user stories
|
||||
De Foundation-unit host geen enkele van de 12 user stories rechtstreeks (die horen allemaal bij de Offerings-functionaliteit) — Foundation bestaat puur om FR-1/FR-2/FR-3 (nieuw project + gedeelde hosting-extractie) te realiseren, zonder zichtbaar persona-voordeel op zich.
|
||||
|
||||
Is het acceptabel dat een unit in `unit-of-work-story-map.md` geen enkele story toegewezen krijgt (wel FR's), of geef je de voorkeur aan een andere aanpak?
|
||||
|
||||
A) Ja, prima — Foundation krijgt FR-1/FR-2/FR-3 toegewezen in de story-map, geen user stories; dat is een geldige, verwachte situatie voor een puur technische enabling-unit
|
||||
B) Nee — voeg Foundation samen met Offerings tot één unit, zodat elke unit minstens één user story heeft (impliceert antwoord B bij Vraag 1)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
@@ -0,0 +1,24 @@
|
||||
# User Stories Assessment
|
||||
|
||||
## Request Analysis
|
||||
- **Original Request**: New `SlpModularCms.Api.SlpSoftware` client hosting a new `SlpModularCms.Modules.Offerings` module: a public unauthenticated `GET /api/v1/offerings` endpoint plus admin CRUD (create/edit/delete/reorder) for offering content.
|
||||
- **User Impact**: Direct — two distinct user types interact with this feature: anonymous website visitors (consumers of the public endpoint, indirectly via the external frontend) and CMS administrators (direct users of the new admin CRUD screens).
|
||||
- **Complexity Level**: Complex (per requirements.md Intent Analysis)
|
||||
- **Stakeholders**: The user (product owner + sole admin operator today), plus the external `SlpSoftware` frontend as a technical consumer of the public contract.
|
||||
|
||||
## Assessment Criteria Met
|
||||
- [x] High Priority: **New User Features** — the admin CRUD screens are entirely new functionality (requirements FR-7).
|
||||
- [x] High Priority: **Customer-Facing APIs** — `GET /api/v1/offerings` is consumed by an external system (requirements FR-6).
|
||||
- [x] High Priority: **Multi-Persona Systems** — anonymous site visitor vs. authenticated CMS administrator have different needs and acceptance criteria.
|
||||
- [x] Medium Priority / Complexity Assessment: **Ambiguity** — requirements intentionally left some admin-UX details open (e.g. how "featured" exclusivity and reordering are enforced), which acceptance criteria can resolve concretely.
|
||||
- [x] Benefits: Clear acceptance criteria for the "exactly one featured" business rule (hand-off doc) and for delete/reorder edge cases, which are exactly the kind of detail that's easy to get wrong without a story-level decision.
|
||||
|
||||
## Decision
|
||||
**Execute User Stories**: Yes
|
||||
**Reasoning**: Meets multiple High Priority criteria outright (new user-facing admin feature, customer-facing API, multi-persona), and there are genuine open UX/business-rule questions (featured-flag exclusivity, empty-state handling, reorder UX) that are better resolved as acceptance criteria now than left ambiguous into Application Design or Code Generation.
|
||||
|
||||
## Expected Outcomes
|
||||
- A concrete, testable acceptance-criteria decision for the "exactly one featured offering" rule (currently only a soft expectation in the external hand-off doc).
|
||||
- A concrete decision for what happens to the public endpoint when zero offerings exist.
|
||||
- A concrete decision for the reorder interaction/persistence model, feeding directly into FR-5's `DisplayOrder` field and FR-7's admin CRUD design.
|
||||
- Two clear personas (Site Visitor, CMS Administrator) that later design/code-generation stages can reference instead of re-deriving "who is this for" each time.
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# Requirements Clarification Questions — SlpSoftware Production API
|
||||
|
||||
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past en beschrijf dan je voorkeur.
|
||||
|
||||
Waar ik iets al uit de code, de solution-structuur of de externe handoff-doc kon opmaken, staat dat als context boven de vraag — dan hoef je vaak alleen te bevestigen of te corrigeren.
|
||||
|
||||
---
|
||||
|
||||
## A. Verhouding tussen `SlpModularCms.Api` en de nieuwe `SlpModularCms.Api.SlpSoftware`
|
||||
|
||||
### Question 1
|
||||
**Context**: ik heb de `.sln` nagekeken. De **Clients**-solution folder bestaat al, maar bevat momenteel **nul projecten** — hij staat leeg te wachten. `SlpModularCms.Api` en `SlpModularCms.Api.Slave` zitten vandaag allebei onder **Application** (samen met `Core` en de `Modules`-submap), precies zoals `CLAUDE.md` het beschrijft: "Development versions of the applications". M.a.w.: de structuur is al voorbereid op precies deze feature.
|
||||
|
||||
Klopt mijn lezing dat `SlpModularCms.Api.SlpSoftware` het **eerste** project wordt dat ooit in Clients komt, en dat `SlpModularCms.Api` gewoon blijft staan waar hij staat (Application, ongewijzigde rol als dev-host)?
|
||||
|
||||
A) Ja — klopt precies zo
|
||||
B) Nee — `SlpModularCms.Api` moet zelf verplaatst/hernoemd worden naar Clients in plaats van een apart nieuw project
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Question 2
|
||||
**Context**: `SlpModularCms.Api` host vandaag vier modules: Core, Identity, Availability en Master (zie `Program.cs` / module-orchestrator). De admin-CMS (login, content-beheer) heeft dus sowieso auth (Identity) en de bestaande availability-gate nodig.
|
||||
|
||||
Moet `SlpModularCms.Api.SlpSoftware` dezelfde vier modules hosten (Core + Identity + Availability + Master) plus de nieuwe module, of ontbreekt er iets bewust?
|
||||
|
||||
A) Ja — zelfde vier modules + de nieuwe module
|
||||
B) Nee, er moet iets weg of anders (beschrijf hieronder)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Question 3
|
||||
**Context**: `SlpModularCms.Api/Program.cs` bevat inmiddels een flinke samengestelde pipeline (static content + SPA-fallback voor `/admin` én `wwwroot/web/`, health checks, security headers/CSP, rate limiting, Sentry, Data Protection, startup-migraties, module-orchestrator) — grotendeels gebouwd tijdens de `gitea-deployment-workflow`-feature. Als `SlpModularCms.Api.SlpSoftware` straks hetzelfde moet doen, kan dat op twee manieren.
|
||||
|
||||
Hoe wil je omgaan met deze hosting-/pipeline-code tussen de twee Client-projecten?
|
||||
|
||||
A) Extraheer de gedeelde samenstelling naar een herbruikbare methode in `SlpModularCms.Core` (bijv. iets als `CmsHost.Configure(...)`), zodat beide `Program.cs`-bestanden dun blijven en niet uit elkaar kunnen groeien — kost wat refactorwerk nu, maar voorkomt duplicatie en drift
|
||||
B) Dupliceer `Program.cs` gewoon naar het nieuwe project (sneller nu, maar toekomstige pipeline-wijzigingen moeten dan op twee plekken worden doorgevoerd)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## B. Scope van de nieuwe module (op basis van `packages-api-handoff.md`)
|
||||
|
||||
### Question 4
|
||||
**Context**: de handoff-doc in de andere workspace vraagt letterlijk alleen om een publieke, unauthenticated `GET /api/v1/packages` — geen mutatie-endpoints, "CMS authoring is out of scope for the marketing site itself" staat er expliciet bij. Maar de hele reden dat dit een CMS-endpoint wordt (in plaats van hardcoded blijven) is dat de content beheerbaar moet zijn.
|
||||
|
||||
Wat moet deze feature opleveren voor het **beheren** van package-content?
|
||||
|
||||
A) Alleen de publieke `GET`-endpoint + geseede content (exact zoals de handoff vraagt) — CRUD/admin-UI voor packages is een latere, aparte feature
|
||||
B) Ook admin-CRUD nu meenemen (aanmaken/bewerken/verwijderen/herordenen van packages via de admin-SPA), zodat er direct een reden is dat dit "CMS-beheerd" is
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
### Question 5
|
||||
**Context**: de handoff-doc noemt `content.ts` (in de andere workspace) als bron van de drie huidige, live pakketten (`pakket_01` Landingspagina, `pakket_02` Website, `pakket_03` Maatwerk) en zegt expliciet: gebruik dat bestand als seed-data zodat de site niet verandert zodra het endpoint live gaat.
|
||||
|
||||
Moet deze feature die drie pakketten automatisch seeden (bijv. via een EF-migratie of startup-seed), of is handmatige invoer later acceptabel?
|
||||
|
||||
A) Automatisch seeden met de exacte waarden uit de handoff-doc (ik geef de drie teksten door / je leest ze uit de referentie-workspace)
|
||||
B) Niet automatisch seeden — content komt er later handmatig in
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: x, ik doe het zelf, maar wil wel beginnen met de waarden die nu worden gebruikt dus dat moet wel ergens vast worden gelegd/worden behouden.
|
||||
|
||||
### Question 6
|
||||
**Context**: de handoff-doc noemt als open item dat er een nginx `location /api/v1/ { proxy_pass ... }` moet worden toegevoegd, omdat die workspace er vanuit gaat dat de Pi's vandaag alleen statische bestanden serveren. Maar in **dit** repo (zie `WEBSITE_WORKSPACE.md` en de — grotendeels al gemergde — `gitea-deployment-workflow`-feature) wordt de site (`wwwroot/web/`) al same-origin door **dezelfde** Client-API geserveerd die ook `/api/v1` en `/admin` bedient; er is dus al geen aparte nginx-proxy voor de API nodig zodra die Client-API de gedeployde host is.
|
||||
|
||||
Klopt mijn lezing dat dit "open item" uit de externe handoff-doc bij ons al is opgelost door de bestaande architectuur, zodra `SlpModularCms.Api.SlpSoftware` de gedeployde host wordt — en dat er dus geen extra nginx-wijziging nodig is?
|
||||
|
||||
A) Ja, klopt — geen extra nginx-config nodig, zolang de juiste Client-API wordt gedeployed
|
||||
B) Nee, er zit een addertje onder het gras (beschrijf hieronder)
|
||||
X) Anders / weet ik niet zeker — laten we dit samen checken tegen de echte nginx-config op de Pi
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## C. Deploy-retarget en levenscyclus van `SlpModularCms.Api`
|
||||
|
||||
### Question 7
|
||||
**Context**: je zei dat `Api.SlpSoftware` "uiteindelijk" de gedeployde API moet worden — dat klinkt alsof het omzetten van de CI/CD-pipeline (`.gitea/workflows/deploy-scp.yaml`, `continuous_integration.yaml`, en `deployment-instructions.md` — allemaal eigendom van de `gitea-deployment-workflow`-feature, momenteel gericht op `SlpModularCms.Api`) niet per se in déze feature hoeft te zitten.
|
||||
|
||||
Hoort het daadwerkelijk omzetten van de CI/CD-pipeline naar `Api.SlpSoftware` bij deze feature (Operations-fase), of is dat expliciet een latere, aparte stap?
|
||||
|
||||
A) Ja, neem de CI/CD-omzetting mee in de Operations-fase van déze feature (uitbreiden op de bestaande pipeline, niet dupliceren)
|
||||
B) Nee — deze feature levert alleen het nieuwe project + de nieuwe module op; de omzetting van de pipeline is een aparte, latere feature
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Question 8
|
||||
Moet `SlpModularCms.Api` (de huidige dev-host) op termijn verdwijnen zodra `Api.SlpSoftware` bewezen in productie draait, of blijft hij net als `Api.Slave` gewoon permanent bestaan als lokale dev-tool?
|
||||
|
||||
A) `SlpModularCms.Api` blijft permanent bestaan als lokale dev-host (zelfde rol als vandaag, geen verwijdering gepland)
|
||||
B) `SlpModularCms.Api` is op termijn kandidaat om verwijderd te worden — noteer dit als toekomstige tech debt, niet nu oppakken
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## D. Naming en techniek van de nieuwe module
|
||||
|
||||
### Question 9
|
||||
Hoe moet de nieuwe module heten? Op basis van de scope (package/pricing-kaarten voor de marketingsite) stel ik `SlpModularCms.Modules.Packages` voor.
|
||||
|
||||
A) `SlpModularCms.Modules.Packages` (aanbevolen — beschrijft het domein, niet de specifieke site)
|
||||
B) `SlpModularCms.Modules.SlpSoftware` (koppelt de module aan de site zelf i.p.v. aan het domeinconcept)
|
||||
X) Anders (geef zelf een naam op na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A, al lijkt het me iets te generiek. Als je de naam leest zou hetr zomaar kunnen zijn dat ik het in de toekomst lees als andere packages. als een soort library module. Het is een dienst of service die je ermee moet aanleveren. Het is nu voor SlpSoftware, maar later wil ik het ook kunnen hergebruiken voor bijvoorbeeld een klant die fotografie doet en fotoshoot verkoopt. Dan wil ik deze module kunnen hergebruiken. Kan je eventueel nog wat andere suggesties doen?
|
||||
|
||||
### Question 10
|
||||
**Context**: `Availability` en `Master` hebben allebei hun eigen `DbContext` (module-isolatie is het bestaande patroon), draaiend op MariaDB via EF Core.
|
||||
|
||||
Moet de nieuwe module z'n eigen `DbContext` krijgen (zelfde isolatie-patroon), of past het beter bij een bestaande context?
|
||||
|
||||
A) Eigen `DbContext` (bijv. `PackagesDbContext`), consistent met Availability/Master
|
||||
B) Hergebruik een bestaande `DbContext` (geef aan welke)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## E. Beveiligingsextensie (standaardvraag van de workflow)
|
||||
|
||||
### Question 11
|
||||
Moeten de beveiligingsregels als harde vereisten worden afgedwongen voor dit project?
|
||||
|
||||
A) Ja — dwing alle BEVEILIGINGSREGELS af als blokkerende vereisten (aanbevolen voor productietoepassingen)
|
||||
B) Nee — sla alle BEVEILIGINGSREGELS over (geschikt voor PoC's, prototypes en experimentele projecten)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
## F. Property-Based Testing-extensie (standaardvraag van de workflow)
|
||||
|
||||
### Question 12
|
||||
Moeten de property-based testing (PBT) regels worden afgedwongen voor dit project?
|
||||
|
||||
A) Ja — dwing alle PBT-regels af als blokkerende vereisten (aanbevolen voor projecten met bedrijfslogica, datatransformaties, serialisatie of stateful componenten)
|
||||
B) Gedeeltelijk — dwing PBT-regels alleen af voor pure functies en serialisatie round-trips (geschikt voor projecten met beperkte algoritmische complexiteit)
|
||||
C) Nee — sla alle PBT-regels over (geschikt voor eenvoudige CRUD-applicaties, UI-only projecten of dunne integratielagen zonder significante bedrijfslogica)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: C
|
||||
|
||||
## G. Operations-fase (standaardvraag van de workflow)
|
||||
|
||||
### Question 13
|
||||
Moet deze feature na Construction ook door de Operations-fase (deployment- en monitoring-setup)?
|
||||
|
||||
**Let op**: dit bepaalt alleen of de Operations-fase van déze feature draait — Question 7 hierboven bepaalt of die Operations-fase ook echt de CI/CD-pipeline omzet naar `Api.SlpSoftware`, of alleen bijvoorbeeld lokale/documentatie-stappen bevat.
|
||||
|
||||
A) Ja — draai de Operations-fase na Construction
|
||||
B) Nee — stop na Build and Test (deployment/monitoring vallen buiten scope voor deze feature)
|
||||
C) Weet ik nog niet — vraag het me nogmaals na de Construction-fase
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Requirements Clarification Questions (Round 2) — SlpSoftware Production API
|
||||
|
||||
Vul je keuze in achter de `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past en geef dan zelf een naam op.
|
||||
|
||||
---
|
||||
|
||||
## Ambiguity 1: Modulenaam is bewust generieker bedoeld dan "Packages"
|
||||
|
||||
Bij Question 9 in de vorige ronde koos je optie A (`SlpModularCms.Modules.Packages`), maar met de kanttekening dat die naam te specifiek/verwarrend aanvoelt: je wilt de module op termijn kunnen hergebruiken voor een heel ander soort klant/dienst (bijv. een fotograaf die fotoshoot-pakketten verkoopt), en "Packages" leest dan al snel als "software packages/library" in plaats van "dingen die een bedrijf aanbiedt op zijn marketingsite". Je vroeg om alternatieve suggesties.
|
||||
|
||||
Kernidee van de module (los van de naam): een set **aanbiedingen/tiers met titel, beschrijving, prijs en features**, getoond op een marketingsite, per "tenant"/site herbruikbaar — dus geen SlpSoftware-specifieke naam, en geen naam die aanvoelt als een NuGet/library-package.
|
||||
|
||||
### Clarification Question 1
|
||||
Welke naam past het best bij dit generieke, herbruikbare concept?
|
||||
|
||||
A) `SlpModularCms.Modules.Offerings` — "wat een bedrijf aanbiedt" (product óf dienst), generiek en leest niet als software-package
|
||||
B) `SlpModularCms.Modules.ServiceCatalog` — benadrukt een catalogus van diensten/pakketten die getoond wordt
|
||||
C) `SlpModularCms.Modules.Pricing` — legt de nadruk op de prijstiers/pricing-cards zelf, minder op "wat" er verkocht wordt
|
||||
D) `SlpModularCms.Modules.Catalog` — maximaal generiek, bruikbaar voor vrijwel elk soort verkoopbaar item (niet alleen prijstiers)
|
||||
X) Anders (geef zelf een naam op na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
@@ -0,0 +1,167 @@
|
||||
# Requirements — SlpSoftware Production API
|
||||
|
||||
## Intent Analysis
|
||||
|
||||
- **User Request**: Add a new deployable API in the `Clients` solution folder, `SlpModularCms.Api.SlpSoftware`, that will eventually replace `SlpModularCms.Api` as the API deployed to `test.slpsoftware.nl` / `slpsoftware.nl`. It hosts the existing Master module plus a new module covering what the SlpSoftware website needs, per the external hand-off doc `packages-api-handoff.md`.
|
||||
- **Request Type**: New Feature (new Client project + new module), with a CI/CD retarget as a downstream consequence.
|
||||
- **Scope Estimate**: Multiple Components — new Client project, shared hosting-pipeline extraction in `SlpModularCms.Core`, a new module + its test project, and (in the Operations phase) an extension of the existing deployment pipeline.
|
||||
- **Complexity Estimate**: Complex — touches solution structure, an in-flight Operations-phase feature's deploy pipeline (`gitea-deployment-workflow`), and a cross-workspace content contract owned by another repo's frontend.
|
||||
|
||||
---
|
||||
|
||||
## System Context
|
||||
|
||||
- The `Clients` solution folder exists but is currently empty; `SlpModularCms.Api` and `SlpModularCms.Api.Slave` sit under `Application` today (per `CLAUDE.md` / `AGENTS.md`). `SlpModularCms.Api.SlpSoftware` will be the first project ever placed in `Clients`.
|
||||
- `SlpModularCms.Api` hosts four modules today: Core, Identity, Availability, Master (see `Program.cs` / module orchestrator), plus a composed hosting pipeline built during `gitea-deployment-workflow`: static content + SPA fallback for `/admin` and `wwwroot/web/`, health checks, security headers/CSP, rate limiting, Sentry, Data Protection, and startup migrations.
|
||||
- The external hand-off doc (`K:\Development\SlpSoftware\Projects\SlpSoftware\...\packages-api-handoff.md`, read-only reference) asks for a single public, unauthenticated `GET /api/v1/packages` backing the "Drie manieren om te starten" section of the website. The frontend already calls `fetch('/api/v1/packages')` with a relative, same-origin path.
|
||||
- That same reference workspace's `WEBSITE_WORKSPACE.md`, together with the (merged) `gitea-deployment-workflow` feature, establishes that in **this** repo the public site (`wwwroot/web/`) and the API are already served same-origin by one Client API process — so the hand-off doc's "add an nginx proxy_pass" open item does not apply here once `Api.SlpSoftware` is the deployed host (Q6 = A).
|
||||
- Roles available today (`HierarchicalRoleHandler`, `ServiceCollectionExtensions`): `Owner` (100) > `Administrator` (50) > `User` (10), with existing `OwnerOnly` / `AdminOnly` / `UserOnly` authorization policies.
|
||||
|
||||
---
|
||||
|
||||
## Decisions (traced to clarification questions)
|
||||
|
||||
| # | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-1 | `SlpModularCms.Api.SlpSoftware` is a new project in `Clients`; `SlpModularCms.Api` is unchanged and stays in `Application` as the dev host. | Q1 = A |
|
||||
| D-2 | `Api.SlpSoftware` hosts the same four modules as `Api` (Core, Identity, Availability, Master) plus the new module. | Q2 = A |
|
||||
| D-3 | The shared hosting/pipeline composition in `Program.cs` is extracted into a reusable method in `SlpModularCms.Core` (e.g. `CmsHost.Configure(...)`) so both Client `Program.cs` files stay thin and cannot drift apart. | Q3 = A |
|
||||
| D-4 | This feature includes admin CRUD (create/edit/delete/reorder) for the new content, not just the public read endpoint from the hand-off doc — that's the reason this becomes a CMS-managed module rather than staying hardcoded. | Q4 = B |
|
||||
| D-5 | No automatic seed migration for the three current packages. The user will enter content manually, but the **current live values must be preserved as documented reference data** so they aren't lost. Retrieved directly from the reference workspace's `content.ts` — see [Reference Content](#reference-content-not-auto-seeded) below. | Q5 = X (custom) |
|
||||
| D-6 | No nginx changes are needed for `/api/v1/` once `Api.SlpSoftware` is the deployed, same-origin host. | Q6 = A |
|
||||
| D-7 | Retargeting the CI/CD pipeline (`deploy-scp.yaml`, `continuous_integration.yaml`, `deployment-instructions.md` — currently owned by `gitea-deployment-workflow`) to `Api.SlpSoftware` is **in scope for this feature's Operations phase**, extending the existing pipeline rather than duplicating it. | Q7 = A |
|
||||
| D-8 | `SlpModularCms.Api` remains permanently as the local dev host, same role as `Api.Slave` today — no removal planned. | Q8 = A |
|
||||
| D-9 | The new module is named `SlpModularCms.Modules.Offerings` — deliberately domain-generic (not `Packages`, which reads as a software/library package, and not `SlpSoftware`-specific), since it must be reusable later for unrelated tenants/domains (the user's stated example: a photography business selling photoshoot packages). | Q9 = A (custom, follow-up round) |
|
||||
| D-10 | The module gets its own `DbContext` (e.g. `OfferingsDbContext`), consistent with the existing Availability/Master module-isolation pattern. | Q10 = A |
|
||||
| D-11 | The Security Baseline extension is **enabled and blocking** for this feature. | Q11 = A |
|
||||
| D-12 | The Property-Based Testing extension is **not enforced** for this feature. | Q12 = C |
|
||||
| D-13 | This feature runs the Operations phase after Construction. | Q13 = A |
|
||||
| D-14 | The public route is `/api/v1/offerings`, matching the module name, not `/api/v1/packages`. The corresponding frontend fetch-path change (`usePackagesQuery.ts`) in the external reference workspace is **out of scope for this feature** — the user will update it separately, on their own. | User feedback, post-Requirements-approval; confirmed via chat clarification (frontend update: "dat regel ik zelf") |
|
||||
| D-15 | The CI/CD pipeline retarget (FR-9) is a **cutover**: `SlpModularCms.Api` is replaced by `SlpModularCms.Api.SlpSoftware` as the pipeline's build/deploy target, not run side by side. | User feedback, post-Requirements-approval |
|
||||
|
||||
---
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### FR-1 — New Client Project: `SlpModularCms.Api.SlpSoftware`
|
||||
A new project `SlpModularCms.Api.SlpSoftware` is created under the `Clients` solution folder (first project ever placed there), per D-1. `SlpModularCms.Api` is not moved, renamed, or otherwise modified in role.
|
||||
|
||||
### FR-2 — Module Composition
|
||||
`SlpModularCms.Api.SlpSoftware` references and hosts: `SlpModularCms.Core`, `SlpModularCms.Modules.Identity`, `SlpModularCms.Modules.Availability`, `SlpModularCms.Modules.Master`, and the new `SlpModularCms.Modules.Offerings` (D-2, D-9).
|
||||
|
||||
### FR-3 — Shared Hosting Pipeline Extraction
|
||||
The composed hosting pipeline currently duplicated in `SlpModularCms.Api/Program.cs` (static content + SPA fallback for `/admin` and `wwwroot/web/`, health checks, security headers/CSP, rate limiting, Sentry, Data Protection, startup migrations, module orchestrator) is extracted into a reusable method in `SlpModularCms.Core` (e.g. `CmsHost.Configure(...)`). Both `SlpModularCms.Api/Program.cs` and `SlpModularCms.Api.SlpSoftware/Program.cs` call this shared method; project-specific differences (e.g. which modules are registered) remain explicit at each call site (D-3).
|
||||
|
||||
### FR-4 — New Module: `SlpModularCms.Modules.Offerings`
|
||||
A new module `SlpModularCms.Modules.Offerings` is created following the existing `IModule` pattern, with its test project `SlpModularCms.Modules.Offerings.Tests` under `Tests/Modules` (per `CLAUDE.md` structure rules). The module owns an `Offering` entity and its own `OfferingsDbContext` + migrations, isolated from other modules' schemas (D-9, D-10).
|
||||
|
||||
### FR-5 — `Offering` Entity
|
||||
The `Offering` entity carries the fields required by the public API contract (see FR-6) and by admin management (FR-7): a stable slug-like `Id`, `Title`, `Description`, `Price` (pre-formatted display string, not a number — e.g. `"€ 300"` or `"Op maat"`), `PriceNote`, an ordered list of `Features` (plain text), `CtaLabel`, a `Featured` flag, and an explicit `DisplayOrder` (or equivalent ordering field) so admin reordering (FR-7) has something durable to persist, independent of the public array's rendered order.
|
||||
|
||||
### FR-6 — Public Endpoint: `GET /api/v1/offerings`
|
||||
A public, unauthenticated `GET /api/v1/offerings` endpoint returns a JSON array of offerings in display order, matching the field contract from the hand-off doc:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "pakket_01",
|
||||
"title": "Landingspagina",
|
||||
"description": "Eén overtuigende pagina die je product of dienst helder neerzet.",
|
||||
"price": "€ 300",
|
||||
"priceNote": "eenmalig, excl. btw",
|
||||
"features": ["Eén pagina in HTML & CSS", "..."],
|
||||
"ctaLabel": "Kies landingspagina",
|
||||
"featured": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Note on the frontend dependency**: the frontend in the external, read-only reference workspace currently fetches the old path (`fetch('/api/v1/packages')`). Changing the route to `/api/v1/offerings` (D-14) means that fetch call needs to change too — the user has confirmed this is **out of scope for this feature**; they will update it separately in that workspace. Until that frontend change happens, the website's package section will show its error state against a deployed `Api.SlpSoftware`, same as today against no backend at all.
|
||||
|
||||
### FR-7 — Admin CRUD for Offerings
|
||||
The admin SPA (served under `/admin` by the same Client API, per the existing hosting pipeline) gets management screens to create, edit, delete, and reorder offerings, backed by authenticated API endpoints (`POST` / `PUT` / `DELETE` / a reorder operation on `/api/admin/offerings` or equivalent). These endpoints require the existing `AdminOnly` authorization policy (`Administrator` role or higher), consistent with other content-management operations in this system (D-4).
|
||||
|
||||
### FR-8 — Reference Content (Not Auto-Seeded)
|
||||
|
||||
<a id="reference-content-not-auto-seeded"></a>
|
||||
|
||||
No database seed migration is created (D-5). Instead, the three packages currently live on the website are captured here as the reference values the user will enter manually through the new admin CRUD (FR-7), retrieved from the external reference workspace's `content.ts`:
|
||||
|
||||
| `id` | `title` | `description` | `price` | `priceNote` | `features` | `ctaLabel` | `featured` |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `pakket_01` | Landingspagina | Eén overtuigende pagina die je product of dienst helder neerzet. | € 300 | eenmalig, excl. btw | Eén pagina in HTML & CSS; Ontwerp op maat, geen template; Responsive op elk scherm; Snelle laadtijd & SEO-basis | Kies landingspagina | false |
|
||||
| `pakket_02` | Website | Een complete website met meerdere pagina's, klaar om te groeien. | € 750 | eenmalig, excl. btw | Drie pagina's in HTML & CSS; Extra pagina's als optie bij te bestellen; Consistente huisstijl over alle pagina's; Responsive, snel & SEO-basis | Kies website | true |
|
||||
| `pakket_03` | Maatwerk | Grotere websites, een eigen back-end of andere .NET-projecten. | Op maat | offerte na intake | Grotere websites & webapplicaties; Losse back-end in .NET / C#; Koppelingen & API's; Advies over de beste aanpak | Vraag offerte aan | false |
|
||||
|
||||
Per the hand-off doc's business rule BR-4 (content fidelity), these values must be reproduced exactly, not paraphrased, if/when entered.
|
||||
|
||||
### FR-9 — CI/CD Retarget (Operations Phase)
|
||||
The existing Gitea Actions pipeline (`continuous_integration.yaml`, `deploy-scp.yaml`) and `deployment-instructions.md` — currently targeting `SlpModularCms.Api`, owned by the `gitea-deployment-workflow` feature — is extended (not duplicated) to build, test, and deploy `SlpModularCms.Api.SlpSoftware` as the artifact shipped to `test.slpsoftware.nl` / `slpsoftware.nl`. This is executed in this feature's Operations phase, coordinated with `gitea-deployment-workflow` rather than forking its pipeline (D-7). Per D-15, this is a **cutover**: the pipeline's build/deploy target switches from `SlpModularCms.Api` to `SlpModularCms.Api.SlpSoftware` — it does not build and deploy both APIs side by side.
|
||||
|
||||
---
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### NFR-1 — No Nginx Changes Required
|
||||
No nginx `location /api/v1/` proxy is added. `Api.SlpSoftware` serves the site, `/admin`, and `/api/v1` same-origin from one process, matching the existing `Api` hosting model (D-6).
|
||||
|
||||
### NFR-2 — Module Test Coverage
|
||||
`SlpModularCms.Modules.Offerings` follows this project's existing test coverage standard for new modules, with unit tests for the `Offering` entity, `OfferingsDbContext`, the public read endpoint, and the admin CRUD endpoints (including authorization checks).
|
||||
|
||||
### NFR-3 — Long-Term Reusability of the Offerings Module
|
||||
The module's naming, entity design, and any tenant/site-scoping must not bake in SlpSoftware-specific assumptions (e.g. hardcoded copy, hardcoded routes beyond the one fixed contract in FR-6), since the user's stated intent is to reuse this module for unrelated future sites/domains (D-9).
|
||||
|
||||
### NFR-4 — Property-Based Testing Not Required
|
||||
No PBT tooling or rules are introduced for this feature's tests (D-12).
|
||||
|
||||
---
|
||||
|
||||
## Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|---|---|---|
|
||||
| SECURITY-01 Encryption at rest/transit | **Pre-existing, unchanged** | `OfferingsDbContext` uses the same MariaDB connection (TLS-enforced) as Availability/Master; no new data store technology introduced. |
|
||||
| SECURITY-02 Access logging on intermediaries | **N/A** | No load balancer, API gateway, or CDN in this architecture — the application itself is the only network-facing component. |
|
||||
| SECURITY-03 Application-level logging | **Pre-existing, unchanged** | Reuses the shared logging/Sentry setup extracted in FR-3. |
|
||||
| SECURITY-04 HTTP security headers | **Pre-existing, unchanged** | Reuses the shared security-headers middleware extracted in FR-3; applies identically on `Api.SlpSoftware`. |
|
||||
| SECURITY-05 Input validation | **Addressed** | Admin CRUD endpoints (FR-7) validate all `Offering` fields (type, length bounds on `Title`/`Description`/`Price`/`PriceNote`/`CtaLabel`, array bounds on `Features`) and use EF Core parameterized queries. |
|
||||
| SECURITY-06 Least-privilege access policies | **Addressed** | Admin endpoints use the existing `AdminOnly` policy, not `OwnerOnly` or an ad-hoc broader policy (FR-7). |
|
||||
| SECURITY-07 Restrictive network configuration | **N/A** | No cloud networking/firewall resources introduced by this feature. |
|
||||
| SECURITY-08 Application-level access control | **Addressed** | The public `GET /api/v1/offerings` (FR-6) is deliberately and explicitly anonymous, matching the hand-off doc's requirement; all mutation endpoints (FR-7) require authentication and the `AdminOnly` policy, following deny-by-default. |
|
||||
| SECURITY-09 Hardening and misconfiguration | **Pre-existing, unchanged** | Reuses the shared pipeline's existing error handling (`GlobalExceptionHandler`) and Development-only OpenAPI/Scalar exposure. |
|
||||
| SECURITY-10 Supply chain | **Pre-existing, unchanged** | New module adds no new third-party dependencies beyond what the solution already uses (EF Core, ASP.NET Core); covered by the existing blocking vulnerability gate from `gitea-deployment-workflow`. |
|
||||
| SECURITY-11 Secure design | **Addressed** | Admin (security-critical) operations are isolated behind `AdminOnly`, distinct from the public read path; rate limiting is inherited from the shared pipeline (FR-3) and therefore also applies to the new public endpoint. |
|
||||
| SECURITY-12 Authentication and credentials | **Pre-existing, unchanged** | Reuses the existing Identity module; no new credential handling introduced. |
|
||||
| SECURITY-13 Software and data integrity | **Addressed** | Admin mutations to `Offering` records should be auditable at the same level as other content mutations in this system (who/when) — tracked as an open item (see below) if no existing audit mechanism covers module-level content changes. |
|
||||
| SECURITY-14 Alerting and monitoring | **Pre-existing, unchanged** | Reuses the shared Sentry-based alerting from `gitea-deployment-workflow`; no new alert categories introduced by this feature. |
|
||||
| SECURITY-15 Exception handling and fail-safe defaults | **Pre-existing, unchanged** | Reuses the shared `GlobalExceptionHandler`; new endpoints follow the same fail-closed pattern (auth failure → 401/403, not silent fallback). |
|
||||
|
||||
**Open item**: SECURITY-13 audit-trail coverage for `Offering` create/edit/delete/reorder is not yet confirmed against an existing system-wide audit mechanism (or its absence) — to be resolved at Functional Design / NFR Design for the Offerings unit, not blocking Requirements sign-off.
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
| In Scope | Out of Scope |
|
||||
|---|---|
|
||||
| New `SlpModularCms.Api.SlpSoftware` project in `Clients` | Moving/renaming `SlpModularCms.Api` |
|
||||
| Shared `CmsHost.Configure(...)`-style extraction in `SlpModularCms.Core` | Removing or deprecating `SlpModularCms.Api` |
|
||||
| New `SlpModularCms.Modules.Offerings` module + `SlpModularCms.Modules.Offerings.Tests` | Multi-tenant/site-scoping infrastructure for reuse by a future, unrelated site (only the *naming/design* must not preclude it — D-9, NFR-3) |
|
||||
| `Offering` entity + `OfferingsDbContext` + migrations | Automatic data seeding of the three current packages (D-5) |
|
||||
| Public `GET /api/v1/offerings` (unauthenticated) | nginx configuration changes (D-6) |
|
||||
| Admin CRUD (create/edit/delete/reorder) for offerings, `AdminOnly`-protected | Frontend changes in the external `SlpSoftware` reference workspace, **including** updating `usePackagesQuery.ts`'s fetch path to `/api/v1/offerings` (D-14 — user's own follow-up) |
|
||||
| CI/CD retarget of the existing pipeline to `Api.SlpSoftware` as a **cutover** (Operations phase, D-15) | Running `Api` and `Api.SlpSoftware` pipelines side by side |
|
||||
| Documenting the current live package copy as reference content (FR-8) | Entering that content into the running system (the user will do this manually) |
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
| # | Assumption | Risk if wrong |
|
||||
|---|---|---|
|
||||
| ~~A-1~~ | ~~The public route stays `/api/v1/packages` even though the module is `Offerings`.~~ **Resolved 2026-08-01**: route is `/api/v1/offerings` (D-14); frontend update is the user's own, separate follow-up. | Closed |
|
||||
| A-2 | Admin CRUD for offerings requires `AdminOnly` (Administrator role), not `OwnerOnly`. | Low-medium — if the user wants offering management restricted to Owner only, this is a one-line policy change, best confirmed at Application Design. |
|
||||
|
||||
---
|
||||
|
||||
**9 functional requirements, 4 non-functional requirements, 15 traced decisions, 1 open assumption (1 resolved), 1 open item (security audit-trail coverage).**
|
||||
@@ -0,0 +1,28 @@
|
||||
# Personas — SlpSoftware Production API
|
||||
|
||||
## Persona 1: Site Visitor
|
||||
|
||||
| Attribute | Description |
|
||||
|---|---|
|
||||
| **Role** | Anonymous visitor of the public website (`test.slpsoftware.nl` / `slpsoftware.nl`) |
|
||||
| **Access Level** | None — unauthenticated, public |
|
||||
| **Goal** | Understand what services/packages are on offer, their price and what's included, so they can decide which one fits their needs and get in touch. |
|
||||
| **Pain Points** | A broken or empty "Drie manieren om te starten" section erodes trust before the visitor even reaches the contact form. |
|
||||
| **Technical Context** | Interacts only through the existing React frontend (external, read-only reference workspace) — never calls the API directly. All API behavior is experienced indirectly through what the frontend renders. |
|
||||
| **Relationship to this feature** | Consumes `GET /api/v1/offerings` (FR-6) indirectly. Never touches the admin CRUD (FR-7). |
|
||||
|
||||
## Persona 2: CMS Administrator
|
||||
|
||||
| Attribute | Description |
|
||||
|---|---|
|
||||
| **Role** | Authenticated user holding the `Administrator` role (or higher — `Owner`, per the existing role hierarchy) |
|
||||
| **Access Level** | `AdminOnly` policy — full CRUD access to offerings via the admin SPA under `/admin` |
|
||||
| **Goal** | Keep the list of offerings shown on the website accurate and up to date (pricing, features, copy) without needing a code deploy, and control which one is highlighted as "most chosen". |
|
||||
| **Pain Points** | Today this content is hardcoded in the frontend's `content.ts` — any change requires a frontend deploy. This feature removes that dependency. |
|
||||
| **Technical Context** | Uses the existing admin SPA (already served under `/admin` by the same Client API, per the shared hosting pipeline). Not a developer — needs a UI, not direct API/database access. |
|
||||
| **UI Language Note** | The existing admin SPA already ships with `nl`/`en` i18n locales (`frontend/src/i18n/locales/`), where the `Administrator` role is already labeled `"Beheerder"` in Dutch. This persona's documentation name ("CMS Administrator") is the English documentation term; the on-screen label the persona actually sees follows the existing i18n setup and needs no new translation work. |
|
||||
| **Relationship to this feature** | Sole user of the new admin CRUD (FR-7): create, edit, delete, reorder offerings, and control the `featured` flag. |
|
||||
|
||||
---
|
||||
|
||||
**2 personas**, matching the two distinct access levels established in requirements.md (public/anonymous vs. `AdminOnly`).
|
||||
@@ -0,0 +1,162 @@
|
||||
# User Stories — SlpSoftware Production API
|
||||
|
||||
Breakdown approach: **persona-based** (approved). Acceptance criteria format: **Given/When/Then** (approved).
|
||||
|
||||
---
|
||||
|
||||
## Epic A — Site Visitor: Viewing Offerings
|
||||
|
||||
### US-01 — View the list of available offerings
|
||||
**As a** Site Visitor, **I want** to see the current list of offerings on the website, **so that** I can compare what's available and pick one that fits my needs.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** one or more offerings exist in the CMS, **when** the frontend requests `GET /api/v1/offerings`, **then** the response is `200 OK` with a JSON array of offerings in display order, each including `id`, `title`, `description`, `price`, `priceNote`, `features`, `ctaLabel`, and `featured`.
|
||||
- **Given** the offerings are returned, **when** rendered, **then** the array order is the same order the CMS Administrator configured (FR-5 `DisplayOrder`) — no re-sorting happens client-side or server-side beyond that stored order.
|
||||
|
||||
**Traceability**: FR-5, FR-6
|
||||
|
||||
---
|
||||
|
||||
### US-02 — See the recommended offering highlighted
|
||||
**As a** Site Visitor, **I want** to see which offering is the "most chosen" one, **so that** I have a quick recommendation if I'm unsure which package to pick.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** the CMS Administrator has marked exactly one offering as featured, **when** `GET /api/v1/offerings` is called, **then** exactly one item in the response has `"featured": true` and all others have `"featured": false`.
|
||||
- **Given** no offering has been explicitly marked as featured, **when** `GET /api/v1/offerings` is called, **then** every item has `"featured": false` (no forced default featured item).
|
||||
|
||||
**Traceability**: FR-5, FR-6; enforcement mechanism defined in US-10
|
||||
|
||||
---
|
||||
|
||||
### US-03 — Website stays functional with zero offerings
|
||||
**As a** Site Visitor, **I want** the site to still work correctly even if no offerings have been configured yet, **so that** I don't encounter a broken page during initial setup or content maintenance.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** zero offerings exist in the CMS, **when** the frontend requests `GET /api/v1/offerings`, **then** the response is `200 OK` with an empty JSON array `[]` — not an error response.
|
||||
- **Given** this empty-array response, **then** it is the frontend's responsibility (out of scope for this feature) to decide how to render an empty state; the API's only obligation is a valid, non-error response.
|
||||
|
||||
**Traceability**: FR-6; decision Q4 = A (allow empty list, no deletion guard)
|
||||
|
||||
---
|
||||
|
||||
## Epic B — CMS Administrator: Managing Offerings
|
||||
|
||||
### US-04 — Create a new offering
|
||||
**As a** CMS Administrator, **I want** to create a new offering with title, description, price, price note, features, and CTA label, **so that** I can add a new package/service to the website without a code deploy.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** I am authenticated with at least the `Administrator` role, **when** I submit a new offering with all required fields (`Title`, `Description`, `Price`, `PriceNote`, at least one `Feature`, `CtaLabel`), **then** the offering is persisted and immediately appears in `GET /api/v1/offerings` at the end of the display order.
|
||||
- **Given** I create an offering without marking it featured, **when** it is saved, **then** `featured` defaults to `false`.
|
||||
|
||||
**Traceability**: FR-5, FR-7
|
||||
|
||||
---
|
||||
|
||||
### US-05 — Edit an existing offering
|
||||
**As a** CMS Administrator, **I want** to edit an existing offering's content, **so that** I can correct or update pricing and copy as the business changes.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** an existing offering, **when** I update any of its fields and save, **then** `GET /api/v1/offerings` reflects the new values on the next request.
|
||||
- **Given** I edit an offering, **when** I save it, **then** its `id` (stable slug) and `DisplayOrder` are not changed as a side effect of the edit — only reordering (US-08/US-09) changes order.
|
||||
|
||||
**Traceability**: FR-5, FR-7
|
||||
|
||||
---
|
||||
|
||||
### US-06 — Delete an offering
|
||||
**As a** CMS Administrator, **I want** to delete an offering that's no longer relevant, **so that** the website doesn't show outdated packages.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** more than one offering exists, **when** I delete one of them, **then** it no longer appears in `GET /api/v1/offerings`, and the remaining offerings keep their relative display order.
|
||||
|
||||
**Traceability**: FR-7
|
||||
|
||||
---
|
||||
|
||||
### US-07 — Delete the last remaining offering
|
||||
**As a** CMS Administrator, **I want** to be able to delete the last remaining offering if needed, **so that** I'm not blocked from clearing content during a redesign or content pause, even though it temporarily leaves the site with nothing to show.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** exactly one offering exists, **when** I delete it, **then** the deletion succeeds (no blocking validation error) and `GET /api/v1/offerings` subsequently returns `[]`.
|
||||
|
||||
**Traceability**: FR-7; decision Q4 = A (deletion is never blocked to prevent an empty list)
|
||||
|
||||
---
|
||||
|
||||
### US-08 — Reorder offerings via drag-and-drop
|
||||
**As a** CMS Administrator, **I want** to reorder offerings by dragging them into a new position in the list, **so that** I can control the order visitors see them in without editing a numeric field.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** two or more offerings, **when** I drag one to a new position and the change is saved, **then** the `DisplayOrder` values are updated so `GET /api/v1/offerings` reflects the new order.
|
||||
|
||||
**Traceability**: FR-5, FR-7; decision Q5 = A+B
|
||||
|
||||
---
|
||||
|
||||
### US-09 — Reorder offerings via up/down buttons
|
||||
**As a** CMS Administrator, **I want** an alternative to drag-and-drop — explicit "move up" / "move down" controls per row, **so that** I can reorder offerings accurately even without a mouse, or when drag-and-drop is impractical (accessibility, precision).
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** an offering that is not first in the list, **when** I use its "move up" control, **then** it swaps display order with the offering immediately before it.
|
||||
- **Given** an offering that is not last in the list, **when** I use its "move down" control, **then** it swaps display order with the offering immediately after it.
|
||||
- **Given** the first offering in the list, **then** its "move up" control is disabled (and symmetrically for "move down" on the last offering).
|
||||
|
||||
**Traceability**: FR-5, FR-7; decision Q5 = A+B (accessible fallback alongside drag-and-drop)
|
||||
|
||||
---
|
||||
|
||||
### US-10 — Mark an offering as featured (system-enforced exclusivity)
|
||||
**As a** CMS Administrator, **I want** the system to guarantee that at most one offering is marked "featured" at any time, **so that** I don't accidentally end up with a confusing website showing more than one "most chosen" badge (per the external hand-off doc's expectation, which the frontend itself does not enforce).
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** offering A is currently featured, **when** I mark offering B as featured, **then** offering A is automatically un-featured in the same operation — the system never persists more than one featured offering at a time.
|
||||
- **Given** no offering is currently featured, **when** I mark one as featured, **then** exactly that one becomes featured.
|
||||
- **Given** the currently featured offering, **when** I explicitly un-feature it (without featuring another), **then** zero offerings are featured — this is a valid state (see US-02).
|
||||
|
||||
**Traceability**: FR-5, FR-7; decision Q3 = A
|
||||
|
||||
---
|
||||
|
||||
### US-11 — Receive validation feedback on invalid input
|
||||
**As a** CMS Administrator, **I want** clear validation errors when I submit incomplete or malformed offering data, **so that** I can fix my mistake instead of silently corrupting the website's content.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** I submit an offering missing a required field (`Title`, `Description`, `Price`, `PriceNote`, `CtaLabel`, or `Features`), **when** I save, **then** the request is rejected with a validation error identifying which field(s) are invalid, and no partial record is persisted.
|
||||
- **Given** I submit a field exceeding its defined maximum length, **when** I save, **then** the request is rejected the same way.
|
||||
|
||||
**Traceability**: FR-7; SECURITY-05 (Input Validation)
|
||||
|
||||
---
|
||||
|
||||
### US-12 — Admin actions require the Administrator role
|
||||
**As a** CMS Administrator, **I want** offering management to be inaccessible to anyone without at least the `Administrator` role, **so that** unauthenticated visitors or lower-privileged users can't alter the website's content.
|
||||
|
||||
**Acceptance Criteria**
|
||||
- **Given** an unauthenticated request, **when** it targets any admin offerings endpoint (create/edit/delete/reorder), **then** it is rejected with `401 Unauthorized`.
|
||||
- **Given** an authenticated request from a `User`-role account (below `Administrator` in the hierarchy), **when** it targets any admin offerings endpoint, **then** it is rejected with `403 Forbidden`.
|
||||
- **Given** an authenticated request from an `Administrator`- or `Owner`-role account, **when** it targets any admin offerings endpoint, **then** it is permitted (subject to the other acceptance criteria above).
|
||||
|
||||
**Traceability**: FR-7; SECURITY-06, SECURITY-08
|
||||
|
||||
---
|
||||
|
||||
## Persona-to-Story Mapping
|
||||
|
||||
| Persona | Stories |
|
||||
|---|---|
|
||||
| Site Visitor | US-01, US-02, US-03 |
|
||||
| CMS Administrator | US-04, US-05, US-06, US-07, US-08, US-09, US-10, US-11, US-12 |
|
||||
|
||||
## INVEST Self-Check
|
||||
|
||||
| Story | Independent | Negotiable | Valuable | Estimable | Small | Testable |
|
||||
|---|---|---|---|---|---|---|
|
||||
| US-01..US-03 | ✅ each stands alone | ✅ display details open | ✅ core public value | ✅ | ✅ | ✅ Given/When/Then |
|
||||
| US-04..US-07 | ✅ each CRUD op independent | ✅ UI details open | ✅ core admin value | ✅ | ✅ | ✅ Given/When/Then |
|
||||
| US-08, US-09 | ✅ each interaction mode independent | ✅ exact UI open | ✅ usability | ✅ | ✅ | ✅ Given/When/Then |
|
||||
| US-10 | ✅ | ✅ | ✅ prevents a real content bug | ✅ | ✅ | ✅ Given/When/Then |
|
||||
| US-11, US-12 | ✅ | ✅ | ✅ security/data-quality value | ✅ | ✅ | ✅ Given/When/Then |
|
||||
|
||||
---
|
||||
|
||||
**2 personas, 12 user stories (3 Site Visitor, 9 CMS Administrator), all with Given/When/Then acceptance criteria and explicit traceability to functional requirements and/or Security Baseline rules.**
|
||||
Reference in New Issue
Block a user