# PRD — snell-freestock-5.6 (Freestock Requisition)

Evidence date: 2026-10-02. Source: `git@bitbucket.org:overdrive/snell-freestock-5.6.git`, `main` at `812337f` (2025-06-13).

There is no prior PRD in the repository or in Silicon Ark. The README is the empty Bitbucket template. Every requirement below was recovered from PHP, SQL strings, and `web.config`. Provenance is marked on each item.

## Product purpose

Snell staff raise and authorise freestock requisitions from a browser. A requisition names a region, a bill-to account, a ship-to account, a request type, whether the cost is Marketing or Sales, and one or more products with case and bottle quantities. A manager or executive manager approves or declines it. The screen title is "Freestock Requisition".

Provenance: **Implemented**. A written product statement was not found.

## Users and roles

Roles are rows, not code enums. Group names live in `mstUserGroupFreeStock.strUserGroup` and were not in the repository. Only group id **6** is named in a comment, as the Executive security group.

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Windows-authenticated staff user | IIS supplies `LOGON_USER`. The app loads `mstUser` where `strDomainLogin` matches and `blnActive = 1` | `LogginUser` | Implemented |
| Requester | `fsrRequisition.refUserID` equals the session user. Can edit a Draft | `requisitionForm.php` permission switch | Implemented |
| Manager or alternate manager | Session user id equals `refManagerID` or `refAlternateManagerID`. Can edit a Pending requisition when the item total is under R10 000 | Same switch; `$SALESMANAGERMAXVALUE = 10000` | Implemented |
| Executive manager on the requisition | Session user id equals `refExecutiveManagerID`. Can edit a Pending requisition at or above R10 000 | Same switch | Implemented |
| Special user (`blnSpecial = 1`) | Menu shows Users. Comment says "Dev\|\|Admin". Bypasses the status lock except when status is Approved (and, for group 6, also Completed, Declined, Cancelled) | `layout.menu.inc.php`, `requisitionForm.php` | Implemented. Which table owns `blnSpecial` is **Unknown** because the login query is `SELECT *` |
| User group 6 | Comment: Executive security group, distinct from `blnManagerExecutive`. Inside the special-user branch, group 6 cannot edit Completed, Approved, Declined, or Cancelled | `requisitionForm.php` | Implemented. The group name in the database was not verified |
| Anonymous caller of `ajax.functions.php` | The XML endpoints do not check `$_SESSION[USER]` | `ajax.functions.php` vs `layout.full.inc.php` | Implemented |

Login copy says "Login is controlled through the Active Directory." The code does not call adLDAP. It matches `LOGON_USER` to `mstUser.strDomainLogin`. Provenance of the AD sentence: **Documented in the login page**. Provenance of the lookup: **Implemented**. Whether IIS authentication is the whole AD integration is **Requires Confirmation**.

## Main workflows

### W1 — Sign in

Trigger: `index.php` redirects to `login.php`. `login.php` destroys any existing session, then includes the system bootstrap.

Inputs: `$_SERVER["LOGON_USER"]`. There is no username or password field. The Login button posts `Action=Login`, but `Action` is also set to `Login` whenever `LOGON_USER` is not null, before the button is used.

Behaviour:

1. If the user-agent matches Internet Explorer 6, 7, 8, 9, 10, or 11 (MSIE or Trident), `Action` is cleared and a red message tells the user to install Firefox or Chrome.
2. Otherwise `LogginUser` runs.
3. `LogginUser` currently replaces the supplied name with the constant `AzureAD\UrielBalela` whenever that constant is not null. The line is commented `////notforprod`. Provenance: **Implemented**. Whether this override is intentional on the deployed site is **Requires Confirmation**.
4. When the host name is `sql-server`, `system.inc.php` sets `LOGON_USER` to `silicon\pj` before login. Provenance: **Implemented** dev override.
5. Lookup: active user joined to `mstUserGroupFreeStock` on `strDomainLogin`.
6. On success, `$_SESSION["USER"]` is that row, a row is inserted into `mstUsageLog` (`strSystem = 'FreeStock'`), `sleep(1)` runs, and the browser goes to `requisition.php`.
7. On failure the message is "Insufficient Access for {LOGON_USER}" plus the Active Directory sentence.

Output: session and the authorisation list, or the login message.

`checkLogin()` compares `$_SESSION['IP']` to `REMOTE_ADDR` and redirects to login. Nothing in the tree calls it. Provenance: **Dormant**.

A request to `requisitionForm.php` with any `Link` query parameter also calls `LogginUser` before showing the form. The parameter value is not read. Comment: "logs in via email". Provenance: **Implemented**. The email template that would add `Link` is not in this repository.

### W2 — Authorisation list

Trigger: `requisition.php` after a session exists. Default action builds `Requisition::getFilters()` and `getList()`.

First visit in the session, when filters are unset: status **Pending**, manager **-1** (All), start date the first of the current month, end date today.

Filters (posted as `Action=Filter`, stored on `$_SESSION[PAGES]->Page[script]->Filters` for keys starting `fr`):

- Requested by, region, manager (matches manager, alternate, or executive id), status, bill-to, ship-to, free-text search, start date, end date, ignore-dates checkbox.
- Search matches requisition id, request type, contact number, detail, bill-to, ship-to, or contact person, using `LIKE`.
- `Action=Clear` drops that page's filters. The Clear button also resets the date inputs in the browser to month-start and today.

The grid shows pink-slip number (link to the form), requester, request type, date, region, bill-to, ship-to, status, authorised by, rounded total, and last edit. A footer sums the rounded totals. Sort links toggle ASC/DESC per column.

Missing customer names display as `*MISSING*`.

Provenance: **Implemented**.

### W3 — Create and save a requisition

Trigger: menu "New Requisition" (`requisitionForm.php?RequisitionID=0`) or Save on the form.

Inputs required by `validateForm()` before post: request type, M or S expense, region, bill-to, ship-to, detail of request. Contact name and contact number are collected but the checks are commented out (2015-11-23).

New-form defaults before the first save: requester is the session user, status label **New**, date today, region from the user, delivery selected, product table hidden (`display:none`).

Save of id 0 (`Requisition::Save`):

1. Inserts `fsrRequisition` with status **Draft**. Date literal is rewritten to `CONVERT(datetime, 'Ymd', 120)`.
2. Sets pink-slip number to `ES` plus the new id, zero-padded to 7 digits (`ES0000123` for id 123).
3. Copies manager, alternate manager, and executive manager ids from the requester's `mstUser` row via `UpdateManagers`.
4. If the manager id is 0, the message includes "WARNING! No Manager set for this User!".

Save of an existing id updates the requisition (date and pink-slip number are not overwritten) and each posted product line. A line whose product code is empty is skipped. New lines use a negative item id from the browser (`uniqueID` starts at -1 and decreases). Price is re-read from `dwdProduct` (`dblPriceCase`, `dblPriceBottle`). Line total is `round((cases * case price) + (bottles * bottle price), 2)`.

`strInvoiceNumber` and `dtEmailSentToAltManager` are removed from the field list before save, so the form cannot write them.

Provenance: **Implemented**.

### W4 — Add products

Trigger: Add Item on a saved requisition (the product block is hidden when `RequisitionID` was not found).

Behaviour: clone a hidden template row, then `ajaxReloadProductsXML` loads active products for the brand from `dwdProduct`. Changing the product copies `dblPriceCase` and `dblPriceBottle` onto the row. The browser total uses `Math.round` (no cents). The list and the server save use the rules in W3 and in business rule BR-010.

Brand and product lists keep an inactive row when it is already selected (`blnActive = 1 OR` current value).

Bill-to options: `dwdCustomer` where active, same `strRegionDesc2` as the requisition region, and `blnPromotionalAccounts = 1`, plus the currently selected account even if it would otherwise be excluded.

Ship-to options: active customers in the region, plus the current account. The Search box calls `ajaxSearchShipToAccountXML` and also matches customer number or name.

Provenance: **Implemented**.

### W5 — Submit

Trigger: Submit on the form. The handler saves, then `Requisition::Submit`.

Behaviour:

1. Deletes existing `sysCronJob` rows with `Type = 'FSR_NewRequisition'` and `param = 'RequisitionID={id}'`.
2. Inserts a new job with that type, status **New**, and `dtCronJob` from `GETDATE()` as `YYYYMMDD` plus `HHMM`.
3. Sets `fsrRequisition.strStatus` to **Pending**.

Who can press Submit: see BR-007. On a Draft the requester (and anyone else who passes the edit check) can submit. On status **New** (unsaved) Submit is disabled because status is not Draft.

Provenance: **Implemented**. What the cron job does after insert is **Unknown**.

### W6 — Approve, decline, cancel

Trigger: Approve, Decline, or Cancel Requisition. Each action saves the form first, then updates status.

| Action | Status written | `strAuthorizedBy` | Cron |
|---|---|---|---|
| Approve | `Approved` | Session user name, single quotes doubled | Deletes then inserts `FSR_RequisitionApproved`, status New |
| Decline | `Declined` | `Declined [{name}]` | Updates matching `sysCronJob` rows for that param to status Removed. The Decline path does not escape the name in the requisition update; it does escape the name on the cron update |
| Cancel | `Cancelled` | `Cancelled [{name}]` | Same Removed update, name escaped |

The Cancel button is rendered with `display: none` (comment 2015-02-08). The server action still exists.

Approve is disabled on Draft and on the unsaved New form (`permissionEditButtonApprove`). It is enabled when the Pending edit rules in BR-007 clear the disabled flag.

After Approved, a non-special user cannot edit. A special user also cannot edit once status is Approved (comment 2023-11-02). Completed, Declined, and Cancelled are locked for non-special users and for special users in group 6.

Nothing in this repository sets status to **Completed** or writes `strInvoiceNumber`. Provenance of those fields: **Implemented as stored columns**. The writer is **Unknown**.

### W7 — Sign out

Trigger: menu link `logout.php?Action=Logout`.

Behaviour: `session_destroy()` when `Action` is Logout, then the login layout with the text "You have successfully logged out." The menu on that layout is empty because `layout.login.inc.php` does not include `layout.menu.inc.php`.

Provenance: **Implemented**.

### W8 — User maintenance

Trigger: menu Users, only when `blnSpecial = 1`. `user.php` itself does not test `blnSpecial`. Any session that can open the URL reaches the page. Provenance: **Implemented**. Whether that gap is accepted is **Requires Confirmation**.

List columns: user, group, domain login, email, region, manager, alt manager, exec manager, last user, last edit, Active/InActive, select checkbox. Filter: All, Active, Inactive on `mstUser.blnActive`. Buttons: New, Make Inactive.

Make Inactive sets `blnActive = 0`. It does not delete the row. The method is still named `User::Delete`.

The editor requires, in the browser only: user group, user name, domain login, email, region. RepID is labelled optional. Checkboxes: Is Exec Manager, Is Manager, Is Active.

Server save (`User::Save`):

- Unchecked booleans are stored as 0.
- `strLastUser` becomes the session user's name.
- Insert refuses a domain login that already exists (`checkExistingDomainLogin`). Update does not run that check.
- After the main save, manager fields are rewritten:
  - Neither manager nor exec: store posted manager and alt manager; executive id 0.
  - Manager and not exec: manager id becomes this user; alt manager 0; executive id from the form.
  - Exec: manager id and executive id become this user; alt manager 0; `blnManager` is set to 1.

Manager dropdown: `mstUser` where `blnManager = 1`. Alt manager dropdown: all `mstUser` rows. Exec dropdown: `blnManagerExecutive = 1`. None of these three queries filter `blnActive`.

Provenance: **Implemented**.

## Integrations

| System | Direction | What is exchanged | Auth | Failure behaviour |
|---|---|---|---|---|
| IIS / Windows logon | Inbound | `LOGON_USER` | IIS, not implemented in PHP | Empty logon: login page waits. Unknown user: insufficient-access message |
| Active Directory via adLDAP | None in the running path | Library is vendored; `new adLDAP()` is commented out | Unknown | Not called |
| SQL Server via ODBC | Read/write | Users, requisitions, items, customers, products, usage log, cron jobs | DSN user embedded in `include/system.inc.php` | `odbc_exec` failure returns false; many queries do not die. `sqlObject::doQuery` dies with the SQL and `odbc_error()` |
| `sysCronJob` consumer | Outbound only from this app | Job type, param `RequisitionID=`, status | Database | Consumer not in this repo. Failure behaviour **Unknown** |
| Mail | Browser only | `mailto:` links for manager, alt, and exec from view `vieRequsitionManager` | User's mail client | No server-side mail in the live pages. `test.php` has commented `ini_set("SMTP", ...)` |

## Constraints

- Internet Explorer 6–11 is blocked by user-agent. Other IE versions and non-IE browsers are allowed. The check does not run on pages other than `login.php`.
- Session garbage-collection lifetime is 21600 seconds (6 hours), set in `system.inc.php`.
- Sales-manager approval ceiling is R10 000 (`$SALESMANAGERMAXVALUE`).
- Bill-to customers must be flagged promotional, except a customer already stored on the requisition.
- Product prices on save come from `dwdProduct`, not from the numbers shown in the browser.
- Footer cache headers are `no-cache` / `Pragma: no-cache` / `Expires: 0`.

## Requirements discovered from the implementation

### FR-001 — Windows sign-in

The system signs a person in when `LOGON_USER` matches an active `mstUser.strDomainLogin`, then opens the requisition list.

Provenance: Implemented. Confidence: High. The hardcoded replacement of the logon name (W1 step 3) means the deployed identity may not be the Windows user until that line is removed. **Requires Confirmation.**

### FR-002 — Refuse old Internet Explorer on the login page

Login refuses MSIE/Trident versions 6 through 11 and asks for Firefox or Chrome.

Provenance: Implemented. Confidence: High.

### FR-003 — List requisitions with session filters

A signed-in user can filter and sort the requisition list. The default filter is Pending for the current month.

Provenance: Implemented. Confidence: High.

### FR-004 — Draft a requisition and assign a pink-slip number

Saving a new requisition stores status Draft and pink-slip `ES` + 7-digit id, and copies the requester's manager chain.

Provenance: Implemented. Confidence: High.

### FR-005 — Price lines from the product table

Each saved line stores brand, product code, case and bottle quantities, case and bottle cost from `dwdProduct`, and a line total.

Provenance: Implemented. Confidence: High.

### FR-006 — Submit for authorisation

Submit sets status Pending and enqueues `FSR_NewRequisition`.

Provenance: Implemented. Confidence: High for the write. The notification that follows is Unknown.

### FR-007 — Approve under the ceiling

A manager, alternate, or executive assigned on the requisition can approve when status is Pending and the summed line total is strictly under 10000.

Provenance: Implemented. Confidence: High. The comparison uses the SQL `SUM(dblTotal)` cast to int on load (`(int) $rowR->dblTotal`), so cents are dropped before the ceiling check.

### FR-008 — Approve at or above the ceiling

Only the executive manager id on the requisition can approve when the total is greater than or equal to 10000.

Provenance: Implemented. Confidence: High. A special user (`blnSpecial = 1`) does not go through this ceiling. See BR-007.

### FR-009 — Decline

Decline sets status Declined and marks matching cron rows Removed.

Provenance: Implemented. Confidence: High.

### FR-010 — Maintain users

A special user can create users, edit them, and mark selected users inactive. Inactive users fail `LogginUser`.

Provenance: Implemented. Confidence: High for the behaviour. The menu gate is not repeated inside `user.php`.

### FR-011 — Record usage

Each successful `LogginUser` inserts `mstUsageLog` for system `FreeStock`.

Provenance: Implemented. Confidence: High.

## Non-goals visible in the tree

- Password login. There is no password column usage.
- Server-side email sending from the form pages.
- Deleting a user row. Make Inactive is an update.
- Editing invoice number or the alt-manager email timestamp from the form.

## Open points

See the human-confirmation list at the end of `technical-documentation.md`. The same items apply here.
