# Business rules — pernodstaff

Evidence date: 2026-10-07.

Source: `main` at `2ec41f8`.

Rules are taken from code and SQL. They are **Inferred** unless a comment or template states them as policy. They are not a signed business-rules catalogue. Where two sources disagree, both are recorded.

## Identity

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | Order-portal and admin sign-in use Azure AD only. The pages do not post a password for those modules. | **Inferred** from `login.aspx.vb` and `admin_login.aspx.vb` ("now uses Azure AD SSO only"). |
| BR-002 | An Azure AD email that is not already in `USERS` cannot sign in. The application does not create the row. | **Implemented** (`GetOrCreateUser`). Message: "Your account is not set up." |
| BR-003 | A successful Azure sign-in marks the user `isADUser = 1` if that flag was false or null. | **Implemented** (`UpdateUserAsADUser`). |
| BR-004 | Admin sign-in requires `accesslevel = 1`. Any other level is denied and tokens are not stored. | **Implemented**. |
| BR-005 | Merch sign-in requires `merchaccesslevel` 3 or 4. Null and any other value are denied. | **Implemented**. SQL comment also names 5 as Merch Viewer. **Conflict — Requires Confirmation.** |
| BR-006 | Merch AD users must use Microsoft sign-in. The password form tells them so. | **Implemented**. |
| BR-007 | Merch non-AD users need a password hash. An empty hash is told to use Forgot Password, then an email OTP, before the merch session starts. | **Implemented**. |
| BR-008 | Rep sign-in sends the typed password to `REPS_spLOGIN`. It does not use `PasswordService`. | **Implemented**. |
| BR-009 | Application logout does not sign the user out of Microsoft. | **Implemented** (comments on the three login pages). |
| BR-010 | `AllowSsoBypass` must be explicitly true, 1, or yes. Missing or blank means off. The template says never true in production. | **Documented** in `Web.config.template` and **Implemented** in `IsSsoBypassAllowed`. |

## Passwords (non-AD)

| ID | Rule | Provenance |
|---|---|---|
| BR-011 | A new password must be at least 8 characters and include an uppercase letter, a lowercase letter, a digit, and a character that is not a letter or digit. | **Implemented** (`PasswordService.ValidatePasswordComplexity`). Comment: non-AD users. |
| BR-012 | Accepted passwords are stored as PBKDF2-SHA256 (100,000 iterations) with a 32-byte salt. The stored value is salt plus hash, base64. | **Implemented**. |
| BR-013 | If a user update says the person is an AD user, a supplied password is cleared and not stored. | **Implemented** (`JSON_DATA_SERVICE`). |
| BR-014 | Forgot-password mail sends a reset link, not the password. | **Implemented**. Comment in `JSON_DATA_SERVICE`. |

## Employee number (`DSVAccount`)

| ID | Rule | Provenance |
|---|---|---|
| BR-015 | The order portal calls `DSVAccount` "Employee#". A blank value blocks order-portal login and later pages. | **Implemented**. |
| BR-016 | An administrator with a blank Employee# is sent to `admin_users_mini.aspx` to fill it in, instead of the order-portal error. | **Implemented**. |
| BR-017 | An active user cannot be saved with a blank Employee#. | **Implemented**. Message: "Employee# is required for active users." |
| BR-018 | Employee# is compared case-insensitively, trimmed. A duplicate asks for confirmation (`DSV_DUP_CONFIRM`). Confirming sets the other user's `active` to 0 and keeps the number on the user being saved. | **Implemented**. |
| BR-019 | Sending an order with a blank session Employee# is rejected before the procedure runs. | **Implemented**. |

## Staff ordering

| ID | Rule | Provenance |
|---|---|---|
| BR-020 | The cart and the month are capped at 12 bottles for every user. There is no birthday exception. | **Implemented**. Web constant and SQL comments dated 20260605 and 20260826. |
| BR-021 | The web method counts quantities already in the cart (columns `Quantity`, `quantity`, or `qty`) plus the change being saved. Send uses the current cart total only. | **Implemented** (`TryValidateCartBottleLimit`). |
| BR-022 | The latest `SHOPPING_CART_SpIns`, `SpUpd`, and `SpSend` count active cart lines (`Status = 1`) plus `ORDER_DETAIL.quantity` for headers in the current calendar month. | **Implemented** in scripts `015`–`017`. |
| BR-023 | A month-count order is skipped only when header status is `99` and `ERROR_LOG` has `component = PernodIntegration` and `operation = CreateOrder` for that order. A pending `99` without that error still counts, so a second checkout cannot skip the cap. | **Documented** in the script comment and **Implemented** in the SQL. |
| BR-024 | Older script `006` counted only lines with `ordstatus` 20 inside an open/close window, and its comment says 20 means processed successfully. Scripts `015`–`017` replaced that with the calendar-month rule above. | **Implemented** as successive scripts. Which body is installed is **Unknown / Requires Confirmation**. |
| BR-025 | Checkout stops when the server time is after `ORDER_TIMES.CLOSE_TIME` for `ORDER_TIME_ID = 1`. The error text is "Sorry, you missed the 17:00 cut off." | **Implemented**. The 17:00 figure is in the message, not read from the table. |
| BR-026 | A successful send creates a header and lines at status `99`, copies the cart reference into `clientOrderNO` and `BusinessUnit`, sets cart status to `2`, and computes VAT as 15% of line price times quantity. | **Implemented** in `SHOPPING_CART_SpSend`. |
| BR-027 | Order lines are joined to `PRODUCTS` on `DSVCode` where `active = 1` during send. | **Implemented**. |
| BR-028 | Staff catalogue queries `PRODUCTS_SpSel` and `PRODUCTS_SpSearch` drop inactive rows when an active column exists. | **Implemented**. |
| BR-029 | A placed order emails the user a confirmation (order number, warehouse reference, date, status, lines). A procedure error emails the user and any `OrderFailureAlertEmail` addresses, including a cart snapshot. | **Implemented**. |
| BR-030 | App setting `OrderQueriesContact` is `Gloria - gloria.kok@pernod-ricard.com`. | **Documented** in `Web.config.template`. Whether the email body uses it was not fully quoted here; the key exists. |

`ORDER_DETAIL` insert columns include `bc`, `pwr`, `cyl`, `axis`, `ad`, `patientname`, and `trialbox`. Those names are contact-lens fields. The current product language in the cap is bottles. Provenance: **Implemented** schema. Why those columns remain is **Unknown / Requires Confirmation**.

## Access levels

| ID | Rule | Provenance |
|---|---|---|
| BR-031 | Order access labels: 1 Administrator, 2 User, 3 Rep Manager. New users default to 2 in `USERS_SpUpd` when access level is omitted. | **Implemented** on the form and in `Merch/003_update_user_stored_procedures.sql`. |
| BR-032 | Merch labels on the form: blank = no access, 3 Merch Clerk, 4 Merch Admin. | **Implemented**. |
| BR-033 | Clerk and admin may use POS, invoices, and the three reports. Only admin may use products, stock import, receive, adjust, categories, brands, and settings. | **Implemented** (`MerchAuth.UserHasMerchAccess` on each page). |
| BR-034 | With no level list, `MerchAuth` allows 3 or 4. That is what `merch.master` uses. | **Implemented**. |
| BR-035 | Admin navigation items render only when `Session("accesslevel") = 1`. The master page does not repeat that check. | **Implemented**. **Requires Confirmation** whether URL access by a non-admin session is forbidden elsewhere (it is not in the master). |

## Merchandising sales

| ID | Rule | Provenance |
|---|---|---|
| BR-036 | POS will not sell a quantity above stock on hand. Message: "Insufficient stock for {sku}". | **Implemented**. |
| BR-037 | Inactive products are removed from the POS cart before checkout. | **Implemented**. |
| BR-038 | A completed sale inserts `MERCH_INVOICE` with `paymentmethod = 'card'` and `status = 'paid'`. | **Implemented**. No other payment method is written by `pos.aspx.vb`. |
| BR-039 | Invoice numbers are `invoiceprefix` plus an incremented seed from merch settings. | **Implemented**. |
| BR-040 | POS reads VAT from setting `vatrate` and the code comment says prices are VAT-exclusive. The table comment on `MERCH_INVOICE_LINE.unitprice` says the price is VAT-inclusive. | **Conflict — Requires Confirmation.** |
| BR-041 | Staff-order VAT in `SHOPPING_CART_SpSend` is a fixed multiplier `1.15`. Config key `VAT` is `15`. | **Implemented** / **Documented**. |
| BR-042 | Product SKU is unique (`UQ_MERCH_PRODUCT_sku`). | **Implemented** in `001_create_tables.sql`. |
| BR-043 | Stock movement types named in the table comment are `in`, `out_sale`, and `adjustment`. Edit-log change types named there are `opening`, `receive`, and `adjustment`. | **Documented** in the create script. |

## Data and audit

| ID | Rule | Provenance |
|---|---|---|
| BR-044 | Login attempts are logged with module name, type, IP, and user agent. | **Implemented**. |
| BR-045 | Application errors go to `ErrorLogger`, which records component, operation, message, exception, email, and optional order id. Script `014_ERROR_LOG_add_orderid.sql` adds `orderid`. | **Implemented**. |
| BR-046 | JSON calls require the session key to match app setting `KEY`. | **Implemented**. |
| BR-047 | After that key check, the browser-supplied `spAlias` is executed as a stored procedure. | **Implemented**. This is an application constraint, not a business allowance. **Requires Confirmation** that the database login cannot run procedures outside the product set. |

## Legal text on the site

`terms.aspx` says the terms are governed by English law, that privacy contact is `info@pernodstaff.co.za`, and that personal information is kept only as long as needed. Provenance: **Documented** on the page. It also contains template wording (affiliate accounts, Google remarketing, "booking"). Whether that text matches Pernod Ricard policy is **Unknown / Requires Confirmation**.

## Explicitly not a rule

- The Bootstrapious readme's licence (keep the theme footer link, do not redistribute the template) is the theme vendor's licence in `readme.txt` and `license.txt`. It is not a Pernod ordering rule.
- No rule was found for a minimum order value, a per-SKU maximum, delivery charges, or who may approve an order before it is sent. Status `99` is written at send time. What happens after that, including JDE, is outside this repository.
