# Product requirements — pernod_ricard

Evidence date: 2026-10-07.

Source: `git@bitbucket.org:overdrive/pernod_ricard.git` at `24c1e12` (2020-11-18) on `master`.

Requirements below are recovered from the implementation. Each one is labelled:

- **Documented** — stated in a committed comment or on-screen copy.
- **Implemented** — present in code that runs for a user action or the daily script.
- **Inferred** — a reasonable reading of that behaviour, not written down as a requirement.
- **Unknown** — not established.

There is no requirements document in the source repository.

## Product purpose

Suppliers of Pernod Ricard South Africa submit invoices and supporting documents through a portal. Accounts-payable staff review those invoices, record payment, import a JDE payment file, and track due and overdue invoices. The public site root sends the browser to the supplier application.

Provenance: **Inferred** from the two applications, the invoice status values, and the payment-import code. The on-screen name "Welcome to the PRSA Supplier Portal" and the production host are **Implemented** (`supplier/index.php`, `index.php`).

## Users and roles

| Actor | How they sign in | What the code allows |
|---|---|---|
| Supplier | Email and password on `supplier/login.php`. The user and their security group must be active. Password is checked with `NemoPassword` for type `Supplier` | Own invoices when `blnSpecial` is 0, profile, terms and password gate |
| Webadmin user | Email and password on `webadmin/login.php`. The user and their security group must be active. Password is checked for type `User` | Screens granted by `sysSecurity` for that group: view, save, delete, new, and a special flag |
| Supplier who opens webadmin | Same email lookup. Security group id `10` is refused after a successful password check | Redirect to `../supplier/index.php` with "Supplier Access Restricted: Log in through the supplier portal." |
| Cron | `webadmin/cron.daily.php` sets the session user to id `-1`, name `CRON`, security group `1` | Due dates, status changes, JDE payment import, emails |

Menu entries and the flags `blnView`, `blnSave`, `blnDelete`, `blnNew`, and `blnSpecial` come from `sysSecurity` joined to the user's security group (`webadmin/_framework/_nemo.cls.php`). The names of groups other than the id `10` check are **Unknown**. They live in `sysSecurityGroup` and were not in the repository.

Hardcoded group checks on the invoice screen, from comments in `webadmin/invoice.php`:

- Security group id `<= 3`: show "Pay Invoice" / "Pay Invoices". Comment: "only security groups AP Manager and up".
- Security group id `<= 2`: show "Undo Payment" when status is `Paid`. Comment: "Admin (and up) only".

Provenance: **Implemented** as numeric comparisons. The names "AP Manager" and "Admin" are **Documented** only in those comments. The actual group names in the database are **Unknown / Requires Confirmation**.

Supplier login does not test for group id `10`. Any active user in an active group can open the supplier portal if the supplier password check passes. Provenance: **Implemented** (`supplier/login.php`).

When `blnSpecial` is `0`, invoice, supplier, remittance, and report screens limit rows with `whereDepartmentHR()` / `checkDepartmentHR()`. A supplier invoice whose company is not the signed-in company redirects to `message.php?MID=r11`. Provenance: **Implemented**. What `blnSpecial` means to the business is **Inferred** as "not restricted to a department or to the user's company".

`Nemo` redirects to `login.php` when `$_SESSION[USER]` (webadmin) or `$_SESSION[SUPPLIER]` (supplier) is missing. A page that does not construct `Nemo` is not covered by that check. `cron.daily.php` constructs `Nemo` after it has written the cron session, so the redirect does not fire for that request. Provenance: **Implemented**.

## Workflows

### Supplier sign-in and first use

1. User opens `supplier/login.php` and submits email and password.
2. The app loads `sysUser` by email where the security group is active.
3. Inactive user: "User account is inactive." Wrong password: "Username or Password incorrect." plus the lockout check. Unknown email or inactive group: "Login Details are invalid."
4. On success the session stores user id, username, email, security group, and `refCompanyID` as `COMPANYID`, then opens `index.php`.
5. If `blnTCAccepted` is 0, or `NemoPassword::checkExpiry` returns false, `index.php` shows the welcome form instead of the normal page. The user must accept `documents/TC/TCs - PR SA Supplier Portal.pdf` when terms are not yet accepted, and must set a new password when the password is aged.
6. Otherwise `index.php` renders the framework page with no extra invoice content. Invoice work is on `my.invoice.php`.

Provenance: **Implemented**. Whether the menu that reaches `my.invoice.php` is present for a supplier is **Unknown**. Menu rows are in the database.

`checkExpiry` returns true immediately when `sysSettings` `PASSWORD_MaxAge` is the string `false`, which is the value in the class's seed `INSERT`. A non-false value is treated as a number of days. The value in a live database is **Unknown / Requires Confirmation**.

### Supplier invoice

1. User opens `supplier/my.invoice.php`.
2. The list includes invoices with `blnDeleted = 0`. When `blnSpecial` is 0 it also appends `whereCompanyID()`.
3. New and edit save through `MyInvoice::Save`. A new row starts as status `New`. `refSupplierID` is set from the session company id.
4. The form asks the user to confirm that the amount includes VAT. The database column remains `dblInvoiceAmountExcluding`. The label is "Total Invoice Amount".
5. The user can upload an invoice file and one other file typed as Credit Note, GR, Purchase Order, Statement, or Other.
6. An on-screen checklist names the fields Pernod Ricard expects on the document, including the legal name, Cape Town address, and VAT number `4670144973`.
7. Status `Paid` hides save and upload. Delete updates `blnDeleted` only when status is not `Paid`.
8. Opening an invoice whose `refSupplierID` is not the session company redirects to `message.php?MID=r11`.

Provenance: **Implemented**. The checklist text is **Documented** on the page.

The toolbar also shows "Submit Invoice" (`e6500d1`). The `Action` switch has no case for that value, and no function submits the invoice to JDE. A click falls through to the invoice list. Provenance: **Implemented** as a button only. What the button was intended to do is **Unknown / Requires Confirmation**.

### Webadmin home

`webadmin/index.php` builds the same terms and password form as the supplier home when `blnTCAccepted` is 0 or `checkExpiry` returns false. The expiry call uses `$_SESSION[SUPPLIER]->ID`, not the webadmin user id. The next statement always sets `$page->Content` to `getDashboard($page->Security->blnSpecial)`, so `Display()` renders the dashboard and not the form.

`getDashboard` lists `Due` and `Overdue` invoices with no date window, and `Paid` invoices whose invoice, due, or payment date falls in the last 6 weeks. When the argument is `0` it adds `whereDepartment()`. The caller passes `blnSpecial`, so a user with `blnSpecial` 0 is filtered by their own `strDepartment`. Provenance: **Implemented**.

### Webadmin invoice

1. User opens `webadmin/invoice.php`.
2. Filters include AP team, department, supplier, date range, and status `New`, `Due`, `Overdue`, or `Paid`. `Partial Payment` is not in this filter. It is in the report filter.
3. When `blnSpecial` is 0, `whereDepartmentHR()` is applied.
4. Save, soft-delete (not when status is `Paid`), query, pay, and undo are on this page.
5. Pay sets status `Paid` and `dtPaymentDate` for selected invoices that are not already paid and not deleted. Undo, for group id `<= 2` on a paid invoice, sets status back to `New`.
6. "Query Invoice" emails the AP team with template "Invoice Query" and stores the query text.
7. A deleted invoice hides save, pay, and query and shows DELETED.
8. Export builds an Excel file through the list class.

Provenance: **Implemented**.

### Daily processing

`webadmin/cron.daily.php` runs the following in order on the default path. How the script is scheduled is **Unknown**. Nothing in the repository registers a cron job.

1. Task 5 reads CSV files from `documents/payments/`, matches them to invoices, inserts `tblPayment`, and sets invoice status `Paid` or `Partial Payment`. The FTP download step is commented out.
2. Task 6 builds a "Paid Invoices to Supplier" email per supplier for payments still in process status `New`, then marks those payments `Processed`. `$nemoEmail->Send()` is commented out, so the message is not sent.
3. Task 7 marks over-payments as process status `Error` and, when the report has rows, emails template "Payments Exception Report" to `sysSettings` `OverduePaymentsExceptionCC`.
4. Task 1 sets `dtDueDate` from the invoice date and `tblSupplier.intTerms` when the due date is empty.
5. Task 2 sets status `Due` or `Overdue` for invoices that are not `Paid` and not deleted, using `sysSettings` `DUE_DATE_MODIFIER` and `OVERDUE_DATE_MODIFIER`. Weekend dates are pushed to Monday.
6. Task 3 emails each AP team its `Due` invoices ("Due Invoices") and its `Overdue` invoices ("Overdue Invoices"). If any overdue invoices exist, it also emails `sysSettings` `OverdueCC`.

Task 4 (deactivate supplier users after inactivity) is inside `case "TESTING"` and the query is commented out. The comment says 30 days. Provenance: **Documented** in that comment. It does not run on the default path. **Implemented**.

On any host other than `www.prsa-supplierportal.co.za`, task 6 replaces the supplier email with `pj@overdrive.co.za` before the send call that is commented out. Provenance: **Implemented**.

### Supplier welcome

`webadmin/supplier.php` can send "Send Welcome Email". That path generates a random password, stores it with password date `2010-01-01`, and emails the address, the new password, and a login link. The comment in `supplier.php` says the welcome mail also resets the password. Provenance: **Implemented**.

## Integrations

| System | Direction | What is exchanged | Failure behaviour |
|---|---|---|---|
| MySQL | App to database | Users, invoices, payments, settings, mail templates | `system.php` prints "Database Connection has Failed!" and the mysqli error |
| SMTP | App to mail server | Templates named above, from "SMTP Send As" | `NemoEmail` sets `tblEmail.strStatus` to `Sent` or `Error`. The mail transport settings beyond those keys are **Unknown** |
| JDE payment file | File on disk into the app | CSV columns include JDE supplier number, invoice number, PO number, amount, payment date | Unmatched invoice or PO becomes process status `Warning`. Negative amounts are warnings. Download from an FTP directory is not called |
| Help desk | Browser mailto | `helpdesk@xprocure.com` with subject "PR Supplier Portal Query" | No server-side handler |
| QA FTP | Pipeline to host | `git ftp push` of the tree to `public_html/prsa.overdriveqa.co.za` on `master` | Pipeline secrets are not in the repo. Result of a real deploy was not observed |

No HTTP client call to JDE was found. Provenance: **Implemented** absence in the tree that was read.

## Constraints

- Invoice files are stored under `sysSettings` `SUPPLIER_Document_Path`. The value is not in the repository.
- Sessions last 7200 seconds and are stored on disk. The path differs by host key (`/pernod_ricard/supplier/SESSION/` on local and QA keys, `/supplier/SESSION/` on the production key, and the matching `webadmin` paths).
- Password policy keys are read from `sysSettings`: `PASSWORD_Enable_Policy`, `PASSWORD_Strength_Check`, `PASSWORD_Strength_Check_MinLength`, `PASSWORD_MaxAge`, `PASSWORD_MaxHistory`, `PASSWORD_Restricted_Words`, `PASSWORD_Reset_Interval`. Seed SQL in `NemoPassword` inserts policy on, minimum length 8, max age `false`, history 8, restricted words `password|123`, and reset interval `60 MINUTE`. Live values may differ.
- Five consecutive failed password attempts, with no later success inside that window, set `blnActive` to 0 and email the user. Provenance: **Implemented** (`CheckSecurityLock`).
- Root `.htaccess`, and the same rule under `supplier/` and `webadmin/`, redirect HTTP to HTTPS. The PHP root redirect uses `http://`. Provenance: **Implemented**. Which response the browser follows first depends on the web server. **Unknown / Requires Confirmation** for the deployed host.
- `display_errors` is set to `1` in both `system.php` files.

## Error and edge behaviour

| Situation | Behaviour | Provenance |
|---|---|---|
| Database connection fails | Message printed, settings not loaded | Implemented |
| Upload cannot be moved | "File upload failed." and message class `error` | Implemented |
| Delete or pay a paid invoice | SQL `WHERE` excludes status `Paid` | Implemented |
| Supplier opens another company's invoice | Redirect to `message.php?MID=r11` | Implemented |
| Webadmin password-age check | `webadmin/index.php` calls `checkExpiry("User", $_SESSION[SUPPLIER]->ID)`, then overwrites the page with the dashboard | Implemented. The supplier session key, and the discarded form, are **Requires Confirmation** as defects |
| Task 2 after a partial payment | Any non-paid invoice can be set to `Due` or `Overdue`, including status `Partial Payment` | Implemented |
| Reset link older than `PASSWORD_Reset_Interval` | `resetPassword.php` shows that the link has expired | Implemented |
| Empty cron download directory | Constant `???`. Import still scans `documents/payments/` | Implemented |

## Requirements that could not be confirmed

- The business names of security groups, and whether id `10` is the only supplier group.
- The menu labels operators actually see.
- Live `sysSettings`, including password max age, due-date modifiers, SMTP, and the document path.
- Whether production still runs this commit or a later repository.
- Whether "Submit Invoice" was approved scope. The code does not submit to JDE.
- Whether task 6's commented `Send()` and task 4's commented suspension are intentional.
- The meaning of message id `r11`. The message text is not in the PHP that was read.
