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

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:
2026-08-02 01:28:39 +02:00
co-authored by Claude Sonnet 5
parent dcc82cdf62
commit fa389e42ee
51 changed files with 3119 additions and 127 deletions
@@ -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.
@@ -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.
@@ -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.
@@ -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.