# 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).