# Business rules — convergence_production (Convergence 2017)

Evidence date: 2026-10-18.

Source: `master` at `a91a555`. There is no business-rules document in the repository. Every rule below is taken from code or from a comment that describes the code. Rules inferred from structure are marked **Inferred**.

Live status labels, menu rights, and `sysSettings` values can change these rules without a code change. Those rows were not in git.

## Access

### BR-001 — Active user and active group

Login selects a user by email only when `sysSecurityGroup.blnActive = 1`. The user must also have `blnActive = 1`. Otherwise the attempt is logged and refused.

- Evidence: `login.php`.
- Provenance: **Implemented**.

### BR-002 — Password check

The password is not stored on `sysUser` for the check in `login.php`. `NemoPassword::checkPassword` compares a hash of salt plus the typed password in `sysPassword` for type `User`.

- Evidence: `_framework/_nemo.password.cls.php`.
- Provenance: **Implemented**.
- The class can also store type `Supplier`. No supplier login page was found. Provenance: **Inferred** that supplier passwords were prepared and not wired to a screen.

### BR-003 — Lockout

Five `sysLogin` rows of "Login failed: Incorrect password", with no "Login successful" inside that window of five, set the user inactive and email them. The limit is the literal `5` in `CheckSecurityLock`.

- Evidence: `_framework/system.functions.inc.php`.
- Provenance: **Implemented**.

### BR-004 — Session required on Nemo pages

If `$_SESSION[USER]` is missing, `Nemo` redirects to `login.php` and exits. The 2017-03-07 comment says guest users were turned off. A commented line that forced security group 1 remains in the constructor and is not executed.

- Evidence: `_framework/_nemo.cls.php`.
- Provenance: **Implemented**.

### BR-005 — Menu visibility

A level-2 item is shown when it is a menu item, it is active, the group has `blnView = 1`, and the parent level-1 row is active. Flags also include `blnSave`, `blnDelete`, `blnNew`, and `blnSpecial`.

- Evidence: `_framework/_nemo.menu.cls.php`.
- Provenance: **Implemented**.

### BR-006 — Direct URL view check is forced open

After loading rights, the current page’s `blnView` is set to `1`. The comment says this was added for testing (2017-01-10). The redirect to `message.php` for a denied page therefore does not run for a normal request.

- Evidence: `Nemo::iniSecurity`.
- Provenance: **Implemented**.
- **Discrepancy:** the menu hides items, and the page check does not. Requires confirmation whether the override is still intended.

### BR-007 — Maintenance mode clears the session

When `SYSTEM_UNDER_CONSTRUCTION` is `1`, the session is unset and every script except `under.construction.php` redirects there.

- Evidence: `system.php`.
- Provenance: **Implemented**.

### BR-008 — Client authorisation links are not staff-authenticated

`ticket.ops.php` uses `NemoBasic`. Approve and decline run from `Action`, `TicketID`, and `ContactID` on the request.

- Evidence: `ticket.ops.php`, `Ticket::AuthorizeClient`, `Ticket::DeclineClient`.
- Provenance: **Implemented**.

## Tickets and tasks

### BR-009 — Archived tickets stay out of the default list

Both ticket list queries add `crmTicket.blnArchive = 0` unless a filter path says otherwise. A comment warns that "include archived" must not be implemented as "only archived".

- Evidence: `includes/ticket.cls.php` `getList` and `getListUncompact`.
- Provenance: **Implemented**.

### BR-010 — Complete and archive

`Complete` sets `strStatus` to `Completed`. `Archive` sets `blnArchive` to `1`. Decline by the client sets `Closed`, archives the ticket, sets `dtCompletedDate` to today, and writes `txtNotes` `Authorization: DECLINED`.

- Evidence: `Ticket::Complete`, `Ticket::Archive`, `Ticket::DeclineClient`.
- Provenance: **Implemented**.

### BR-011 — Authorise

A successful client authorisation sets `strStatus` to `New: Authorized`, `dtApprovedDate` to now, and `strApprovedBy` to the contact name.

- Evidence: `Ticket::AuthorizeClient`.
- Provenance: **Implemented**.

### BR-012 — Client requires authorisation

If `crmClient.blnRequiresAuthorisation = 1` and the approval date is empty or `0000-00-00`, the screen shows "Ticket needs approval" in red. If an approval date exists, the same field is green. If the client does not require authorisation, the approval date is hidden.

While status is `New: Pending Authorization`, the status field is read-only.

The request-authorisation button is shown when the ticket exists, the approval date is empty, and the client requires authorisation (2018-01-15 comment, condition in `ticket.php`).

- Evidence: `ticket.php`.
- Provenance: **Implemented**.
- A line that would force status `New` when authorisation is not required is commented out.

### BR-013 — Merge

`mergeTickets` creates one new ticket with status `New`, copies client, site, contact, department, service board, owner, and SLA link from a source, and sets `blnArchive = 1` on the selected tickets. Tasks are moved onto the new ticket.

- Evidence: `Ticket::mergeTickets`.
- Provenance: **Implemented**.

### BR-014 — Ticket templates

Templates are tickets with `TicketID < 0`, status not `Closed`, and `blnArchive = 0`.

- Evidence: `Ticket::getTicketTemplateList`.
- Provenance: **Implemented**.

### BR-015 — Task mail skips archived tickets

The hourly `Task.Assigned` process does not email tasks whose ticket is archived (2018-03-08 comment).

- Evidence: `cron.hourly.php` header and the `Task.Assigned` case.
- Provenance: **Implemented** as a comment on the case. The SQL inside the case was not quoted here line by line.

### BR-016 — New ticket from an email about an archived ticket

If the mailbox subject contains a ticket id and that ticket has `blnArchive = 1`, Email Scry creates a new ticket and task instead of attaching the mail to the old ticket.

- Evidence: header of `cron.email.scry.php`.
- Provenance: **Documented in that header**. The class `MailFetcher` implements the import. Confidence: Medium.

### BR-017 — Unknown sender

If the From address is not a contact and the domain is not a client domain, Email Scry creates a client and a contact, then a ticket. If the address matches a contact, that client and contact are used and the owner is the client’s `refDefaultSupportUserID`. If only the domain matches, the client is set and the contact is left for the service delivery manager to add.

- Evidence: `cron.email.scry.php` header; `MailFetcher` comment "use Domain field in crmClient".
- Provenance: **Documented in that header**.

## SLA

### BR-018 — Only active, non-terminated SLA tickets spawn work

`cron.daily.tickettask.php` selects `slaTicket` where `blnActive = 1` and (`dtTerminationDate` is null or after today). A schedule row can carry weekly, monthly, and yearly patterns separated by semicolons.

- Evidence: `cron.daily.tickettask.php`, `includes/sla.schedule.cls.php`.
- Provenance: **Implemented**.

### BR-019 — SLA copy includes the client site

Comments on 2018-04-25 say `refClientSiteID` is copied when SLA tickets become operational tickets.

- Evidence: `cron.daily.php` task 4, `cron.daily.createticket.php`, `cron.daily.tickettask.php`.
- Provenance: **Implemented**.

## Proposals

### BR-020 — Revision and duplicate statuses

`NewRevision` sets the current proposal’s status to `3c. Revised` and copies it forward. `Duplicate` sets the copy’s status to `Draft`.

- Evidence: `includes/proposal.cls.php`.
- Provenance: **Implemented**.
- Other statuses are `crmDepartmentStatus` values. The allowed list is **Unknown** without the table.

### BR-021 — Tickets created from a proposal start as New

The temporary ticket insert uses status `New`. The temporary task insert uses priority `Medium` and status `New`.

- Evidence: `Proposal::createTmpTicket`.
- Provenance: **Implemented**.

### BR-022 — Empty client domain and empty default owner are NULL

Saving a client stores a blank domain as NULL and an unselected `refDefaultSupportUserID` as NULL, so the foreign key is not violated.

- Evidence: comments and save path in `includes/client.cls.php` (2018-03-08, 2018-04-12).
- Provenance: **Implemented**.

## Leave

### BR-023 — Leave statuses

The only statuses the leave class sets are `New`, `Submitted`, `Pending Approval`, `Approved`, `Declined`, and `Cancelled`.

- Evidence: `LeaveApplication` constants and `Submit`, `ApproveHR`, `ApproveMAN`, `Decline`, `Cancel`.
- Provenance: **Implemented**.

### BR-024 — Leave sequence

1. Save keeps the posted status (a new row stamps `dtApplicationDate`).
2. Submit sets `Submitted`. The on-screen note says the applicant cannot edit after submit.
3. HR approve sets `Pending Approval` and stores the notified line managers.
4. Manager approve sets `Approved`, the approver name, and the approval date.
5. Decline sets `Declined` and `txtDeclineNotes`.
6. Cancel sets `Cancelled`.

- Evidence: `includes/hr.leave.application.cls.php`; steps text in `getLeaveApplicationSteps`.
- Provenance: **Implemented**.
- Staff, once submitted, see Cancel and not Save. HR on `Submitted` sees Approve HR and Decline. A manager on `Pending Approval` sees Approve MAN and Decline. The full matrix is in the class constructor.

### BR-025 — Who is HR, who is a manager

On an existing application:

- The user is Staff when their id equals `refStaffID`.
- The user is Manager when their id equals line manager 1, 2, 3, or 4. A later match overwrites Staff.
- The user is HR when the page security row has `blnSpecial = 1`. That overwrites Manager.
- If both manager and HR match, the button set is `HR_Manager`, then the label is set back to HR.

On a new application, a user who is the line manager of at least one staff row (email match on `strLineManagerEmail1`) may create an application for those staff. The default staff id is the logged-in user.

- Evidence: `hr.leave.application.php`.
- Provenance: **Implemented**.

### BR-026 — Only security group 3 can delete leave

`Delete` sets `blnActive = 0`. The update runs only when `SECURITYGROUPID == 3`. The comment says "Restrict Delete to HR".

- Evidence: `LeaveApplication::Delete`.
- Provenance: **Implemented**.
- **Requires Confirmation:** that group id 3 is HR in the live `sysSecurityGroup` table.

### BR-027 — Working days

`calculateDays` counts Monday–Friday dates from start through end, skips dates that are an active public holiday’s `dtEffectiveDate` in that range, multiplies by `hrLeaveType.dblLeaveDays`, and rounds to one decimal. If the loop passes 90 days it returns `-1`.

- Evidence: `LeaveApplication::calculateDays`.
- Provenance: **Implemented**.
- Public holidays are matched on `dtEffectiveDate`, not on `dtPublicHoliday`. **Inferred:** `dtEffectiveDate` is the calendar day that is skipped. Requires confirmation if those two columns differ.

### BR-028 — Leave reminders

Daily task 5 reminds HR and managers about pending leave. Daily task 6 reminds staff to submit. The dates come from `sysSettings` keys `HR_LeaveApplication_FollowupDate_Reschedule` and `HR_LeaveApplication_Staff_Reminder`.

Hourly leave notification cases are commented out.

- Evidence: `cron.daily.php`, `cron.hourly.php`.
- Provenance: **Implemented**.

## Performance reviews

### BR-029 — Review parties

A review links staff, line manager, role, and 360 reviewers. Comments add reviewer slots through December 2019 (four reviewer foreign keys are described in `hr.performance.review.cls.php`).

- Evidence: `includes/hr.performance.review.cls.php`.
- Provenance: **Implemented** as foreign keys in comments and fields the class lists. The exact column count on the live table is **Unknown** (no schema dump).

### BR-030 — Wizard order

The wizard saves one step at a time: Hard KPAs and KPIs, Soft KPAs and KPIs, Job role objectives and goals, Personal development plan, Employee satisfaction. The manager must tick a confirmation before the final submit (2019-06-21 comment).

- Evidence: `includes/hr.survey.wizard.cls.php`.
- Provenance: **Implemented**.

### BR-031 — Review mail

Hourly cases notify the employee (`PR.INI.EE`), 360 reviewers (`PR.INI.360`), and the manager (`PR.INI.MAN`), and notify on manager submit (`PR.SubmitMAN`). Daily task 7 reminds HR. Daily task 8 reminds "R3" staff to submit.

- Evidence: `cron.hourly.php`, `cron.daily.php`.
- Provenance: **Implemented**.
- The meaning of "R3" is **Unknown / Requires Confirmation**.

## Follow-up scheduling

### BR-032 — Priority reschedule

Daily cron computes the next follow-up date from `sysSettings`:

| Priority | Setting |
|---|---|
| High | `CRONDAILY_ReschedulePriority_HIGH` |
| Medium | `CRONDAILY_ReschedulePriority_MEDIUM` |
| Low | `CRONDAILY_ReschedulePriority_LOW` |
| Leave | `HR_LeaveApplication_FollowupDate_Reschedule` |

If the computed Low date is today, it is cleared.

- Evidence: `cron.daily.php`.
- Provenance: **Implemented**. The `strtotime` strings stored in settings are **Unknown**.

### BR-033 — Zero delivery dates

A 2018-07-25 comment on `cron.daily.php` says zero delivery dates are trapped. Provenance: **Documented in that comment**. The trap itself was not extracted as a separate rule.

## Money and documents

### BR-034 — VAT lookup is off

The line that would set `SystemSettings["VAT"]` from `sysVAT` for the current date is commented out.

- Evidence: `system.php`.
- Provenance: **Implemented** as disabled.
- Proposal lists still show a "Cost (Ex Vat)" column from `dwfProposal.dblCostExcluding`. How VAT is applied on a PDF is **Unknown / Requires Confirmation**.

### BR-035 — Proposal numbers

`Proposal::GetNextProposalNumber` assigns the next number. The format is whatever that function returns.

- Evidence: `includes/proposal.cls.php`.
- Provenance: **Implemented**. The pattern is a function, not a documented code list.

## File and environment rules

### BR-036 — Live host rewrite

`www.convergence.overdrive.co.za` and a null server name become `convergence.overdrive.co.za` before the database is chosen.

- Evidence: `system.php`.
- Provenance: **Implemented**.
- **Inferred:** a cron with an empty server name uses the live database.

### BR-037 — `lamp` is the live database

From 2018-08-06 the `lamp` settings point at the hosted server and `convergence_2017`, not at localhost. The localhost block is commented out.

- Evidence: `system.php`.
- Provenance: **Implemented**.
- **Requires Confirmation:** whether any machine still uses the host name `lamp`.

### BR-038 — Emails are stored on the aux database

`NemoEmail` calls `switchBD` to the aux settings before writing `tblEmail` and `tblEmailTemplate`, then switches back.

- Evidence: `_framework/_nemo.email.cls.php`, `includes/email.cls.php`.
- Provenance: **Implemented**.

### BR-039 — Sessions last three days

`session.gc_maxlifetime` is 259200. The save path is the environment’s `sessionpath` (`SESSION/` under the document root on live, a `Convergence_2017/dev/SESSION/` path on `lamp`).

- Evidence: `system.php`.
- Provenance: **Implemented**.

### BR-040 — FIM exclusions

Paths matching fragments in `sysSettings.FIM_EXCLUDE_PATH_LIKE` (semicolon-separated) are excluded from the file scan. The class comment lists examples (`.git`, images, spreadsheets, PDFs) and then says to load the list from the database instead.

- Evidence: `_framework/_nemo.fim.cls.php`.
- Provenance: **Implemented**. The live exclusion list is **Unknown**.

## Rules that are not enforced in code

- Page-level view permission, because of BR-006.
- A fixed ticket-status machine. Department status rows decide the dropdown. Only the constants in the ticket and leave classes are fixed in PHP.
- Password maximum age. The default written for `PASSWORD_MaxAge` is `false`.
- The hourly leave emails. Those cases are comments.

## Requires confirmation

1. Security group id `3` is HR.
2. The `blnView = 1` override should remain.
3. Unauthenticated client authorisation links should remain.
4. `dtEffectiveDate` is the public-holiday day that leave calculations skip.
5. "R3" in the performance-review reminder.
6. Whether VAT is applied anywhere after the `sysVAT` lookup was commented out.
7. Whether Email Scry’s header still matches `MailFetcher` after later edits. The header is the rule statement. The class is the implementation. They were not diffed line by line.
