# PRD — qoutes (Silicon Overdrive Quotes)

Evidence date: 2026-10-05.

Source: `git@bitbucket.org:overdrive/qoutes.git`, `master` at `0eb2f86` (2019-11-19, "First Commit").

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the implementation, from foreign-key notes, and from `Sales Workflow.xlsx`. Provenance is labelled. Database passwords in `system.php` are not reproduced.

## Product purpose

**Implemented.** Staff at Silicon Overdrive build priced quotes for clients, send them as PDF email, track status through invoicing and delivery, and receive operational email when status or QuickBooks data changes.

**Documented** in `Sales Workflow.xlsx`: a quote arrives "via Website, sales or Quotes", is allocated, built and sent, then moves through rejection, revision, invoicing, QuickBooks import, payment, supplier order, stock, and delivery.

**Unknown / Requires Confirmation:** whether a public website still creates quotes. No public quote-intake form was found in this repository. Intake in code is a logged-in user on `quote.php` or a CSP flow that generates a quote.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Staff user | `login.php` checks `sysUser.strEmail` and `strPasswordMD5` against MD5 of the typed password. The user and security group must be active | **Implemented** |
| Security group | `sysSecurity` flags `blnView`, `blnDelete`, `blnSave`, `blnNew`, `blnSpecial` per menu URL. `Nemo` blocks the page when `blnView` is not 1 | **Implemented**. Group names are in the database and were not in git |
| Sales person (comment: group id 3) | `report.php` defaults the sales-person filter when `SECURITYGROUPID == 3` by matching `tblSalesPerson.strEmail` to the user email | **Inferred** from that comment. Conflicts with the next row |
| Admin (comment: group id 3) | `quote.php` action `MarkQuoteAsInvoiced` sets a dummy session with `SECURITYGROUPID = 3` and comment "admin" | **Implemented** dummy session. Comment conflicts with `report.php` |
| Dev admin or admin (comment: groups 1 and 2) | `csp.dashboard.php` shows export and "Send For Invoicing" only for those ids | **Implemented** gate. Names are comments |
| Cron job | `cron.daily.php` and `cron.daily12.php` create `$_SESSION[USER]` with id `-4`, username "Cronjob", group 1, when no session exists | **Implemented** |
| Accounts dummy | `MarkQuoteAsInvoiced` sets username "Accounts" and email `admin@overdrive.co.za` before `Nemo` session check | **Implemented** |
| Client | Receives quote email and follow-up. No client login was found | **Implemented** as an email recipient |
| Developer reading `info.php` | `info.php` is `phpinfo()` | **Implemented**. No auth on that file was found |

`my.profile.php` lets the logged-in user edit their own `sysUser` row. The display name and security group are rendered read-only.

## Systems the product talks to

1. The staff browser.
2. MySQL for quotes (`quoteslive` live, `eit2` on the `lamp` profile).
3. MySQL for eIT clients (`eitlivedb` on the live second connection). Used by `ajaxfunctions.php` types `loadEitClientDDL` and `ImportEitClient`.
4. MySQL for QuickBooks web services (`invoice_table`). Hourly cron reads it and can set quote status.
5. PHP `mail()` via `NemoEmail`.
6. A web service at setting `WebSeviceURL`. Status `3a. To be Invoiced` injects a browser AJAX POST to `{WebSeviceURL}/handler.php` with quote number, PO number, and quote id. Status `6. Credited` has a similar call to `credited.php` that is commented out.
7. PDF files stored under the documents directory from settings (`DocumentsDir`).
8. Google is not referenced. Highcharts and jQuery are vendored locally.

## Major workflows

### WF-1 — Log in

`login.php` destroys any existing session, then on `Action=Login`:

1. Loads `sysUser` joined to `sysSecurityGroup` where email matches and the group is active.
2. If no row: message "Login Details are invalid." and `tblLoginInsert("Login failed: User not found or SG not active")`.
3. If `blnActive` is not 1: "User account is inactive."
4. If MD5 of the password does not match `strPasswordMD5`: "Username or Password is incorrect." plus `CheckSecurityLock`.
5. On success: session fields `ID`, `USERNAME`, `EMAIL`, `SECURITYGROUPID`, log "Login successful", redirect to `index.php`.

`Action=Redirect` looks up a user by email and `strPasswordMD5` equal to the posted hash, copies `strPassword` onto the request, and then runs the login path. Who calls `Redirect` is **Unknown**.

Client-side `Validate()` always returns true.

Provenance: **Implemented**.

### WF-2 — Open the home dashboard

`index.php` replaces the page with a `Quote` list (`getList`) and adds two Highcharts bar charts:

- "Status of quotes per sales person" for all quotes.
- A second chart limited to quotes whose `dtQuoteDate` falls in the current calendar month (same file, later in the script).

A usage-log widget and a booking widget are commented out.

Provenance: **Implemented**.

### WF-3 — Create and edit a quote

`quote.php` assimilates `tblQuote`. A new quote (`QuoteID` empty):

- Status is forced to `1. New` and the status control is disabled.
- Revision is 1 and read-only.
- On save, `Quote::Save` writes status `1. New`, then sets `intQuoteNumber` to `MAX(intQuoteNumber)+1` and `strQuoteNumber` to `Q` plus that number zero-padded to 7 digits plus `.1`.

An existing quote can add lines, delete lines, save, duplicate, create a new revision, pull a template onto the quote, update product cost from the line or line cost from the product, update the line description from the product, request a supplier price (`Get Price`), and order from a supplier.

Silicon Notes (`txtInstructions`) are required by client-side validation. Client Notes are a separate field.

Changing client runs `jsGetClientDetails`. The form can also open an eIT client picker (`jsShowEitClientDiv`).

Provenance: **Implemented**.

### WF-4 — Price a line

On save, each posted line stores units, cost, markup, ETA, grouping heading, grouping order, description, product, and supplier. Price is cost plus cost times markup, unless `blnExcludeMargin` is 1, in which case price equals cost. Markup posted as a percent is divided by 100.

Quote totals are the sum of price times units. Inclusive price is exclusive price times `(1 + SystemSettings[VAT])`, unless posted currency is `$`, in which case inclusive equals exclusive.

New lines from "Add Items" copy the client's `percMarkup` and set units to 1.

Quote bundles document a default markup constant `0.13` and a comment that markup was removed on bundles at Theo's request (2015-07-10).

Provenance: **Implemented**. The numeric VAT rate is a database setting and was not in git.

### WF-5 — Revise a quote

`NewRevision` reads the current quote, takes `MAX(intRevision)` for that `intQuoteNumber`, adds 1, and sets `strQuoteNumber` to `Q` + padded number + `.` + new revision. The controller saves, generates a sectional PDF, creates the revision, and generates a PDF again. The revision dropdown lists each revision and its inclusive total.

`Sales Workflow.xlsx` says the user must set status to `2. Sent` once the revision is finished and sent. The code path that creates the revision does not itself set `2. Sent`.

Provenance: number format **Implemented**. The "must set Sent" step is **Documented** in the spreadsheet and is a manual status change in the UI.

### WF-6 — Email the quote

Toolbar "Email Quote" opens a choice: Email Client, Email Yourself, and a checkbox "Hide PDF total".

Both paths save, generate a sectional PDF (with or without totals), and call `EmailQuoteTo`.

- Client: to the client email, BCC from setting `EmailClientBCC`, CC from the quote `strEmailCC`.
- User: to the logged-in user's email, no CC/BCC.

Template name is "Client Quote". Subject is `{client} - {description} - Pricing: Silicon - {quote number}`. Payment-terms text switches on `strPaymentTerms`: `Month End` versus anything else, which is worded as upfront. Attachment is `documents/{strFilename}`.

If `mail()` succeeds, status is set to `2. Sent`.

Provenance: **Implemented**.

### WF-7 — Change status

The status dropdown is the MySQL enum on `tblQuote.strStatus` (the code explodes `COLUMN_TYPE`). A change asks for confirmation, then posts `Action=ChangeStatus`.

If the new status is `3a. To be Invoiced` and PO number is empty, the browser blocks the change. The same rule is in `jsValidate` on `strPONumber`.

`ChangeStatus` stores the previous status and previous status date, then sets the new status. Side effects:

| New status | Side effect |
|---|---|
| `3a. To be Invoiced` | Email template "Quote To Be Invoiced" to `EmailToBeInvoiceQuotesRecipient`. Browser AJAX to `{WebSeviceURL}/handler.php` |
| `3b. Rejected` | Email "Quote Rejected" |
| `3d. Pending Approval` | Email "Quote Pending Approval" |
| `8. Stock Arrived` | Email "Quote Arrived" and "Quote Arrived HelpDesk" |
| `9. Delivered` | Email "Quote Delivered" |
| `4. Invoiced` | No email inside `ChangeStatus`. Save sends "Quote Invoiced" when status is invoiced, an invoice number is present, `blnInvoiced` is not already 1, and previous status was not invoiced |
| `6. Credited` | Message says the quote was removed from the web-service queue. The AJAX to `credited.php` is commented out |

`Archive` on the list sets `3b. Rejected` with a direct `UPDATE` and does not call `ChangeStatus`, so it does not send the rejected email.

Provenance: **Implemented**.

Statuses named in code: `0. Error`, `1. New`, `2. Sent`, `3a. To be Invoiced`, `3b. Rejected`, `3c. Revised`, `3d. Pending Approval`, `4. Invoiced`, `5. Paid`, `6. Credited`, `7. Ordered`, `8. Stock Arrived`, `9. Delivered`.

The enum is read from the database, so production may contain more values. **Unknown / Requires Confirmation** if the live enum matches this list.

### WF-8 — QuickBooks feedback

`cron.hourly.php` joins `invoice_table` to quotes where `blnEmail = 0`, builds an HTML table, and emails it. The recipient in the active code is hardcoded `gareth@overdrive.co.za`. The setting `Email QBWS Recipients` is read into a variable and not used for the send. On success it sets `blnEmail = 1` on those invoice rows.

The same script then sets quote status from the invoice row:

- QuickBooks error message present: `0. Error`.
- Invoice number present and `paid == 1`: `5. Paid`.
- Invoice number present and not paid: `4. Invoiced`.

This update writes status directly. It does not call `ChangeStatus`.

A third step sets `blnInvoiced = 1` where it is 0 and `strInvoiceNumber` is not empty. The invoiced email call is commented out.

`Sales Workflow.xlsx` says QuickBooks import returns invoice numbers and that paid allocation can be wrong if an order is rushed. That warning is **Documented** in the spreadsheet. The code does implement paid-from-QuickBooks.

### WF-9 — Supplier order number

On save, if invoice number and client are set, `strSupplierOrderNumber` becomes `{invoice number}_{first word of client name}`. Otherwise it is cleared. The field is read-only on the form, with placeholder "(Automatically generated when invoiced)".

Provenance: **Implemented**.

### WF-10 — Follow-up email

`cron.monthly27.php` calls `emailQuoteFollowup(null, 1)`. Cron mode selects quotes in status `2. Sent` whose quote date is more than 7 days before today, grouped by quote number, and emails each client with template "Client Quote Followup", from and BCC `ops@overdrive.co.za`, attaching each quote PDF.

The quote list can also post "Email Client Followup" for selected rows. That path does not apply the 7-day or status filter unless no ids are posted, in which case the query is `AND 1=0`. If the cron query finds nothing, the function returns text that tells the operator to check status `2. Sent`.

Provenance: **Implemented**.

### WF-11 — Reports

`report.php` offers:

- ETA Report (`rpt.eta`): lines on quotes in `3a`, `4`, `5`, `7`, `8`, plus delivered quotes in a separate date condition.
- Quote Status Report (`rpt.quoteStatus`): used by the daily-12 cron with statuses `1. New`, `5. Paid`, `0. Error` whose last status date is at least one day old.
- Profit Report (`rpt.profit`): quotes in `4. Invoiced`, `5. Paid`, `7. Ordered`, `8. Stock Arrived`, `9. Delivered`. Labour products (`blnLabour = 1`) are excluded from dealer cost and counted as labour. Profit is quoted subtotal minus dealer cost minus labour.

Default report when none is posted: ETA. Export sends the HTML table as an `.xls` download.

Group 3 users get their sales person pre-selected when the email matches `tblSalesPerson`.

Provenance: **Implemented**.

### WF-12 — CSP pipeline

CSP clients have their own statuses in the dashboard filter: `1. New`, `2. Pre screened`, `3a. Callback`, `3b. Meeting`, `3c. Sent`, `4. To be Invoiced`, `5. Sent to QB`, `6. Invoiced`, `7. Not interested`. Tiers are `Tier 1`, `Tier 2`, `Tier 3`.

"Send For Invoicing" (groups 1 and 2) selects `cspClient` rows in `4. To be Invoiced`. For each, it creates a quote client and a quote if missing, generates a sectional PDF, calls `Quote::ChangeStatus` to `3a. To be Invoiced` with PO number `Office 365`, and sets the CSP client to `5. Sent to QB`.

Provenance: **Implemented**.

### WF-13 — Catalogue and parties

Standard Nemo list/detail pages support New, Edit, Save, Delete, Export for category, supplier, sales person, user, security group, service 360, service 360 grouping, and CSP FAQ.

Products add Archive, which sets `blnActive = 0`. Product import uploads a CSV. The sample `productimport.csv` header is `SKU,Product,Supplier,Category,Warranty,Extended Warranty,Cost,Description,Notes`.

Clients support New Quote, which opens `quote.php` with that client. Clients can be chosen from eIT.

Quote templates can add items, create a quote, and push a quote back to a template (`Quote To Template`).

Provenance: **Implemented**.

### WF-14 — Service 360 report

`client.service.360.php` loads `tblService360` ordered by `intReportOrder` and can save the client record or generate a PDF.

Provenance: **Implemented**. The business meaning of each service row is data, not code.

## Integrations

| Integration | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| Quotes MySQL | App reads and writes | Every page | Quotes, clients, products, security, settings, email log | Username and password in `system.php` | Connection failure sets a critical system message |
| eIT MySQL | App reads; import writes a local client | AJAX `loadEitClientDDL`, `ImportEitClient` | eIT client fields | Second connection in `system.php` | **Unknown** beyond the function return text |
| QuickBooks `invoice_table` | Cron reads; cron updates `blnEmail` and quote status | `cron.hourly.php` | Error text, paid flag, invoice number, quote id | QB connection in `system.php` | Email branch reports "Email send failed." Status updates have no extra error handler in the loop |
| `{WebSeviceURL}/handler.php` | Browser POST | Status becomes `3a. To be Invoiced` | Quote number, PO number, quote id | **Unknown**. The URL is a setting | The success handler only `alert`s the response |
| PHP `mail()` | Outbound | Quote email, status email, crons, lockout warning | HTML body, PDF attachment | Server mailer. From address is the user email or a setting or a hardcoded ops/gareth address | Functions return "SENDING FAILED" or print the result. Several cron paths echo the HTML instead |
| Documents directory | App writes PDF; browser opens the file link | Create PDF, email, new revision | PDF named from the quote | Web server file permissions | If `strFilename` is empty the form says to press Create PDF |

## Important constraints

- HTTPS is forced by `.htaccess` (`RewriteCond %{HTTPS} off`).
- `robots.txt` disallows all crawlers.
- Pages other than login require a session, except `info.php` (no Nemo include) and `quote.php` action `MarkQuoteAsInvoiced`, which fabricates a session before `new Nemo()`.
- View permission is required per script name. `windowLocation` prints a JavaScript redirect and does not stop PHP. After the redirect is printed, `$_SESSION[intAttempt]` increments. When that counter is already greater than 5, the message id is `e10` instead of `r09`. `message.php` has no `e10` case, so that id produces an empty message body unless `$M` is also set.
- Quote number format `Q` + 7 digits + `.` + revision is **Implemented**.
- PO number is required only for status `3a. To be Invoiced`.
- Currency `$` suppresses the VAT-inclusive calculation.
- Deleting a quote is a SQL `DELETE`. Foreign-key notes in `relationships.rtf` use `ON DELETE CASCADE` from quote to items and from client to quote. If those constraints are installed, deleting a client deletes quotes. Whether production has those constraints is **Unknown** (they are scripts in comments and an RTF file, not a migration runner).

## Requirements discovered from the implementation

### FR-001 — Staff login

The system authenticates an active user in an active security group by email and MD5 password, stores the user on the session, and records the attempt.

Evidence: `login.php`, `tblLoginInsert` in `system.functions.inc.php`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Page authorisation

After login, each script is allowed only when `sysSecurity.blnView` is 1 for that URL and the user's group. Otherwise the user is sent to `message.php` with `MID=r09`.

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

Implementation: Implemented.

Confidence: High.

### FR-003 — Quote identity

A new quote receives the next integer quote number and display number `Q#######.1`. A new revision keeps the integer and increments the suffix.

Evidence: `GetNextQuoteNumber`, `Save`, `NewRevision`.

Implementation: Implemented.

Confidence: High.

### FR-004 — Quote document

The user can generate a sectional PDF, optionally without totals, and open it from the filename field.

Evidence: `quote.php` actions `Create PDF`, `Email Client`, `Email Yourself`; `GenerateSectionalPDF`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Status side effects

Moving to the statuses listed in WF-7 sends the matching email and, for "To be Invoiced", calls the web service from the browser.

Evidence: `Quote::ChangeStatus`, `Quote::EmailQuote`, `quote.php` status script.

Implementation: Implemented.

Confidence: High.

### FR-006 — QuickBooks status mirror

An hourly script copies invoiced, paid, and error state from `invoice_table` onto the quote and emails a digest of rows not yet emailed.

Evidence: `cron.hourly.php`.

Implementation: Implemented. Live database names in the SQL may not match `system.php`. Confidence on production effect: Medium.

### FR-007 — Operational reports and digests

The system can show or email ETA, stale quote status, and profit.

Evidence: `report.php`, `cron.daily.php`, `cron.daily12.php`, `includes/rpt.*.php`.

Implementation: Implemented.

Confidence: High for the scripts. The clock schedule is Unknown.

### FR-008 — CSP to quote

An admin can turn CSP clients that are "To be Invoiced" into quotes at status `3a. To be Invoiced` with PO `Office 365`.

Evidence: `CSPClient::sendForInvoicing`.

Implementation: Implemented.

Confidence: High.

## Assumptions (not requirements)

- That security group 3 is the sales role. Comments disagree.
- That `cron.daily12.php` runs at 12:00 and `cron.monthly27.php` runs on the 27th. Only the file names suggest that.
- That `Sales Workflow.xlsx` is the approved process. Several of its emails and the 16:00 ETA step are not the same as the cron code.
- That cascade foreign keys in `relationships.rtf` are applied in production.
