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,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.**