---
title: SAIF.Business.Menu business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-08-17
library: SAIF.Business.Menu
reference_type: package
---

# SAIF.Business.Menu business rules

This page captures the 27 business rules enforced by `SAIF.Business.Menu` — the menu-authorization resolution model (grants, unsecured items, favorites), the CRUD lifecycle common to every maintenance object, and the in-memory lookup/filter predicates used for navigation.

## BR-MENU-001: Scoped vs. unfiltered retrieval

| Field | Value |
| --- | --- |
| **Statement** | A collection retrieval scoped by a key (user, item) uses the filtered statement only when the scoping value is supplied; otherwise the unfiltered "retrieve all" statement runs. |
| **Trigger** | Collection `BeforeRetrieve` |
| **Enforcement** | `clsMenuAuthorizedItemCollection`, `clsMenuFavoriteCollection`, `clsMenuSecurityCollection`, `clsMenuSecurityUserCollection` choose between a filtered and unfiltered `RetrieveSQL` variant based on whether the scoping value is populated |
| **Outcome** | Scoped or full result set is returned depending on caller input |
| **SQL** | [SQL-015](sql/SQL-015-retrieve-authorized-menu-items-for-user.md), [SQL-016](sql/SQL-016-retrieve-all-authorized-menu-items.md), [SQL-020](sql/SQL-020-retrieve-all-menu-favorites.md), [SQL-021](sql/SQL-021-retrieve-menu-favorites-for-user.md), [SQL-027](sql/SQL-027-retrieve-all-menu-security.md), [SQL-028](sql/SQL-028-retrieve-menu-security-for-item.md), [SQL-029](sql/SQL-029-retrieve-all-menu-security-users.md), [SQL-030](sql/SQL-030-retrieve-menu-security-users-for-user.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-002: Group-granted authorization

| Field | Value |
| --- | --- |
| **Statement** | A user is authorized for a menu item when they belong to a security group granted access to that item. |
| **Trigger** | Authorized-item retrieval |
| **Enforcement** | `clsMenuAuthorizedItemCollection.Criteria.RetrieveSQL` branch 1 (`MENU_SECURITY` joined to `MENU_SECURITY_USER`) |
| **Outcome** | Item appears in the user's authorized set with `UnsecuredFlag = 'N'` |
| **Tables** | [MENU_SECURITY](database-tables.md#menu_security), [MENU_SECURITY_USER](database-tables.md#menu_security_user) |
| **SQL** | [SQL-015](sql/SQL-015-retrieve-authorized-menu-items-for-user.md), [SQL-016](sql/SQL-016-retrieve-all-authorized-menu-items.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-003: Case-insensitive group matching

| Field | Value |
| --- | --- |
| **Statement** | Security group names are matched case-insensitively between the grant and the membership. |
| **Trigger** | Authorized-item retrieval |
| **Enforcement** | `RetrieveSQL` (`upper(USR.SECURITY_GROUP_TXT) = upper(SEC.SECURITY_GROUP_TXT)`) |
| **Outcome** | Case differences in group text do not deny access |
| **Tables** | [MENU_SECURITY](database-tables.md#menu_security), [MENU_SECURITY_USER](database-tables.md#menu_security_user) |
| **SQL** | [SQL-015](sql/SQL-015-retrieve-authorized-menu-items-for-user.md), [SQL-016](sql/SQL-016-retrieve-all-authorized-menu-items.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-004: Unsecured items open to all

| Field | Value |
| --- | --- |
| **Statement** | A menu item with no security grants at all is available to every user. |
| **Trigger** | Authorized-item retrieval |
| **Enforcement** | `RetrieveSQL` branch 2 (`NOT EXISTS … menu_security`) |
| **Outcome** | Item appears for all users flagged `UnsecuredFlag = 'Y'` |
| **Tables** | [MENU_ITEM](database-tables.md#menu_item), [MENU_SECURITY](database-tables.md#menu_security) |
| **SQL** | [SQL-015](sql/SQL-015-retrieve-authorized-menu-items-for-user.md), [SQL-016](sql/SQL-016-retrieve-all-authorized-menu-items.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-005: Favorites appear in the authorized set

| Field | Value |
| --- | --- |
| **Statement** | A user's favorites appear in the authorized set as favorite rows carrying the favorite's own display order. |
| **Trigger** | Authorized-item retrieval |
| **Enforcement** | `RetrieveSQL` branch 3 (`MENU_FAVORITE`) |
| **Outcome** | Favorite rows flagged `FavoriteFlag = 'Y'` ordered by `MENU_ORDER_NUM` |
| **Tables** | [MENU_FAVORITE](database-tables.md#menu_favorite) |
| **SQL** | [SQL-015](sql/SQL-015-retrieve-authorized-menu-items-for-user.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-006: Inactive favorites excluded

| Field | Value |
| --- | --- |
| **Statement** | A favorite with an item-inactive date is excluded from the user's authorized/favorite set. |
| **Trigger** | Authorized-item retrieval |
| **Enforcement** | `RetrieveSQL` branch 3 (`FAV.ITEM_INACTIVE_DATE IS NULL`) |
| **Outcome** | Retired favorites are not shown |
| **Tables** | [MENU_FAVORITE](database-tables.md#menu_favorite) |
| **SQL** | [SQL-015](sql/SQL-015-retrieve-authorized-menu-items-for-user.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-007: Authorized items are read-only

| Field | Value |
| --- | --- |
| **Statement** | Authorized items are read-only — the projection supports no create, retrieve-by-key, update, or delete. |
| **Trigger** | Any attempt to persist a `clsMenuAuthorizedItem` |
| **Enforcement** | `clsMenuAuthorizedItem.Criteria` (all five SQL properties throw `ApplicationException(BOC.Messages.InvalidSQLProperty)`) |
| **Outcome** | Persistence attempts fail fast |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItem.vb |

---

## BR-MENU-008: Page reachability requires an authorized entry

| Field | Value |
| --- | --- |
| **Statement** | A page is reachable by a user only if it appears in that user's authorized items for the requesting application. |
| **Trigger** | Page lookup by the presentation layer |
| **Enforcement** | `clsMenuAuthorizedItemCollection.FindPage` |
| **Outcome** | `Nothing` returned for unauthorized pages; caller denies access |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-009: Page link normalization

| Field | Value |
| --- | --- |
| **Statement** | Page links are compared with surrounding whitespace removed, case-insensitively, and (for the non-exact lookups) ignoring any query string. |
| **Trigger** | Any page/item lookup |
| **Enforcement** | `FindPage`, `Item`, `ItemByCategory`, `LinkOnly` |
| **Outcome** | `Page.aspx?id=1` matches the grant for `Page.aspx` |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-010: Visible-only lookups

| Field | Value |
| --- | --- |
| **Statement** | Lookups may be restricted to visible items only. |
| **Trigger** | `Item` / `ItemExact` / `ItemByCategory` with `visibleOnly = True` |
| **Enforcement** | `clsMenuAuthorizedItemCollection` filter predicates (`AuthItem.IsVisible`) |
| **Outcome** | Hidden items are excluded from navigation resolution |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-011: Category lookup fallback

| Field | Value |
| --- | --- |
| **Statement** | Category-scoped lookup first tries an exact link match, then falls back to a query-string-insensitive match. |
| **Trigger** | `ItemByCategory` |
| **Enforcement** | `clsMenuAuthorizedItemCollection.ItemByCategory` |
| **Outcome** | Parameterized links still resolve to their menu entry |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-012: Application resolution by virtual path

| Field | Value |
| --- | --- |
| **Statement** | An application is identified from the request's virtual path, compared trimmed and case-insensitively against the registered application path. |
| **Trigger** | `NavigationContext` resolving the current application |
| **Enforcement** | `clsApplicationCodeCollection.ItemByAppPath` |
| **Outcome** | The application code used for all authorization lookups |
| **Tables** | [BUSONLINE_APPS_CODE](database-tables.md#busonline_apps_code) |
| **Source** | SAIF.Business.Menu/clsApplicationCodeCollection.vb |

---

## BR-MENU-013: Active-only application filtering

| Field | Value |
| --- | --- |
| **Statement** | Only applications whose active code is `Y` (or null) are offered when active-only codes are requested. |
| **Trigger** | `GetActiveApplicationCodes` |
| **Enforcement** | `clsApplicationCodeCollection.Criteria.RetrieveActiveSQL` (`NVL(ACTIVE_CODE,'Y') = 'Y'`) |
| **Outcome** | Retired applications are excluded; a missing flag defaults to active |
| **Tables** | [BUSONLINE_APPS_CODE](database-tables.md#busonline_apps_code) |
| **SQL** | [SQL-014](sql/SQL-014-retrieve-active-application-codes.md) |
| **Source** | SAIF.Business.Menu/clsApplicationCodeCollection.vb |

---

## BR-MENU-014: Sequence-assigned identity

| Field | Value |
| --- | --- |
| **Statement** | New menu items, pages, security grants, and favorites take their identity from a dedicated Oracle sequence. |
| **Trigger** | Object creation |
| **Enforcement** | `CreateSQL` on `clsMenuItem` (`MENU_ITEM_SEQ`), `clsMenuPage` (`MENU_PAGE_SEQ`), `clsMenuSecurity` (`MENU_SECURITY_SEQ`), `clsMenuFavorite` (`MENU_FAVORITE_SEQ`); `clsMenuSecurityUser` takes `MENU_SECURITY_USER_SEQ.NEXTVAL` inline in its `INSERT` |
| **Outcome** | Server-assigned surrogate keys |
| **SQL** | [SQL-001](sql/SQL-001-menu-favorite-next-seq.md), [SQL-002](sql/SQL-002-menu-item-next-seq.md), [SQL-003](sql/SQL-003-menu-page-next-seq.md), [SQL-004](sql/SQL-004-menu-security-next-seq.md), [SQL-011](sql/SQL-011-insert-menu-security-user.md) |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-015: Natural-key creation for codes

| Field | Value |
| --- | --- |
| **Statement** | Application codes and menu category codes are created with a caller-supplied natural key rather than a sequence. |
| **Trigger** | `NewApplicationCode`, `NewMenuCategoryCode` |
| **Enforcement** | `CreateSQL` throws `InvalidSQLProperty` on both |
| **Outcome** | No sequence round trip; key must be supplied |
| **SQL** | [SQL-005](sql/SQL-005-insert-application-code.md), [SQL-006](sql/SQL-006-insert-menu-category-code.md) |
| **Source** | SAIF.Business.Menu/clsApplicationCode.vb |

---

## BR-MENU-016: Audit stamp on insert and update

| Field | Value |
| --- | --- |
| **Statement** | Every audited row records the acting user (upper-cased, trimmed) and a database-side `SYSDATE` timestamp on insert and update. |
| **Trigger** | Insert/update of `MENU_ITEM`, `MENU_PAGE`, `MENU_SECURITY` |
| **Enforcement** | `Before*` methods (`AddParm("UserId", DataPortalInfo.UserID.Trim.ToUpper, 30)`) and the SQL literals `UPDATE_DATE = SYSDATE` |
| **Outcome** | Consistent audit stamp from the database clock |
| **Tables** | [MENU_ITEM](database-tables.md#menu_item), [MENU_PAGE](database-tables.md#menu_page), [MENU_SECURITY](database-tables.md#menu_security) |
| **SQL** | [SQL-008](sql/SQL-008-insert-menu-item.md), [SQL-009](sql/SQL-009-insert-menu-page.md), [SQL-010](sql/SQL-010-insert-menu-security.md), [SQL-034](sql/SQL-034-update-menu-item.md), [SQL-035](sql/SQL-035-update-menu-page.md), [SQL-036](sql/SQL-036-update-menu-security.md) |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-017: Dirty tracking on change

| Field | Value |
| --- | --- |
| **Statement** | A property assignment marks the object dirty only when the value actually changes. |
| **Trigger** | Any property setter |
| **Enforcement** | All business-object setters (`If m_X <> Value Then … MarkDirty()`) |
| **Outcome** | No-op assignments do not trigger a save |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-018: Save only when dirty

| Field | Value |
| --- | --- |
| **Statement** | An object is written only when it is dirty, and a child object is saved through the child save path rather than as a root. |
| **Trigger** | `Save` / `SaveChild` |
| **Enforcement** | `SaveMe` in each business object (`If MyBase.IsDirty`, `If IsChild`) |
| **Outcome** | Clean objects skip database work; children participate in the parent's transaction |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-019: Y/N boolean flag storage

| Field | Value |
| --- | --- |
| **Statement** | Boolean menu attributes are stored as single-character `Y`/`N` flags. |
| **Trigger** | Insert/update of items and pages |
| **Enforcement** | `IsVisible.ToY_N_String` and the `ToBoolean` read path |
| **Outcome** | Database holds `Y`/`N`; the object model exposes `Boolean` |
| **Tables** | [MENU_ITEM](database-tables.md#menu_item), [MENU_PAGE](database-tables.md#menu_page) |
| **SQL** | [SQL-008](sql/SQL-008-insert-menu-item.md), [SQL-009](sql/SQL-009-insert-menu-page.md), [SQL-034](sql/SQL-034-update-menu-item.md), [SQL-035](sql/SQL-035-update-menu-page.md) |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-020: Single-row write verification

| Field | Value |
| --- | --- |
| **Statement** | A create, update, or delete must affect exactly one row; anything else is reported as a failure on the object. |
| **Trigger** | `AfterInsert` / `AfterUpdate` / `AfterDelete` |
| **Enforcement** | All business objects (`If RowsAffected <> 1 Then AddErrorMessage(...)`) |
| **Outcome** | `InsertError` = "Insert Failed"; `UpdateError` / `DeleteError` = `BOC.Messages.DBContentionError` |
| **SQL** | [SQL-005](sql/SQL-005-insert-application-code.md), [SQL-006](sql/SQL-006-insert-menu-category-code.md), [SQL-007](sql/SQL-007-insert-menu-favorite.md), [SQL-008](sql/SQL-008-insert-menu-item.md), [SQL-009](sql/SQL-009-insert-menu-page.md), [SQL-010](sql/SQL-010-insert-menu-security.md), [SQL-011](sql/SQL-011-insert-menu-security-user.md), [SQL-031](sql/SQL-031-update-application-code.md), [SQL-032](sql/SQL-032-update-menu-category-code.md), [SQL-033](sql/SQL-033-update-menu-favorite.md), [SQL-034](sql/SQL-034-update-menu-item.md), [SQL-035](sql/SQL-035-update-menu-page.md), [SQL-036](sql/SQL-036-update-menu-security.md), [SQL-037](sql/SQL-037-update-menu-security-user.md), [SQL-038](sql/SQL-038-delete-application-code.md), [SQL-039](sql/SQL-039-delete-menu-category-code.md), [SQL-040](sql/SQL-040-delete-menu-favorite.md), [SQL-041](sql/SQL-041-delete-menu-item.md), [SQL-042](sql/SQL-042-delete-menu-page.md), [SQL-043](sql/SQL-043-delete-menu-security.md), [SQL-044](sql/SQL-044-delete-menu-security-user.md) |
| **Source** | SAIF.Business.Menu/clsMenuPage.vb |

---

## BR-MENU-021: Cascading child security-group save

| Field | Value |
| --- | --- |
| **Statement** | A menu item's security groups are saved with it, and the item is invalid or dirty whenever its groups are. |
| **Trigger** | `clsMenuItem.Save`, `IsValid`, `IsDirty` |
| **Enforcement** | `clsMenuItem.SaveMe` (`SaveChildObject(SecurityGroups)`), `IsValid`/`IsDirty` overrides |
| **Outcome** | Item and its grants stay consistent |
| **Tables** | [MENU_SECURITY](database-tables.md#menu_security) |
| **SQL** | [SQL-028](sql/SQL-028-retrieve-menu-security-for-item.md), [SQL-036](sql/SQL-036-update-menu-security.md), [SQL-043](sql/SQL-043-delete-menu-security.md) |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-022: Delete returns object to new state

| Field | Value |
| --- | --- |
| **Statement** | A successful delete returns the object to a "new" state rather than disposing it. |
| **Trigger** | `AfterDelete` |
| **Enforcement** | All business objects (`MarkNew()`) |
| **Outcome** | The in-memory object can be re-inserted |
| **SQL** | [SQL-038](sql/SQL-038-delete-application-code.md), [SQL-039](sql/SQL-039-delete-menu-category-code.md), [SQL-040](sql/SQL-040-delete-menu-favorite.md), [SQL-041](sql/SQL-041-delete-menu-item.md), [SQL-042](sql/SQL-042-delete-menu-page.md), [SQL-043](sql/SQL-043-delete-menu-security.md), [SQL-044](sql/SQL-044-delete-menu-security-user.md) |
| **Source** | SAIF.Business.Menu/clsMenuPage.vb |

---

## BR-MENU-023: Favorite name fallback

| Field | Value |
| --- | --- |
| **Statement** | A favorite falls back to the page's own name when no favorite-specific name is stored. |
| **Trigger** | Reading `FavoritePageName` |
| **Enforcement** | `clsMenuAuthorizedItem.FavoritePageName`, `clsMenuPage.FavoritePageName` |
| **Outcome** | Favorites always render with a label |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItem.vb |

---

## BR-MENU-024: Lazy-loaded navigation properties

| Field | Value |
| --- | --- |
| **Statement** | A menu item's page, category, security groups, application, and a favorite's item are fetched on first access, not with the parent. |
| **Trigger** | First read of the navigation property |
| **Enforcement** | `clsMenuItem.MenuPage` / `MenuCategory` / `SecurityGroups`, `clsMenuPage.Application`, `clsMenuFavorite.MenuItem` |
| **Outcome** | Related objects load on demand (one query per access) |
| **SQL** | [SQL-022](sql/SQL-022-retrieve-menu-item-by-seq.md), [SQL-024](sql/SQL-024-retrieve-menu-page-by-seq.md), [SQL-027](sql/SQL-027-retrieve-all-menu-security.md), [SQL-028](sql/SQL-028-retrieve-menu-security-for-item.md) |
| **Source** | SAIF.Business.Menu/clsMenuItem.vb |

---

## BR-MENU-025: UNSECURED pseudo-user membership

| Field | Value |
| --- | --- |
| **Statement** | The membership test for a user treats the pseudo-user `UNSECURED` as matching any user. |
| **Trigger** | `clsMenuAuthorizedItemCollection.contains(userID, menuItem)` |
| **Enforcement** | `clsMenuAuthorizedItemCollection` |
| **Outcome** | Unsecured items count as contained for every user |
| **SQL** | [SQL-016](sql/SQL-016-retrieve-all-authorized-menu-items.md) |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-026: Secured application items exclusion

| Field | Value |
| --- | --- |
| **Statement** | "Secured application items" excludes both favorites and items that were granted only because they are unsecured. |
| **Trigger** | Reading `SecuredAppItems` |
| **Enforcement** | `clsMenuAuthorizedItemCollection.SecuredAppItems` |
| **Outcome** | Callers can distinguish genuinely granted items |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## BR-MENU-027: Category filtering safety

| Field | Value |
| --- | --- |
| **Statement** | Category filtering is case-insensitive, and a null category yields an empty result rather than an error. |
| **Trigger** | `AppItemsByCategory` |
| **Enforcement** | `clsMenuAuthorizedItemCollection.AppItemsByCategory` |
| **Outcome** | Safe category filtering for menu rendering |
| **Source** | SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb |

---

## Notes
- Mark inferred statements with `*` and unknown statements with `**`. Do not use bold inline confidence labels.
- No stored procedures are referenced by this library; all persistence is inline or `StringBuilder`-assembled SQL through the SAIF BOF data portal.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Database tables](database-tables.md)
- [SQL catalog](sql/index.md)
- [Security and authorization](security-authorization.md)
- [API reference](api/index.md)

## Sources
- SAIF.Business.Menu/clsApplicationCode.vb
- SAIF.Business.Menu/clsApplicationCodeCollection.vb
- SAIF.Business.Menu/clsMenuAuthorizedItem.vb
- SAIF.Business.Menu/clsMenuAuthorizedItemCollection.vb
- SAIF.Business.Menu/clsMenuItem.vb
- SAIF.Business.Menu/clsMenuPage.vb
