Files
SluijsensandClaude Sonnet 5 fa389e42ee
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
Adds SlpModularCms.Api.SlpSoftware and extracts shared CmsHost composition
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
2026-08-02 01:28:39 +02:00

18 lines
2.8 KiB
Markdown

# 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.