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.
|
||||
Reference in New Issue
Block a user