# Business rules — convergence

Evidence date: 2026-10-18. Source: `master` at `6b66438`.

Rules below are what the PHP does. Where the code is the only source, the rule is marked **Inferred** only when the code does not state the rule in a comment or a literal. Rules that are direct conditions in the code are **Implemented**. No rule was taken from `convergence_production`.

Live labels for statuses, priorities, and menus are rows in MySQL. Those rows are **Unknown**.

## Access

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | Sign-in matches `sysUser.strEmail`. The password is checked against the latest `sysPassword` row for that user id and type `User` | Implemented, `login.php` |
| BR-002 | The user's security group must be active (`sysSecurityGroup.blnActive = 1`) or the login is treated as not found | Implemented |
| BR-003 | `sysUser.blnActive` must be 1 or the message is "User account is inactive." | Implemented |
| BR-004 | A correct password stores user id, username, email, security group id, and department id in the session and opens `index.php` | Implemented |
| BR-005 | Five "Login failed: Incorrect password" rows since the last "Login successful", looking at the latest five relevant rows, set `blnActive` to 0 and email the user | Implemented, `CheckSecurityLock`. The count 5 is a literal |
| BR-006 | Failed and successful logins are written to `sysLogin` with IP, result, and email. The password column write is commented out | Implemented, `tblLoginInsert` |
| BR-007 | `Nemo` pages redirect to `login.php` when `$_SESSION[USER]` is unset. Comment 2017-03-07: guest users removed | Implemented |
| BR-008 | After security flags are loaded, the current page's `blnView` is set to 1. The denial redirect therefore does not run for a logged-in user | Implemented, `Nemo::iniSecurity`. Comment: added for testing, 2017-01-10 |
| BR-009 | Menu level-2 items render only when `blnMenuItem = 1` and the group has a `sysSecurity` row. A row with view not allowed is rendered as an inactive item in one menu builder | Implemented, `_nemo.menu.cls.php` |
| BR-010 | New email send is limited to security special. Comment in `includes/email.cls.php`: "only Security.Special to send New Emails." | Documented in that comment. The flag check itself was not re-traced past the comment |
| BR-011 | `ticket.ops.php` and `cron.daily.createticket.php` construct `NemoBasic` and do not use the `Nemo` session redirect | Implemented |
| BR-012 | Password policy settings are created on first use if `sysPassword` does not exist: policy on, minimum length 8, max history 8, restricted words `password` and `123`, reset interval `60 MINUTE`, max age `false` | Implemented bootstrap. Live `sysSettings` may differ. **Unknown** |
| BR-013 | When strength checking is on, the password must meet the minimum length and include a lower-case letter, an upper-case letter, and a digit | Implemented, `validatePasswordJS` |
| BR-014 | A reset link is expired according to `PASSWORD_Reset_Interval` | Implemented. Default bootstrap `60 MINUTE` |
| BR-015 | Session files are stored on disk. `session.gc_maxlifetime` is 259200 seconds | Implemented, `system2.php` |

## Clients and suppliers

| ID | Rule | Provenance |
|---|---|---|
| BR-020 | The client list excludes `strClientType = 'Supplier'` unless the type filter is set | Implemented, `includes/client.cls.php` |
| BR-021 | A supplier screen is the client entity filtered to type supplier | Implemented. Comment in `supplier.php` |
| BR-022 | Deactivating a client sets `blnActive = 0`. It does not delete the row | Implemented |
| BR-023 | A blank `strAccountCode` was a known SQL error and was guarded (comments 2017-03-01 on client and supplier) | Implemented as a fix comment. The current condition is the code in those classes |
| BR-024 | Ticket and time client dropdowns use `vieClient`, and several of them require `blnActive = 1`, keeping the current client if it is inactive | Implemented |
| BR-025 | `vieClient`'s definition is not in the repository | Unknown |

## Tickets and authorisation

| ID | Rule | Provenance |
|---|---|---|
| BR-030 | Ticket status labels for the dropdown come from `crmDepartmentStatus` for process `Ticket` and the ticket's department | Implemented, `DepartmentStatus::getDepartmentStatusSQL` |
| BR-031 | If `crmClient.blnRequiresAuthorisation = 1`, the ticket status is set to `Pending Authorization`. Otherwise it is set to `New` | Implemented, `ticket.php` (comment 2017-11-27) |
| BR-032 | While status is `Pending Authorization`, the status control is read-only | Implemented |
| BR-033 | The authorisation request button shows when the ticket exists, approval date is empty or `0000-00-00`, and the client requires authorisation | Implemented |
| BR-034 | Authorisation mail goes to each active contact of the client with `blnCanAuthoriseJobs` true | Implemented, `Ticket::RequestAuthorization` |
| BR-035 | The template name loaded for that mail is `Ticket Request Authorization` | Implemented |
| BR-036 | Authorise sets status `New Authorized`, approval date to now, and approver to the contact name, only when approval date and completed date are empty and the ticket is not archived | Implemented, `AuthorizeClient` |
| BR-037 | Decline sets status `Declined`, `blnArchive = 1`, completed date to now, and the same approver, under the same preconditions | Implemented, `DeclineClient` |
| BR-038 | A repeat authorise or decline does not change the row. The message says the ticket was already authorised or declined and names `strApprovedBy` | Implemented |
| BR-039 | The links in the authorisation mail point at `lamp/Convergence_2017/dev/ticket.ops.php` with `Action`, `TicketID`, and `ContactID` | Implemented |
| BR-040 | Ticket list department filter defaults to the session user's department | Implemented |
| BR-041 | Foreign keys described in comments on `crmTicket` point owner, department, client, and contact at `sysUser`, `crmDepartment`, `crmClient`, and `crmContact`, with `ON DELETE CASCADE` | Documented in comments in `includes/ticket.cls.php`. Whether the live database has those constraints is **Unknown** |

## Tasks, time, and SLA

| ID | Rule | Provenance |
|---|---|---|
| BR-050 | A task references a ticket. Time references a task. Comments also declare cascade foreign keys to user, ticket, and task | Documented in comments in `includes/time.cls.php`. Live constraints **Unknown** |
| BR-051 | The time list defaults to the current user and the current calendar month | Implemented |
| BR-052 | An SLA schedule produces a ticket only when the schedule is active, the SLA ticket is not terminated (null or after today), and the weekly text contains today's weekday | Implemented, `cron.daily.createticket.php` |
| BR-053 | Monthly and yearly schedule texts are optional. If set, they must contain the computed week-of-month label or the three-letter month | Implemented |
| BR-054 | Week-of-month is First, Second, Third, or Fourth. ISO week index 4 and 5 both map to Fourth Week | Implemented |
| BR-055 | The created ticket and its tasks are status `New`, active, due today. Tasks are not completed | Implemented |
| BR-056 | The new ticket id is `MAX(TicketID)` after the insert | Implemented. **Inferred:** concurrent runs can attach tasks to the wrong ticket |
| BR-057 | The cron page prints a link back to `index.php` and does not require the `Nemo` session | Implemented |
| BR-058 | SLA task comments say the 2018 screen has tabs for Task, Billing, Fault and Fix, and Technical | Documented in `sla.task.php` |

## Proposals and products

| ID | Rule | Provenance |
|---|---|---|
| BR-070 | A new revision number is `Q` + quote number padded to 7 digits + `.` + next revision | Implemented, `Proposal::NewRevision` |
| BR-071 | The previous proposal's status is set to `3c. Revised` | Implemented |
| BR-072 | The duplicated proposal's status is set to `Draft` | Implemented, `Proposal::Duplicate` |
| BR-073 | Duplication copies items and can refresh cost from the product | Implemented. Comment: "duplicate quote & quote item, but pull the updated cost from Product" |
| BR-074 | Selected proposal items can be updated so `dblBaseCost` equals `crmProduct.dblCost` | Implemented, `UpdateProposalItemsCostFromProduct` |
| BR-075 | Ad hoc product lines allow edited cost and margin. Margin percentage is recalculated when margin cost is edited | Documented in comments on `product.bundle.php` and `proposal.template.php` (2017-09-12) |
| BR-076 | Product text is passed through `cleanProductFields`, which drops characters outside letters, digits, and a fixed punctuation set | Implemented, `project.functions.inc.php` |
| BR-077 | Proposal PDF is A4 via mPDF and always writes the exc. VAT, VAT, and incl. VAT totals. A 2017-03-17 comment says the grand total is hidden when `blnShowGrandTotal` is 1. The PDF writer does not read the flag | Implemented write. The hide behaviour is a comment only |
| BR-078 | Supplier price-request subject is "Price request from Silicon Overdrive" | Implemented |
| BR-079 | Supplier order subject is `Supplier Order [SupplierOrderNumber]: Silicon` | Implemented |
| BR-080 | Creating a template from a proposal inserts `refOwnerID` as the session user and `blnShowGrandTotal` as 1 | Implemented, `includes/proposal.template.cls.php` |
| BR-081 | The allowed proposal statuses are the MySQL column type of `crmProposal.strStatus`, read at runtime | Implemented lookup. The enum values, other than `Draft` and `3c. Revised`, are **Unknown** |
| BR-082 | A commented migration makes `crmProposal.strStatus` a foreign key to `crmDepartmentStatus.strStatus` | Documented as a comment only. Live constraint **Unknown** |

## Departments, codes, and training

| ID | Rule | Provenance |
|---|---|---|
| BR-090 | Department status rows are filtered by process (`strProcess`) and department | Implemented, `includes/department.status.cls.php` |
| BR-091 | User department is `sysUser.refDepartmentID` referencing `crmDepartment`. A commented migration uses `ON DELETE CASCADE` | Documented in `includes/user.cls.php`. Live constraint **Unknown** |
| BR-092 | Fault and fix codes are maintained as their own screens. A 2018-02-01 comment says the fault table was renamed to `crmTaskFault` | Implemented screens. The rename is a comment |
| BR-093 | Training certificates, courses, and materials are separate records. Certificate upload uses ajax type `UploadTrainningCertificate` | Implemented |
| BR-094 | No pass-mark or expiry rule for a certificate was found | Unknown |

## Mail and audit

| ID | Rule | Provenance |
|---|---|---|
| BR-100 | Outbound mail uses PHP `mail()`. From and BCC default from settings `SMTP Send As` and `SMTP BCC` | Implemented, `_framework/_nemo.email.cls.php` |
| BR-101 | Sent messages are stored in `tblEmail`, optionally tied to `tblEmailTemplate` | Implemented, `includes/email.cls.php` |
| BR-102 | Template substitution uses the `NemoEmail::Substitute` map. A 2017-03-06 comment says substitutions and attachments use `;` as the delimiter | Documented in `includes/email.cls.php` |
| BR-103 | `NemoAudit::Audit` writes the field diff to `sysAuditLog` when a page constructs it | Implemented |
| BR-104 | `SendSMS` posts to MyMobileAPI using settings `smsUsername`, `smsPassword`, and `smsLive`. Nothing in this tree calls it | Implemented helper, unused |

## Home page figures

| ID | Rule | Provenance |
|---|---|---|
| BR-110 | The six home tiles do not aggregate tickets or invoices. Each query returns a constant formatted number and still touches `sysUser` where `UserID = 2` | Implemented, `Dashboard::TileStats` |
| BR-111 | The tile footer always says "4% From last Week" | Implemented |
| BR-112 | The to-do lists are fixed HTML checkboxes | Implemented |

## What this document does not claim

- It does not list security-group names or menu labels.
- It does not copy credentials from `system.php` or `system2.php`.
- It does not treat `convergence_production` behaviour (HR, email scry, under-construction) as rules of this repository.
