# Business rules — Paterson & Cooke Management System

Evidence date: 2026-10-08. Source: `git@bitbucket.org:overdrive/patersoncooke.git`, `master` at `008c5ac`.

Rules below are taken from conditions in the PHP. They are **Inferred** as business rules where the code enforces them and no separate policy document exists. The repository has no business-rules document of its own.

Numeric flags: `0` none or off, `1` view or yes, `2` full access or the "off" value used by some boolean columns (see BR-006 and BR-011).

## Access

### BR-001 — Only an active personnel row can sign in

**Inferred** from `login.php`. `blnActive` must equal `1`. Any other value, including the delete value `2`, is rejected with the disabled-account message.

### BR-002 — The password check is an MD5 comparison

**Inferred** from `login.php` and `personnel.cls.php`. The browser sends the MD5 of what was typed. The server computes `md5` of `tblPersonnel.strPassword` and compares the two digests. The personnel form stores the typed password, not a digest.

The personnel form's browser check requires 6 or more characters and at least one digit. The server does not repeat that check.

### BR-003 — A missing cookie returns the person to the login page

**Inferred** from `checkLogin`. The cookie is not revalidated against the database. A cookie that is present and non-empty is enough.

### BR-004 — Area access is None, View, or Full

**Inferred** from `securitylevel.cls.php` and `checkSecurity`. The page opens when the level's column is greater than or equal to `1`. New, Save, and Delete are shown when the column is greater than or equal to `2`.

Columns enforced on a page header: `blnActivityCode` (also used by activity category), `blnBillingCode`, `blnBillingRate`, `blnClient`, `blnCurrency`, `blnFeeStructure`, `blnOffice`, `blnPersonnel`, `blnProject`, `blnProjectstatus`, `blnSecurityLevel`, `blnSettings`, `blnWorkgroup`.

`blnReports` is edited on the security-level form. No page that was inspected calls `checkSecurity("blnReports", ...)`.

Invoice and report-console gates are commented out. They name columns `blnInvoice` and `blnReportConsole`, which are not on the security-level form.

### BR-005 — Timesheet visibility and edit rights

**Inferred** from `timesheet.cls.php` and `completedtimesheet.cls.php`.

- The signed-in person can always open their own sheet.
- If their level has `blnTimesheetOffice` greater than 0 and `blnTimesheetAll` is 0, the person list is the office list.
- If `blnTimesheetAll` is greater than 0, the person list is the all-personnel query.
- They may save another person's sheet in the same office only when `blnTimesheetOffice` is 2.
- They may save another office's sheet only when `blnTimesheetAll` is 2.
- A week with `blnPersonnelSignOff = 1` does not offer save on the timesheet screen.

`checkSecurity` is called in the timesheet class without quotes around the field name (`checkSecurity(blnTimesheetOffice,2)`). In PHP that uses a constant or, if the constant is undefined, the bare string. Behaviour depends on the PHP version's treatment of undefined constants. **Requires Confirmation** on PHP 7.4, which is the version named in the README. The same pattern appears in the 2010-01-07 copy of the class.

### BR-006 — Personnel are not hard-deleted

**Inferred** from `sql_personnel_delete` and `sql_personnel_insert`. Delete sets `blnActive = 2`. A new or updated person whose active checkbox is not 1 is stored as 2.

Login treats only `1` as active (BR-001).

### BR-007 — Usernames are unique

**Inferred** from `sql_personnel_username_check_unique` on personnel save. The message shown is "{display name} already exists!" even though the query checks the username.

### BR-008 — A person should have one primary billing code

**Inferred** from `Personnel::Save`. If `radPrimary` is below 1, the page reports "No default billing code selected!" after the personnel row has already been inserted or updated. Billing-code links are written only when a primary is selected. Internal-disbursement billing codes cannot be chosen on the matrix (checkboxes disabled).

### BR-009 — View-only personnel access can still change own login

**Inferred** from `Personnel::AddEdit`. Full personnel access edits every field. View access makes office, workgroup, supervisor, security level, display name, and email read-only, and enables username and password only when the record is the signed-in person.

## Timesheets

### BR-010 — The default week is the one after the last signed-off week

**Inferred** from `Timesheet::AddEdit`. For the current year, the screen selects `MAX(intWeek)+1` among weeks with `blnPersonnelSignOff = 1`. If that query does not yield a week, it uses the current ISO week.

The completed-timesheet board defaults to the previous ISO week, minimum week 1.

### BR-011 — Projects on a timesheet must be open for entry and linked to the person's office

**Inferred** from `sql_timesheet_get_projects`. The project status must have `blnEnter = 1`, and `tblProjectOffice` must link the project to the personnel row's office.

### BR-012 — Activity codes follow project work versus non-project work

**Inferred** from `timesheet.php`. Project id 0 loads activity codes with `blnProjectWork <> 1`. Any other project loads codes with `blnProjectWork = 1`.

### BR-013 — Hour constraints are multiples of 4 or 8

**Inferred** from `activitycode.cls.php` and `validateTimesheetForm`. A code's constraint is none (`0`), 4, or 8. On save in the browser, total hours must be a multiple of that number when it is greater than 0. A constraint of 4 therefore also accepts 8. The server `Save()` does not repeat this test.

### BR-014 — Half-hour cells cannot overlap on the same day

**Inferred** from `RodentOver` in the timesheet script. A cell index already selected on another row of the same day is not toggled. Each selected cell adds 0.5 to the row total. There are 36 cells.

### BR-015 — Zero hours or the delete flag removes an existing line

**Inferred** from `Timesheet::Save`. If the row id is non-zero and hours are 0 or the delete checkbox is set, `tblTimesheet` and `tblTimesheetHours` for that id are deleted.

### BR-016 — Sign-off is a flag on the week

**Inferred** from `AcceptTSR`. Accept sets `blnPersonnelSignOff = 1` on `tblWeek`. It does not store the accepter or a timestamp. The review screen hides Accept once the flag is 1 and shows "Timesheet has been Signed Off".

### BR-017 — Reopen requires Full timesheet scope

**Inferred** from `CompletedTimesheet::View` and `Reactivate`. The button is shown when `blnTimesheetOffice` is 2 or `blnTimesheetAll` is 2, and the week is signed off. Reactivate sets the flag back to 0 for that personnel id, week, and year.

### BR-018 — An outstanding week can be reminded by email

**Inferred** from `SendMail`. The message states that the timesheet for that week and year is outstanding and asks the person to complete it. The sender is the signed-in person's display name and email.

## Projects and statuses

### BR-019 — A project has one primary office

**Inferred** from `validateProjectForm`. Save is blocked in the browser when no `radPrimary` office is checked. The server path was not found to repeat that check.

### BR-020 — Enter, Review, and Documents mean different things

**Inferred** from readers of `tblProjectStatus`.

| Flag | Effect found |
|---|---|
| Enter (`blnEnter = 1`) | Project is eligible for timesheet project lists (with the office link in BR-011) |
| Review (`blnReview = 1`) | Project is eligible for invoice and report-console project lists |
| Documents (`blnDocuments`) | Stored and displayed on the status screen. No other reader found |

The names of the statuses themselves are data. **Unknown** without the database.

### BR-021 — Project number is required in the browser

**Inferred** from `validateProjectForm`. Empty `strProjectCode` blocks submit. Technical contact and commercial contact are also in that message list. A uniqueness check for project number was not found in `Project::Save` during this inspection. **Requires Confirmation** if the database has a unique index that the PHP does not mention.

### BR-022 — Proposal time is distinguished by activity code PROP

**Inferred** from `dashboard.cls.php`. Hours whose activity code is `PROP` are summed separately from other hours and labelled as billable bucket `3`. Other hours are grouped by `tblBillingCode.blnBillable`. The code `PROP` is a literal in SQL, not a setting.

## Rates and billing codes

### BR-023 — A billing code is either billable or an internal disbursement

**Inferred** from the billing-code form script. Checking one box clears the other. On save, if billable is not 1 it is stored as 2 (`sql_billing_code_insert`).

### BR-024 — Billing rates are unique per billing code and fee structure

**Inferred** from `sql_billing_rate_check_unique`. Project billing rates are unique per billing code, fee structure, and project (`sql_project_billing_rate_check_unique`).

Each rate has external, internal, and cost amounts, and a project rate also has an effective date.

### BR-025 — An activity category is project-work or leave, not both

**Inferred** from `activitycategory.cls.php` `checkACTypes`. Checking project-work clears leave, and the reverse.

Leave reports select timesheet lines where `tblActivityCategory.blnLeave = 1` (`sql_leave_report_get`, comment in SQL says the query was added 2009-07-23).

### BR-026 — Leave duration is shown in days derived from hours

**Inferred** from `include/reports/leave.cls.php`. The report label says hours are presented as days (hour / 8). The value used is `roundUp($row->dblHours, 8, 5)`. `roundUp` in `system.inc.php` divides by the divisor and by 5, rounds to 2 decimals, rounds to 1 decimal, then multiplies by 5 again.

## Invoices and disbursements

### BR-027 — Invoice numbers are unique

**Inferred** from `sql_invoice_check_unique` in `Invoice::Save`. A duplicate shows "{number} already exists!" and does not write the header.

### BR-028 — Invoicing lists use review-status projects

**Inferred** from `sql_project_list_cmb("tblProjectStatus.blnReview = 1")` on the invoice timesheet, invoice summary, disbursement, and report console screens.

### BR-029 — A new fixed-price invoice creates fee rows from the project's offices

**Inferred** from `Invoice::Save`. When the new id is positive and `radFixedPrice` is 1, `sql_invoice_fee_insert_project_office` runs for every project office if a primary office was found, otherwise for the selected office only.

### BR-030 — External and internal are invoice modes

**Inferred** from `invoice.cls.php`. The header has an external checkbox and an internal flag. The browser helper `lockdown` changes which controls stay editable after the invoice exists. Rate display uses external versus internal amounts from project billing rates. The commercial meaning of "external" (client invoice versus inter-office) is **Inferred** from the labels only.

### BR-031 — Site allowance uses a billing code named Site Allowance

**Inferred** from `sql_internal_disbursement_PBR_get(..., "Site Allowance", ...)`. If that billing code is absent from project rates, the rate lookup returns no row. The screen still lists site-allowance rows.

### BR-032 — Deleting an invoice deletes its child links

**Inferred** from `Invoice::Delete`. It deletes `tblInvoice`, `tblInvoiceItem`, `tblInvoiceFee`, `tblInvoiceSiteAllowance`, `tblInvoiceInternalDisbursement`, and `tblInvoiceExternalDisbursement` for that id. It does not delete the underlying timesheet rows. Whether site-allowance and disbursement header rows remain is visible in `Delete()`: those base tables are not deleted there. An earlier block in `invoice.cls.php` (around the list/open path) copies link rows and then clears `refInvoiceID` on site allowance and disbursements. **Requires Confirmation** of the exact unlink sequence against a running database, because both paths exist.

## Reporting filters

### BR-033 — Report people lists hide names that start with Admin

**Inferred** from `reportconsole.cls.php`. Personnel, project director, and project manager queries use `strDisplayName NOT LIKE 'Admin%'`.

### BR-034 — Signed-off-only and admin-hours are report options

**Inferred** from the report console. Checkboxes `chkSignOff` and `chkAdminHours` are passed to the report URL. Timesheet detail SQL has parameters `blnPersonnelSignOff` and `blnAdminHours` (`sql_timesheet_detail_get_entries_admin`). Which reports honour both flags depends on the report class. The console disables controls per report in `enableVariable`.

### BR-035 — Ignoring the date range means all history up to today

**Inferred** from the leave report and the dashboard. When `chkIgnoreDate` is true, the start becomes `19700101`. Leave ends at today (`Ymd`). The dashboard ends at the last day of the current month (`Ymt`).

## Rules that are not established

- Target hours per day or per week, other than the activity-code constraint and the 36-cell grid.
- Who is allowed to accept a timesheet. The review page shows Accept whenever the week is not signed off and the person can open the page.
- Approval by the supervisor. The supervisor is stored and can be CC'd on a reminder. No approve/reject state was found.
- VAT calculation. Client VAT and project VAT are fields. No tax total was traced in the invoice save path during this inspection.
- Currency conversion. A fee structure has one currency. No FX rate table was found.
- Retention, audit, or a required close of a month. Close-out is a report (`closeout.php`), not a posting process that locks data, as far as the page class shows.
- The meaning of security descriptions and codes. They are data.
