# Business rules — snell-freestock-5.6 (Freestock Requisition)

Evidence date: 2026-10-02. Source: `main` at `812337f` (2025-06-13).

No business-rules document exists in the source repository. Rules below are taken from conditionals, SQL, and comments. Each rule is marked **Inferred** when the code behaves that way but no comment or document states it as policy. A comment that states the intent is marked **Documented in code**.

## Access

### BR-001 — Active domain login is required

**Inferred.** `LogginUser` loads a user only when `mstUser.strDomainLogin` equals the logon name and `mstUser.blnActive = 1`. The user must also have a matching `mstUserGroupFreeStock` row (`INNER JOIN`). No row means "Insufficient Access".

### BR-002 — Login identity can be replaced in code

**Implemented behaviour, not a business rule.** `LogginUser` assigns a fixed override username (`AzureAD\UrielBalela`) and uses it whenever that value is not null. The comment says `notforprod`. `system.inc.php` sets `LOGON_USER` to `silicon\pj` when the host is `sql-server`.

**Requires Confirmation** before anyone treats the Windows user as the person who will be signed in.

### BR-003 — Internet Explorer 6–11 may not log in

**Documented in code** (Yusiry, 2015-10-13). The login page clears the login action for those versions. The rule is a user-agent test on `login.php` only.

### BR-004 — Special users see user administration

**Inferred** from the menu. `blnSpecial = 1` adds the Users link. The comment in the requisition form calls that flag Dev or Admin. `user.php` does not repeat the check.

### BR-005 — Making a user inactive is the delete action

**Documented in code** by the button label "Make Inactive" and the SQL that sets `mstUser.blnActive = 0`. The old hard `DELETE` is commented out. An inactive user fails BR-001. Historical requisitions still join to `mstUser` for the requester name, so the row must remain.

## Requisition status

### BR-006 — Status values the form understands

**Inferred** from writes and from the permission switch. This application writes `Draft`, `Pending`, `Approved`, `Declined`, and `Cancelled`. The unsaved form displays `New` and does not insert that word. The permission switch also handles `Completed`, which this repository never writes.

| Status | How it is reached in this repo |
|---|---|
| New | Display default when `RequisitionID` is 0 or the row is missing. Not inserted |
| Draft | Insert in `Requisition::Save` |
| Pending | `Requisition::Submit` |
| Approved | `Requisition::Approve` |
| Declined | `Requisition::Decline` |
| Cancelled | `Requisition::Cancel` (button hidden) |
| Completed | Read for permissions only. Writer **Unknown** |

The status filter dropdown is the distinct `strStatus` values already stored on `fsrRequisition`, so any value written outside this repo also appears.

### BR-007 — Who may edit, submit, and approve

**Documented in code** for the ceiling bugs (pj 2016-05-05, maanie 2020-10-29) and for "no edits after approve" (2023-11-02). The matrix is **Inferred** from the permission flags.

Identity on the open requisition, first match wins:

1. Session user is `refExecutiveManagerID`: treated as exec, manager, and requester.
2. Session user is `refManagerID` or `refAlternateManagerID`: treated as manager and requester.
3. Session user is `refUserID`: treated as requester.
4. Otherwise the user is not a party to the requisition.

Non-special users (`blnSpecial` is not 1):

| Status | Edit, Save, and Decline | Submit | Approve |
|---|---|---|---|
| Completed, Approved, Declined, Cancelled | Disabled | Disabled with the edit flag | Disabled with the edit flag |
| Pending, total under 10000, and the user is a manager (including exec, because step 1 sets the manager flag) | Enabled | Enabled. Submit is not separately disabled on Pending | Enabled |
| Pending, total 10000 or more, and the user is the executive on the requisition | Enabled | Enabled | Enabled |
| Pending, and neither of the two rows above | Disabled | Disabled | Disabled |
| Draft, and the user is a party | Enabled | Enabled | Disabled |
| Draft, and the user is not a party | Disabled | Disabled | Disabled |
| Any other status, including the unsaved label New | Approve disabled. Submit disabled because status is not Draft. Edit enabled only when the user is a party | | |

`$SALESMANAGERMAXVALUE` is `10000` in `include/system.inc.php`. The total compared is the integer cast of `SUM(fsrItem.dblTotal)`, so a fractional rand is truncated before the test.

Special users (`blnSpecial = 1`):

- The status switch above is skipped.
- If status is Approved, edit controls are disabled for every special user (comment 2023-11-02).
- If the special user's `refUserGroupFreeStockID` is 6, edit controls are also disabled for Completed, Declined, and Cancelled.
- A special user who is not in group 6 is not disabled for Completed, Declined, or Cancelled by this branch.
- The ceiling is not applied in the special-user branch. **Inferred.** **Requires Confirmation** that special users are allowed to approve any amount.

Decline uses the same edit flag as Save. It does not check the ceiling on its own.

### BR-008 — Pink-slip number

**Inferred.** On insert, `strPinkSlipNumber` is `ES` plus the requisition id padded to 7 digits. Later saves do not change it.

### BR-009 — Manager chain is copied from the requester

**Inferred.** `UpdateManagers` sets `refManagerID`, `refAlternateManagerID`, and `refExecutiveManagerID` on the requisition from `mstUser` for `fsrRequisition.refUserID`. The executive id stored is the manager's `refManagerExecutiveID`, not a separate field on the requester. If `refManagerID` is 0 the save message warns that no manager is set.

The copy runs on every save, including Approve, because Approve calls `Save` first.

### BR-010 — Line price and total

**Inferred.**

- Server total: cases times case price, plus bottles times bottle price, rounded to 2 decimals, using prices from `dwdProduct` at save time.
- Browser total: the same product rounded to a whole number and shown without cents.
- List total: the summed value rounded to a whole rand. Caption: "*Rounded Values".

A line with an empty product code is not written.

### BR-011 — Bill-to accounts are promotional

**Inferred.** Bill-to dropdown and `ajaxLoadRegionBillToAccountsXML` require `dwdCustomer.blnActive = 1`, `strRegionDesc2` equal to the selected region, and `blnPromotionalAccounts = 1`. The customer already stored on the requisition is included even when those flags fail.

Ship-to requires active and the same region only. Promotional is not required.

### BR-012 — Request type and brand lists hide inactive rows

**Inferred.** `fsrRequestType` and `mstBrand` options are `blnActive = 1`, plus the id or brand already stored so an old requisition still displays.

Expense is not a table. The only options are Marketing and Sales.

### BR-013 — Required fields

**Documented in code** for the two exceptions. `validateForm` requires request type, expense, region, bill-to, ship-to, and detail. Contact name and contact number are not required (comment: not required, pj / Snell demo 2015-11-23). There is no matching server-side check. Save will persist whatever was posted.

User form, browser only: user group, user name, domain login, email, region. RepID is labelled optional. Disabled manager dropdowns are skipped.

### BR-014 — New user domain login must be unique

**Inferred.** On insert, if `strDomainLogin` already exists, save returns "Domain login already exists" and does not insert. Update does not perform this check.

### BR-015 — Manager flags rewrite the reporting links

**Inferred** from `User::Save` after the main write.

- Exec manager: this user is their own manager and their own executive, `blnManager` is forced to 1, alt manager is 0.
- Manager but not exec: this user is their own manager, alt manager is 0, executive id is the posted value.
- Neither: posted manager and alt manager are stored, executive id is 0.

The browser disables the manager and alt-manager dropdowns when "Is Manager" is checked, and disables manager, alt, exec, and "Is Manager" when "Is Exec Manager" is checked.

### BR-016 — Default list window

**Inferred.** Until the user filters, the requisition list is status Pending, any manager, from the first day of the month through today. Ignore Dates removes the date predicate. Clear removes the session filters; the next construction sets the defaults again.

### BR-017 — Notification jobs

**Inferred.**

| Event | `sysCronJob.Type` | What is written |
|---|---|---|
| Submit | `FSR_NewRequisition` | Existing row with the same type and param is deleted, then a row with status `New` is inserted. Requisition becomes Pending |
| Approve | `FSR_RequisitionApproved` | Same delete-and-insert. Requisition becomes Approved. `strAuthorizedBy` is the session display name |
| Decline or Cancel | any type whose param is `RequisitionID={id}` | Those rows become status `Removed` with a Result string. No new job is inserted |

The meaning of the job types to the downstream system is **Unknown / Requires Confirmation**.

### BR-018 — Authorised-by text

**Documented in code** (2021-05-03, escape single quotes). Approve stores the user name with quotes doubled. Cancel stores `Cancelled [name]` with the name escaped. Decline stores `Declined [name]` on the requisition without escaping, and escapes only the cron Result value.

### BR-019 — One usage row per successful login

**Inferred.** Insert into `mstUsageLog` (`mstUserID`, `dtStamp`, `strSystem`) with `strSystem = 'FreeStock'` and `dtStamp` formatted `YmdHis`. `sleep(1)` runs before the insert. No other page writes this table.

## Rules that look like policy but are not enforced

- The login page says Active Directory controls access. The PHP rule is BR-001 against `mstUser`.
- User-admin screens are implied to be limited to special users. The page does not enforce that.
- AJAX lookups are part of the signed-in form, but `ajax.functions.php` does not check the session.
- Contact details are on the form and in the search filter, and they are not required.
