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
2.8 KiB
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:
- Create/Update with featured exclusivity (US-10): when a request sets
Featured = true, the service first callsGetFeaturedAsync(); if a different offering currently holds it, that offering is un-featured (viaUpdateAsyncon 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. - Full reorder (US-08):
ReorderAsyncreceives the complete ordered list of IDs from the drag-and-drop UI and reassignsDisplayOrdersequentially (0, 1, 2, ...) to match. - Adjacent swap (US-09):
MoveUpAsync/MoveDownAsynclocate the neighboring offering byDisplayOrderand swap the twoDisplayOrdervalues. 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. - Soft delete (US-06/US-07, Q5 = B):
DeleteAsyncsetsIsDeleted = true/DeletedAt = nowrather 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. - Public vs. admin projections:
GetPublicOfferingsAsyncreturnsOfferingDto(the exact FR-6 contract shape:id,title,description,price,priceNote,features,ctaLabel,featured).GetAllForAdminAsyncreturnsOfferingAdminDto, which additionally exposesDisplayOrder(and, if useful in the admin UI,IsDeleted/DeletedAtare 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.