Adds the Offerings module and retargets the CI/CD pipeline to Api.SlpSoftware
Continuous Integration / config (pull_request) Successful in 12s
Continuous Integration / changes (pull_request) Successful in 22s
Continuous Integration / backend-build (pull_request) Successful in 5m53s
Continuous Integration / vulnerability-scan (pull_request) Successful in 5m46s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m54s
Continuous Integration / backend-test (pull_request) Successful in 7m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m59s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 7m34s
Continuous Integration / deploy-test (pull_request) Skipped

Implements Unit 2 "Offerings" (backend module, admin CRUD UI with
drag-and-drop reordering, public GET /api/v1/offerings endpoint) and
executes the feature's D-15 CI/CD cutover, switching the deploy
pipeline's build/publish target from SlpModularCms.Api to
SlpModularCms.Api.SlpSoftware.

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 16:23:09 +02:00
co-authored by Claude Sonnet 5
parent b6e9c07c06
commit cfb06b28b6
79 changed files with 4069 additions and 94 deletions
@@ -0,0 +1,21 @@
# Logical Components — Unit: Offerings
## Component: `OfferingsService` Transactional Operations
**Type**: Application-service logic (existing component from Application Design, no new class) — three of its methods gain an explicit transaction boundary per NFR Design Pattern 1.
**Scope**: `CreateAsync`/`UpdateAsync` (when `Featured` transitions to `true`), `ReorderAsync`, `MoveUpAsync`, `MoveDownAsync`. Each wraps its read-modify-write sequence in a single EF Core transaction, committed on success and rolled back on any exception (letting the exception propagate to `GlobalExceptionHandler` unchanged — no new error-handling path).
## Component: `offerings-public` Rate-Limiting Policy
**Type**: Configuration + attribute, not a new class — an addition to the existing `AddCmsRateLimiting` registration (see `tech-stack-decisions.md`), applied via `[EnableRateLimiting("offerings-public")]` on `OfferingsController`'s public `GET` action only.
**Consumers**: `OfferingsController` (public GET action).
## Component: Structured Audit Logging in `OfferingsService`
**Type**: Logging calls within the existing service, not a new component — `ILogger<OfferingsService>.LogInformation` on create/update/delete, per NFR Design Pattern 3's field list.
## No Other New Logical Components
This unit introduces no new queues, caches, background jobs, or infrastructure components — the only additions are the transaction boundary around three existing service methods, one rate-limiting policy, and structured logging calls.
@@ -0,0 +1,27 @@
# NFR Design Patterns — Unit: Offerings
## Pattern 1: Transactional Multi-Row Operations (Data Integrity)
**Decision** (Q1 = A): every `OfferingsService` operation that touches more than one row in a single logical action runs inside one explicit DB transaction (`BeginTransactionAsync`/`CommitAsync`, rolled back on any exception):
- **Featured-exclusivity swap** (US-10): un-featuring the previously-featured offering and saving the newly-featured one.
- **Full reorder** (US-08): reassigning `DisplayOrder` across the entire list from the drag-and-drop UI.
- **Adjacent swap** (US-09): swapping `DisplayOrder` between two neighboring offerings.
**Rationale**: a crash or connection failure mid-operation must never leave the dataset in an inconsistent state (two offerings both un-featured, duplicate `DisplayOrder` values). This closes the open item `services.md` deferred to Functional Design but that was never actually decided there.
**Pattern**: standard EF Core `DbContext.Database.BeginTransactionAsync()` wrapping the read-modify-write sequence within each of the three `OfferingsService` methods (`CreateAsync`/`UpdateAsync` when `Featured` transitions to `true`, `ReorderAsync`, `MoveUpAsync`/`MoveDownAsync`). Single-row operations (plain create/update without a featured transition, soft-delete) do not need an explicit transaction — a single `SaveChangesAsync()` call is already atomic.
## Pattern 2: Rate Limiting Applied at the Action Level
**Decision**: `[EnableRateLimiting("offerings-public")]` is placed on the public `GET` action method only, following the exact precedent in `AuthController` (`[EnableRateLimiting("login")]`/`[EnableRateLimiting("refresh")]` on individual actions, not the whole controller). `OfferingsController`'s admin mutation actions carry no rate-limiting attribute — consistent with NFR-OFF-01 (Q1 = A) scoping the policy to the public endpoint only.
**No new pattern beyond this** — the `offerings-public` policy itself (config-driven `FixedWindowLimiter`) is already fully specified in `tech-stack-decisions.md`.
## Pattern 3: Audit Logging — Structured Fields
**Decision**: `OfferingsService` emits one `LogInformation` structured log entry per create/update/delete, with these fields: `OfferingId` (Guid), `Action` (`"Created"`/`"Updated"`/`"Deleted"`), `LastModifiedByUserId` (Guid, the same value persisted on the entity). No before/after value diffing — consistent with NFR-OFF-03's accepted scope (minimal audit trail, not a full audit-log table).
## No Other New Patterns
Scalability and Performance categories are N/A for this unit (see `offerings-nfr-design-plan.md`) — no new pattern required beyond what's already covered above and in `nfr-requirements.md`.