Files
slp-modular-cms/aidlc-docs/features/slpsoftware-api/construction/offerings/functional-design/business-rules.md
T
SluijsensandClaude Sonnet 5 cfb06b28b6
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
Adds the Offerings module and retargets the CI/CD pipeline to Api.SlpSoftware
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
2026-08-02 16:23:09 +02:00

5.8 KiB

Business Rules — Unit: Offerings

At most one non-deleted Offering may have Featured = true at any time.

%%{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)

%%{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)

%%{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)

%%{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).