# 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
has Featured = true?"} find_current{"A different Offering
is currently Featured?"} unfeature["Set that Offering's
Featured = false"] save["Save the request's Offering
with Featured = true"] save_asis["Save the request's Offering
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
non-deleted Offering?"} proceed["Set X.IsDeleted = true,
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
for Offering X"} is_first{"X has the lowest
DisplayOrder (already first)?"} noop_up["No-op — X is already
first, nothing to swap with"] swap_up["Swap DisplayOrder with the
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,
PriceNote, CtaLabel all non-empty,
and at least 1 Feature?"} check_lengths{"Title ≤100, Description ≤500,
Price ≤50, PriceNote ≤100, CtaLabel ≤50,
each Feature ≤200, Features count ≤10?"} reject["Reject: 400 with field-level
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).