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
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:
+105
@@ -0,0 +1,105 @@
|
||||
# Business Rules — Unit: Offerings
|
||||
|
||||
## BR-OFF-01: Featured Exclusivity Rule (US-10)
|
||||
|
||||
At most one non-deleted `Offering` may have `Featured = true` at any time.
|
||||
|
||||
```mermaid
|
||||
%%{init: {'themeVariables': {'primaryTextColor':'#000000','textColor':'#000000','tertiaryTextColor':'#000000'}}}%%
|
||||
graph TD
|
||||
start{"Create or Update request<br/>has Featured = true?"}
|
||||
find_current{"A different Offering<br/>is currently Featured?"}
|
||||
unfeature["Set that Offering's<br/>Featured = false"]
|
||||
save["Save the request's Offering<br/>with Featured = true"]
|
||||
save_asis["Save the request's Offering<br/>as submitted (Featured value unchanged)"]
|
||||
|
||||
start -->|"No"| save_asis
|
||||
start -->|"Yes"| find_current
|
||||
find_current -->|"Yes"| unfeature --> save
|
||||
find_current -->|"No"| save
|
||||
|
||||
classDef decision fill:#fbd38d,stroke:#92400e,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef outcome fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
|
||||
class start,find_current decision;
|
||||
class unfeature,save,save_asis outcome;
|
||||
```
|
||||
|
||||
Text alternative: if a create/update request sets `Featured = true`, the service first checks for a different currently-featured offering and un-features it before saving; if the request does not set `Featured = true`, the offering is saved with whatever `Featured` value was submitted (allowing an admin to explicitly un-feature the current one, per US-10's third scenario).
|
||||
|
||||
**Applies identically regardless of UI entry point** (Functional Design Q5): whether the admin sets `Featured` via the create/edit form's toggle, or via the list-row quick-action, both call the same `IOfferingsService.CreateAsync`/`UpdateAsync` methods — this rule lives in the service layer, not in either UI path.
|
||||
|
||||
## BR-OFF-02: Soft Delete Never Blocked (US-06/US-07)
|
||||
|
||||
```mermaid
|
||||
%%{init: {'themeVariables': {'primaryTextColor':'#000000','textColor':'#000000','tertiaryTextColor':'#000000'}}}%%
|
||||
graph TD
|
||||
delete_req{"Delete request for Offering X"}
|
||||
is_last{"X is the last remaining<br/>non-deleted Offering?"}
|
||||
proceed["Set X.IsDeleted = true,<br/>X.DeletedAt = now"]
|
||||
|
||||
delete_req --> is_last
|
||||
is_last -->|"Yes"| proceed
|
||||
is_last -->|"No"| proceed
|
||||
|
||||
classDef decision fill:#fbd38d,stroke:#92400e,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef outcome fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
|
||||
class delete_req,is_last decision;
|
||||
class proceed outcome;
|
||||
```
|
||||
|
||||
Text alternative: deletion is always permitted regardless of how many offerings remain — the "last remaining offering" case is drawn explicitly to show it is not a special case that blocks the operation (D-5/Q4 in requirements.md).
|
||||
|
||||
**Admin UI adds a confirmation step** (Functional Design Q4) before the delete request is even sent — a UI/UX safeguard, not a backend rule; the backend itself does not require confirmation semantics.
|
||||
|
||||
## BR-OFF-03: Reorder Boundary Rules (US-08/US-09)
|
||||
|
||||
```mermaid
|
||||
%%{init: {'themeVariables': {'primaryTextColor':'#000000','textColor':'#000000','tertiaryTextColor':'#000000'}}}%%
|
||||
graph TD
|
||||
move_up{"MoveUp requested<br/>for Offering X"}
|
||||
is_first{"X has the lowest<br/>DisplayOrder (already first)?"}
|
||||
noop_up["No-op — X is already<br/>first, nothing to swap with"]
|
||||
swap_up["Swap DisplayOrder with the<br/>Offering immediately before X"]
|
||||
|
||||
move_up --> is_first
|
||||
is_first -->|"Yes"| noop_up
|
||||
is_first -->|"No"| swap_up
|
||||
|
||||
classDef decision fill:#fbd38d,stroke:#92400e,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef outcome fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
|
||||
class move_up,is_first decision;
|
||||
class noop_up,swap_up outcome;
|
||||
```
|
||||
|
||||
Text alternative: `MoveUp` on the first item in the list is a no-op (the frontend disables the button in this state per US-09's acceptance criteria); `MoveDown` on the last item is symmetric. `ReorderAsync` (drag-and-drop, US-08) receives the complete ordered list of IDs and reassigns `DisplayOrder` sequentially (0, 1, 2, ...) to match — it has no boundary case, since it always resequences the entire list at once.
|
||||
|
||||
## BR-OFF-04: Field Validation Rules (SECURITY-05, Functional Design Q3)
|
||||
|
||||
```mermaid
|
||||
%%{init: {'themeVariables': {'primaryTextColor':'#000000','textColor':'#000000','tertiaryTextColor':'#000000'}}}%%
|
||||
graph TD
|
||||
submit{"Create/Update request submitted"}
|
||||
check_required{"Title, Description, Price,<br/>PriceNote, CtaLabel all non-empty,<br/>and at least 1 Feature?"}
|
||||
check_lengths{"Title ≤100, Description ≤500,<br/>Price ≤50, PriceNote ≤100, CtaLabel ≤50,<br/>each Feature ≤200, Features count ≤10?"}
|
||||
reject["Reject: 400 with field-level<br/>validation errors (US-11)"]
|
||||
accept["Proceed to BR-OFF-01"]
|
||||
|
||||
submit --> check_required
|
||||
check_required -->|"No"| reject
|
||||
check_required -->|"Yes"| check_lengths
|
||||
check_lengths -->|"No"| reject
|
||||
check_lengths -->|"Yes"| accept
|
||||
|
||||
classDef decision fill:#fbd38d,stroke:#92400e,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef outcome fill:#f56565,stroke:#9b2c2c,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef success fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
|
||||
class submit,check_required,check_lengths decision;
|
||||
class reject outcome;
|
||||
class accept success;
|
||||
```
|
||||
|
||||
Text alternative: every create/update request is checked for required fields first, then for length/count bounds; either failure rejects the request with field-level errors (no partial save), matching US-11's acceptance criteria. Bounds are enforced identically on the backend (source of truth, SECURITY-05) and mirrored in the frontend form schema for immediate user feedback (Functional Design Q3 = A: Title 100, Description 500, Price 50, PriceNote 100, CtaLabel 50 characters; Features 1-10 items, each ≤200 characters).
|
||||
Reference in New Issue
Block a user