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