# PRD — convergence

Evidence date: 2026-10-18. Source: `git@bitbucket.org:overdrive/convergence.git`, `master` at `6b66438` (2021-03-15).

There is no prior PRD in the repository or in Silicon Ark for this repository. `silicon_ark/convergence_production/` documents a different repository. Every requirement below was recovered from this tree. Provenance is marked on each item.

## Product purpose

Staff run Silicon Overdrive operations from a browser: clients and suppliers, helpdesk tickets and tasks, time, SLAs, proposals, products, and training records.

Provenance: **Implemented**. A written product statement was not found. `README.md` says "Convergence's is an Silicon override project".

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Staff user | Signs in with email and password. Session stores user id, username, email, security group, and department | `login.php` | Implemented |
| Security group | Menu rows and the flags `blnView`, `blnNew`, `blnSave`, `blnDelete`, `blnSpecial` are loaded for the user's group | `_framework/_nemo.menu.cls.php`, `Nemo::iniSecurity` | Implemented |
| Inactive user | Login refused with "User account is inactive." | `login.php` | Implemented |
| User in an inactive group | Login query requires `sysSecurityGroup.blnActive = 1`. Failure message: "Login Details are invalid." | `login.php` | Implemented |
| Client contact | Receives a ticket authorisation email when `crmContact.blnCanAuthoriseJobs` is true, and can open `ticket.ops.php` | `Ticket::RequestAuthorization`, `ticket.ops.php` | Implemented |
| Anonymous caller of `ticket.ops.php` or `cron.daily.createticket.php` | Those pages construct `NemoBasic`, which does not redirect to `login.php` | `ticket.ops.php`, `cron.daily.createticket.php`, `_nemo.basic.cls.php` | Implemented |

Group names and which URLs each group may open are stored in MySQL. They are **Unknown** from this repository.

`Nemo::iniSecurity` sets `blnView = 1` for the current script before the denial check. The comment says this was added for testing on 2017-01-10. A user who is logged in is not redirected for lack of view rights. Provenance: **Implemented**. Whether that bypass is still accepted is **Requires Confirmation**.

## Main workflows

### W1 — Staff sign-in

Trigger: browser opens `login.php`. `Nemo` pages redirect to `login.php` when `$_SESSION[USER]` is unset.

Inputs: `strEmail`, `strPassword`. Client-side `Validate()` returns true without checking either field.

Behaviour (`login.php`, `NemoPassword::checkPassword`):

1. Load `sysUser` joined to `sysSecurityGroup` where `strEmail` matches and the group is active.
2. If the user row is missing, log "Login failed: User not found or SG not active".
3. If `sysUser.blnActive` is not 1, log "Login failed: Inactive User".
4. Compare `hash("sha256", salt + password)` with the latest `sysPassword` row for that user (`Type` = `User`).
5. On success, set session id, username, email, security group, and department, log "Login successful", and redirect to `index.php`.
6. On a bad password, log "Login failed: Incorrect password" and call `CheckSecurityLock`.

Output: session and redirect, or a warning message. Provenance: **Implemented**.

### W2 — Lockout

Trigger: a failed password check.

Behaviour (`CheckSecurityLock`): read the last 5 `sysLogin` rows for that user that are either "Login failed: Incorrect password" or "Login successful", stopping at a success. If five failures are counted, set `sysUser.blnActive = 0`, `strLastUser` = "Security Lockout", and send mail that the account is suspended.

Provenance: **Implemented**. The threshold 5 is a literal in the function, not a `sysSettings` value.

### W3 — Password reset

Trigger: "Lost your password?" posts `type=SendResetEmail` to `ajaxFunctions.php`.

Behaviour: if the email is not in the database the response is `0`. If it is, a reset mail is sent and the response is `1`. `resetPassword.php` uses `NemoPassword`. The reset link lifetime is the setting `PASSWORD_Reset_Interval` (bootstrap value `60 MINUTE`).

Strength, when `PASSWORD_Strength_Check` is true: minimum length from `PASSWORD_Strength_Check_MinLength` (bootstrap `8`), at least one lower-case letter, one upper-case letter, and one digit. Restricted words come from `PASSWORD_Restricted_Words` (bootstrap `password|123`). History limit bootstrap is 8. Max age bootstrap is the string `false`, and `checkExpiry` treats that as "not expired".

Provenance: **Implemented** in `_framework/_nemo.password.cls.php` and `login.php`. The first run creates `sysPassword` and the settings rows, then `die`s with "PASSWORD SYSTEM INITIATED. REFRESH TO CONTINUE".

### W4 — Home

Trigger: `index.php` after login.

Behaviour:

1. `Dashboard::TileStats()` prints six tiles. Each SQL is `SELECT FORMAT(<literal>,0)` or `SELECT CONCAT('R ', FORMAT(<literal>,0))` from `sysUser` where `UserID = 2`. The headings are outstanding SLA tickets, outstanding ad hoc tickets, and billing for sales, software, technical, and company. The footer text is always "4% From last Week".
2. `Dashboard::RenderProjectWBS()` embeds a Lucidchart chart titled "Convergence WBS 2018".
3. `Dashboard::TodoTickets()` prints two static lists (AdHoc and SLA) of unchecked sample lines.
4. `Dashboard::GrowthChart()` exists and is commented out in `index.php`.

Provenance: **Implemented**. These are not live operational totals.

### W5 — Clients, contacts, suppliers

Trigger: `client.php`, `contact.php`, `supplier.php`.

Behaviour:

- Client list search and filters. The client list adds `crmClient.strClientType != 'Supplier'` unless a type filter is set (`includes/client.cls.php`).
- `supplier.php` states that a supplier is a view on `crmClient` where the client type is supplier. `includes/supplier.cls.php` extends `Client`.
- Contacts belong to a client (`crmContact.refClientID`). A contact can be flagged `blnCanAuthoriseJobs`.
- Deactivate sets `crmClient.blnActive = 0`. It does not delete the row (`Client` list delete path).

Provenance: **Implemented**. The SQL definition of `vieClient` is not in the repository.

### W6 — Ticket

Trigger: `ticket.php`.

Inputs include owner, department, client, contact, description, priority, cost budget, and status.

Behaviour (`ticket.php`, `includes/ticket.cls.php`):

1. Status choices come from `DepartmentStatus::getDepartmentStatusSQL("Ticket", $DepartmentID)`. The allowed labels are rows in `crmDepartmentStatus`, not a fixed list in PHP.
2. If the selected client's `blnRequiresAuthorisation` is 1, the status value is set to `Pending Authorization`. Otherwise it is set to `New`.
3. While status is `Pending Authorization` the status control is read-only.
4. While the ticket is unsaved-as-approved (`dtApprovedDate` empty or `0000-00-00`) and the client requires authorisation, a request-authorisation button is shown.
5. `RequestAuthorization` emails every active contact of that client with `blnCanAuthoriseJobs` true, using the template name "Ticket Request Authorization". The mail subject is `SIL: Ticket Authorization Request [#id]`. The authorise and decline links are the relative paths `lamp/Convergence_2017/dev/ticket.ops.php?Action=Approved|Declined&TicketID=&ContactID=`.
6. `ticket.ops.php` calls `Ticket::AuthorizeClient` or `Ticket::DeclineClient`.
7. Authorise, when approval date and completed date are empty and `blnArchive` is not 1: set status `New Authorized`, `dtApprovedDate` to now, `strApprovedBy` to the contact's name.
8. Decline, under the same preconditions: set status `Declined`, `blnArchive` = 1, `dtCompletedDate` to now, and the same approver name.
9. A second click returns a message that the ticket was already authorised or declined.

Ticket list filters default the department to the session department. Client dropdowns use `vieClient`.

Provenance: **Implemented**. Whether the `lamp/Convergence_2017/dev/` links were rewritten in production mail is **Unknown / Requires Confirmation**.

### W7 — Tasks and time

Trigger: `task.php`, `time.php`, `schedule.php`.

Behaviour:

- A task hangs off a ticket (`crmTask.refTicketID`) and may point at a product, a proposal item, or an SLA task.
- Time hangs off a task (`crmTime.refTaskID`). The time list defaults the user filter to the logged-in user and the date range to the current calendar month (`includes/time.cls.php`).
- `time.php` can start from a task or let the user pick client, then ticket, then task. Client choices are active `vieClient` rows, plus the current client if it is inactive.
- The schedule page is a FullCalendar screen. Ajax types `ajaxAddScheduleTimeTask`, `ajaxUpdateScheduleTimeTask`, `ajaxChangeScheduleTimeTask`, `ajaxDeleteScheduleTimeTask`, and `ajaxGetScheduleTimes` are in `ajaxFunctions.php`. The comment on `schedule.php` says the calendar was adapted from a public FullCalendar CRUD example.

Provenance: **Implemented**. Billing-rate rules live in `vieBillingRate`, whose definition is not in the repository.

### W8 — SLA recurrence

Trigger: `cron.daily.createticket.php` (a browser-reachable PHP page; no crontab is in the repo).

Behaviour:

1. Compute today's weekday name, a week-of-month label (`First Week` through `Fourth Week`; week 5 uses `Fourth Week`), and a three-letter month.
2. Select `slaSchedule` rows that are active, whose `slaTicket.dtTerminationDate` is null or after today, and whose weekly, monthly, and yearly text columns match today. Empty monthly and yearly columns are wildcards. An empty weekly column does not match, because every branch requires `strScheduleWeekly` to contain the day.
3. Insert a `crmTicket` from the SLA ticket with status `New`, ticket date and due date today, and `blnActive` 1.
4. Read `MAX(TicketID)` and insert `crmTask` rows from `slaTask` for that SLA, status `New`, due today, `blnCompleted` 0.
5. Print "Completed creating tickets".

The audit-based insert below that query is commented out. Provenance: **Implemented**. Overlap if two runs insert at once is **Inferred** from the `MAX(TicketID)` lookup.

### W9 — Proposal

Trigger: `proposal.php`.

Behaviour (`includes/proposal.cls.php`):

1. A proposal has groups (`crmProposalGroup`) and items (`crmProposalItem`). Items can be added, deleted, and reordered through `ajaxFunctions.php`.
2. PDF output uses mPDF, A4. The total block (exc. VAT, VAT, incl. VAT) is written on every PDF in `includes/proposal.cls.php`. A 2017-03-17 comment says the grand total is hidden when `blnShowGrandTotal` is 1 and the output is a PDF. That function does not read the flag. The flag is stored on the proposal and shown on the Info tab.
3. `NewRevision` copies the proposal, sets the new number to `Q` plus the quote number padded to 7 digits, a dot, and the next `intRevision`, sets the previous row's `strStatus` to `3c. Revised`, and the duplicate path sets `strStatus` to `Draft`.
4. Supplier price-request mail uses subject "Price request from Silicon Overdrive". Supplier order mail uses subject `Supplier Order [SupplierOrderNumber]: Silicon`.
5. `getQuoteStatusArray` reads the MySQL enum on `crmProposal.strStatus` from `information_schema`. The full enum is not in the PHP file.
6. `crmProposal.strStatus` is also described, in a commented `ALTER`, as a foreign key to `crmDepartmentStatus.strStatus`. Which constraint is installed in the live database is **Unknown**.

Proposal templates (`proposal.template.php`) can be created from a proposal and the reverse. The insert that creates a template from a proposal sets `blnShowGrandTotal` to 1 and `refOwnerID` to the session user (`includes/proposal.template.cls.php`).

Provenance: **Implemented**, except the live status enum and the foreign key, which are **Unknown**.

### W10 — Products and bundles

Trigger: `product.php`, `product.bundle.php`.

Behaviour: products have a cost. Bundle items can be added, removed, and reordered (`ajaxAddProductBundleItem` and related cases). Ad hoc products make cost and margin editable (`product.bundle.php` comments, 2017-09-12). `UpdateProposalItemsCostFromProduct` copies `crmProduct.dblCost` onto selected proposal items.

`cleanProductFields` strips characters outside a fixed ASCII set before save (`project.functions.inc.php`).

Provenance: **Implemented**.

### W11 — Training

Trigger: `edu.training.course.php`, `edu.training.material.php`, `edu.training.certificate.php`.

Behaviour: courses, materials, material bookings, and certificates are separate screens. Certificate upload ajax type is `UploadTrainningCertificate` (spelling as in the source). Files are associated with `edulab/training_certificates/`.

Provenance: **Implemented**. Course completion rules are **Unknown**; no scoring logic was found in the page headers.

### W12 — Paint Your Day Green

Trigger: `pydg.report.php`, class `PYDGReport` (2017-09-02).

Behaviour: a filtered report. Default department is the session department. User filter lists active users in active departments. A second class file, `includes/pydg.report.cls.alt.dont.delete.php`, is present and not referenced by the page.

Provenance: **Implemented** as a report screen. What "green" measures, beyond the report title, is **Requires Confirmation** against the SQL in that class and the live data.

## Integrations

| System | Direction | Trigger | Data | Failure | Provenance |
|---|---|---|---|---|---|
| MySQL | App to database | Every page via `system2.php` | CRM, system, email tables | Connection error is printed. `display_errors` is on | Implemented |
| PHP `mail()` | App to mail server | Password reset, lockout, ticket authorisation, proposal and supplier mail | Subject, body, From / BCC from `sysSettings` | `mail()` return value is stored; retry policy was not found | Implemented |
| MyMobileAPI | App to `http://www.mymobileapi.com/api5/http5.aspx` | `SendSMS` | Settings `smsUsername`, `smsPassword`, `smsLive`, number, message | `fopen` failure prints the error and stops | Implemented helper. No caller in this tree |
| Lucidchart | Browser to Lucidchart | Home page iframe | Embedded chart id in `Dashboard::RenderProjectWBS` | If Lucidchart is down, the home page still renders the frame | Implemented |
| mPDF | Local library | Proposal PDF | HTML proposal | Library errors surface as PHP errors | Implemented |

Authentication to MySQL is a username and password in `system2.php`, chosen by HTTP host. Values are omitted here.

## Important constraints

- Pages that construct `Nemo` require a session. Pages that construct only `NemoBasic` do not.
- Session garbage-collection lifetime is set to 259200 seconds (3 days) in `system2.php`.
- Passwords are `sha256(salt + password)` with a 32-byte hex salt. The clear password is not written to `sysLogin` (comment 2016-09-30).
- Unique-key collisions on framework save return "Unique key Violation, i.e. a similar record already exists."
- Client-facing authorisation links in this source point at a `lamp` dev path.
- `DATABASE_SETTINGS_AUX` is unused. Email screens query `tblEmail` on the primary connection.

## Requirements discovered from the implementation

| ID | Requirement | Provenance |
|---|---|---|
| FR-001 | A staff user signs in with email and password and lands on `index.php` | Implemented |
| FR-002 | Five consecutive bad passwords deactivate the user and send a warning mail | Implemented |
| FR-003 | A logged-in user sees menu items their security group is allowed to see | Implemented. The view gate is then forced on |
| FR-004 | Staff maintain clients, contacts, and suppliers | Implemented |
| FR-005 | A ticket for a client that requires authorisation starts as Pending Authorization and can be mailed to contacts who may authorise jobs | Implemented |
| FR-006 | Those contacts can set the ticket to New Authorized or Declined | Implemented |
| FR-007 | Staff record tasks and time against tickets | Implemented |
| FR-008 | A daily page copies active SLA schedules that match today into tickets and tasks | Implemented |
| FR-009 | Staff build proposals, revise them, email them, and render a PDF | Implemented |
| FR-010 | Staff keep products, bundles, training records, departments, and reference codes | Implemented |
| FR-011 | Saves that use `NemoAudit` write `sysAuditLog` | Implemented where the page constructs `NemoAudit` |

## Non-requirements in this repository

HR leave, performance reviews, inbound email ticket creation, file-integrity monitoring, and an under-construction switch are not in this tree. They are documented under `convergence_production/` for a different repository.
