# Business rules — kb-online (Kohler Box)

Evidence date: 2026-10-11. Source: `git@bitbucket.org:overdrive/kb-online.git`, `main` at `5aa7141` (2026-09-17).

No business-rules document exists in the source repository. Rules below are taken from code, SQL comments, and on-screen instructions. Each rule is marked **Inferred** when the code behaves that way and no separate requirements document states it. Nothing here is a new policy.

Database-owned rules (menu, security matrix, full status list, email template text, numeric settings) cannot be listed in full. They are marked **Unknown / Requires Confirmation**.

## Identity and access

### BR-001 — Who may sign in

**Inferred.** A person may sign in only when `sysUser.strEmail` matches the typed username, `sysUser.blnActive = 1`, the linked `sysSecurityGroup.blnActive = 1`, and `strPasswordMD5` equals `md5` of the typed password.

Evidence: `login.php`.

### BR-002 — Failed password message

**Inferred.** A wrong password and an unknown user do not use the same sentence. Unknown user or inactive group: "Login Details are invalid." Inactive user: "User account is inactive." Wrong password: "Username or Password is incorrect."

Evidence: `login.php`.

### BR-003 — Lockout warning

**Inferred.** After five of the latest relevant login rows are "Login failed: Incorrect password" with no later "Login successful" among those five, the system emails the user and tells them the account is suspended. The save that would set `blnLogin = 0` is commented out, and login does not read `blnLogin`.

From two failures upward, the page also says how many attempts remain, using a limit of five.

Evidence: `CheckSecurityLock` in `system.functions.inc.php`.

**Requires Confirmation:** whether the business still expects a real suspension.

### BR-004 — Page access

**Inferred.** Opening a Nemo page requires a session and `blnView = 1` for that script and security group. Five denied attempts in the session go to the internal-error message. Fewer go to the insufficient-access message.

Evidence: `Nemo::iniSecurity`, `message.php`.

### BR-005 — Pricing visibility

**Inferred.** On the estimation price matrix, unit prices are hidden and read-only unless `estimation.php` has `blnSpecial = 1` for the user's group.

Evidence: `Order::renderProductPriceMatrix`.

### BR-006 — Factory card reprint

**Inferred.** If `documents/Orders/{order number}.order.xls` does not exist yet, the user may generate it. If it exists, only security group 1, security group 2, or one of these mailboxes may generate it again:

- `marsilee@kohlerbox.com`
- `kayla@kohlerbox.com`
- `andriah@kohlerbox.com`
- `abigail@kohlerbox.com`
- `gerrie@kohlerbox.com`
- `accounts@kohlerbox.com`

A 2019 comment says reprint was for Admin Sys only. The mailbox list is an additional implemented exception. Kayla Koopman was added in commit `5aa7141` (2026-09-17).

Evidence: `_nemo.details.order.cls.php`.

**Requires Confirmation:** this allowlist is a product decision encoded as literals. Confirm it is still approved.

### BR-007 — Default list for some groups

**Inferred.**

| Security group | Default on the order list |
|---|---|
| 4 | Status filter `Order: New`. Comment calls this group SalesPerson |
| 6 | Status filter `Order: In Production`. Role name not in code |
| 7 | "To Be Invoiced" checked, unless the action is already `Filter` |

Evidence: `Order` constructor in `includes/order.cls.php`.

## The job record

### BR-008 — One row, status prefix

**Inferred.** Enquiry, estimation, quote, and production order are the same `tblOrder` row. Live lists use views that keep `blnArchived = 0` and a status prefix `Enquiry:`, `Estimation:`, `Quote:`, or `Order:`.

Evidence: view SQL in the header comment of `includes/order.cls.php`.

The allowed suffixes are the setting `ORDER_STATUS`. The full list is **Unknown / Requires Confirmation**.

### BR-009 — First save of a new estimation or quote

**Inferred.** Saving a row whose status is `Estimation: New` changes it to `Estimation: In Progress`. Saving `Quote: New` changes it to `Quote: In Progress`. This runs before an explicit status override.

Evidence: `Order::Save`.

### BR-010 — Hand-off status changes

**Inferred.** These succeed only when the email send reports no error:

| Action | New status |
|---|---|
| Send to estimator | `Estimation: New` |
| Send estimation to salesperson | `Quote: New` |
| Send quote | `Quote: Sent to Customer` |
| Convert estimate to quote | `Quote: New` |

Evidence: the matching methods on `Order`.

### BR-011 — Confirm order does not reset status

**Inferred**, and stated in a code comment dated 2026-06-05. `Confirm Order` sends the confirmation email and does not set `strStatus` to `Order: New`.

Evidence: `Order::ConfirmOrder`, commit `6a901e3`.

### BR-012 — Quote save forces a new order

**Inferred** from code, and in tension with the on-screen quote instructions. `quote.php` action `Save` calls `Order::Save` with override `Order: New`.

The instructions tell the user to convert to an order as a separate step after acceptance.

Evidence: `quote.php`, `OrderDetails` quote instructions.

**Requires Confirmation:** which of those two is the intended rule.

### BR-013 — Order number

**Inferred** from `Order::Save`. If the number is empty after insert, it becomes the current year-month (`ym`) concatenated with the last six characters of the order id left-padded with zeros to width 8.

A comment labelled `BR:` says the id is padded to six digits, with examples `1406000001` and `1406000123`. That comment does not match the pad width in `Order::Save`.

**Requires Confirmation:** which formula production data actually uses.

### BR-014 — Order date and due date

**Inferred.** On save, an empty or `0000-00-00` order date becomes today. An empty or `0000-00-00` due date becomes SQL `NULL`. On an order (not on earlier stages), the form requires an order date.

Evidence: `Order::Save`, `OrderDetails::Transform`.

### BR-015 — Archive, not delete

**Inferred.** The delete action on pipeline lists sets `blnArchived = 1` and `strLastUser` to the current username. The message is "Records Archived."

Evidence: `Order::Delete`.

### BR-016 — Printed flag

**Inferred.** A normal save writes back the previous `blnOrderPrinted`. The flag is meant to change when the production spreadsheet is viewed (`jsUpdatePrintedOrder`).

Evidence: comment dated 2019-11-07 in `Order::Save`, and `jsUpdatePrintedOrder` in `OrderDetails`.

### BR-017 — Enquiry edit lock is instructional only

The enquiry instructions say the enquiry will not be editable after it is sent to the estimator. No status check was found that enforces that sentence.

Provenance: **Implemented** help text. The lock itself was not found. **Requires Confirmation.**

## Product and price

### BR-018 — Dimension match tolerance

**Inferred.** Product search by height, width, or length matches the exact value or any value within 2 percent either side.

Evidence: `Product::getList`.

### BR-019 — Sheet size from the style

**Inferred.** Internal dimension text is `{length} x {width} x {height}`. When `tblStyle.blnManualCalculations` is not 1, crease specs are calculated by substituting L, W, and H and evaluating each colon-separated segment. Sheet length is the sum of panel segments. Sheet width is the sum of flap segments, except when the panel spec is exactly `L` and the flap spec is exactly `H` (comment: "Creased Sheet and Sleeve"), in which case sheet width is the plain width. When manual calculations are on, the first pair of calculated sheet fields is set to 0.

Evidence: `jsGetCalculatedProductInfo` in `ajaxfunctions.php`.

### BR-020 — Price check

**Inferred.** `dblPriceCheck` on a product price is `round(board cost * product weight, 4)`, updated when the stored value differs.

Evidence: `updateProductPriceCheck`.

### BR-021 — Quote prices included by default

**Inferred.** If the user posts `chkIncludeInQuote`, existing `tblQuoteProductPrice` rows for the order are deleted and the checked prices are inserted. If that array is absent and the status does not start with `Order`, every `tblProductPrice` row for the order is inserted with `INSERT IGNORE`.

Evidence: `Order::Save`.

### BR-022 — Estimate PDF after a quote price change

**Inferred.** Saving from `quote.php` a price row that was included in the quote regenerates the Kohler Box estimate PDF and tells the user to look under Quote Details, Estimate Filename.

Evidence: `Order::Save`.

### BR-023 — Customer commercial flags

**Inferred.** A customer created from the enquiry dialog stores Pastel account code, price-increase contact name and email, `blnAgent`, `blnPersonalisePriceList`, `blnNoOvers`, and `blnNoUnders`.

What no-overs and no-unders change in later calculations was not established in the paths read for this document. **Unknown / Requires Confirmation** beyond the fact they are stored.

Evidence: `jsSaveNewCustomer`.

### BR-024 — Annual increase is a queue

**Inferred.** Upload fills `tmpProductPrice`. While that table has rows, the toolbar offers "Process Annual Price Increase" and warns that a timeout should be followed by a refresh. When the queue is empty, the toolbar offers "Confirm Annual Price Increase" and asks for confirmation before updating the queued rows and all other product pricing.

Evidence: `product.price.increase.php`.

## Delivery and invoicing

### BR-025 — Completing a delivery marks it to invoice

**Inferred** from on-screen instructions and from `jsDeliveryComplete`. Checking completed checks the invoice flag and enables delivery date, units, and invoice number. If the delivery date is empty when completed is checked, the date field is hidden and then handled further in that function.

Evidence: `OrderDetails` order instructions, `jsDeliveryComplete`.

### BR-026 — To-be-invoiced list

**Inferred.** The filter keeps orders that have a delivery whose invoice number is null or empty, and whose delivery date is not null or is not `0000-00-00`. Because the date tests are combined with `OR`, an empty date can still match.

Evidence: `Order::getList`.

**Requires Confirmation:** whether a delivery with no date should appear on this list.

### BR-027 — Outstanding production

**Inferred.** The Outstanding Production Orders report includes statuses `Order: New` and `Order: In Production` where quantity is not zero.

Evidence: `includes/outstanding.production.orders.rpt.php`.

### BR-028 — Dormant customers

**Inferred.** The Dormant Customers report defaults to an interval of 6 weeks. The user may choose 3, 6, or 12 months, or all.

Evidence: `includes/customer.order.rpt.php`, `report.php`.

## Documents and mail

### BR-029 — Outbound identity

**Inferred.** Pipeline mail is sent from setting `SMTP Send As`, BCC `SMTP BCC`, and Reply-To the salesperson email when that email is not empty.

Evidence: commit `75ca321` and the send methods on `Order`.

### BR-030 — Estimate email needs a file

**Inferred.** `EmailEstimate` returns a failure telling the user to generate the PDF if the file is missing, and a failure about estimator details if the recipient is empty.

Evidence: `Order::EmailEstimate`.

### BR-031 — Quote email attachment

**Inferred.** Send Quote attaches `{dir_quotes}/{order number}.quote.pdf` and writes status `Quote: Sent to Customer` only when send reports no error.

Evidence: `Order::SendQuote`.

### BR-032 — Order number in the subject

**Inferred.** Subjects loaded from templates have `[strOrderNumber]` replaced with the job's number on the enquiry, estimation, quote, and estimate sends.

Evidence: those methods.

## Data import

### BR-033 — Import sheet states

**Inferred.** The import screen recognises sheet states Old, New, Edited, Approved, and Processed. The list filter defaults to New. The legal values of `impSheet.strStatus` are the database enum.

Evidence: `data.import.php`, `Sheet` constructor.

## Rules that are not enforced in this tree

| Claim | What the code does |
|---|---|
| Account suspension after five bad passwords | Email and message only. Save is commented out |
| Enquiry locked after send | Instruction text only |
| SMS sending | Function exists, no caller |
| Two-factor login | Files exist. `login.php` does not call them. Commit says OTP was reverted |
| Only Admin may delete a customer | Comment in `includes/customer.cls.php` is commented out |

## Unknown / Requires Confirmation

- Names and intended duties of security groups other than the comments for groups 1, 2, and 4.
- Full `ORDER_STATUS` catalogue.
- Whether no-overs and no-unders change manufactured quantity.
- Whether Mailchimp CSV and the Pastel account code are still operational business rules.
- Whether the PRSA wording in the OTP seed is a mistaken copy. It is not a Kohler Box rule.
