# Business rules — pernod-ricard-prod

Evidence date: 2026-10-07.

Source: `master` at `19e9775` (2026-06-11).

Rules below are taken from code. **Inferred** means the code does this and no separate requirement states it. Live thresholds that are read from `sysSettings` are named, not invented. There is no business-rules document in the source repository.

## Access

1. A supplier user is security group id `10`. Creating a supplier inserts `sysUser.refSecurityGroupID = 10` and `refCompanyID` equal to the new supplier id. **Implemented** (`webadmin/includes/supplier.cls.php`).
2. Group `10` cannot use webadmin. Login, Azure callback, SSO bypass, and document download redirect to the supplier portal. **Implemented**.
3. Webadmin Microsoft sign-in only continues when `sysUser.strEmail` matches the Azure `preferred_username` or email, the user is active, and the security group is active. The code does not create a user from Azure. **Implemented** (`webadmin/auth.php`).
4. The visible webadmin instruction is "All users must authenticate using Microsoft Azure AD Single Sign-On." **Implemented** (login page copy).
5. `SSO_OVERRIDE` must be `true`, `1`, or `yes` before `?bypass=` logs a webadmin user in without Azure. `.env.example` says this is for dev/staging. **Implemented** and **Documented** in that example file.
6. Menu rights are `blnView`, `blnDelete`, `blnSave`, `blnNew`, and `blnSpecial` on `sysSecurity` for the user's group. **Implemented**.
7. When `blnSpecial` is `0`, the user only sees suppliers that pass `whereDepartmentHR()`, and opening another department's invoice, supplier, or remittance redirects to the access-denied message. **Implemented**. The department value comes from `$_SESSION["USER"]->DEPARTMENT` (`strDepartment` on `sysUser`). **Implemented**.
8. Pay actions are shown when the security group id is less than or equal to `3`. Undo payment is shown when the id is less than or equal to `2`. Comments call these "AP Manager and up" and "Admin and up". **Implemented** as numbers. The names are **Documented** only in comments. **Unknown / Requires Confirmation** which group ids exist and whether a lower id is a higher privilege in the database.
9. A supplier invoice list, when `blnSpecial` is `0`, adds `whereCompanyID()`, which filters on `$_SESSION["USER"]->COMPANYID`. That value is `sysUser.refCompanyID`, and a new supplier user is stored with `refCompanyID` equal to the new `SupplierID`. **Implemented**. Document download for the supplier appends the same company filter. **Implemented** (`supplier/download_document.php`).

## Supplier login

10. Login requires an active user and an active security group. **Implemented**.
11. A correct password does not open the portal. It emails a 6-digit code and opens the verification page. **Implemented**.
12. The code is stored as `strNewCode`. The previous code, if a row already exists, is copied to `strPreviousCode`. Expiry is 5 minutes, written into `dtCreatedAt`. Verification requires `dtCreatedAt` later than the current time and `blnActive = 1`. **Implemented**. The column name `dtCreatedAt` holding an expiry time is what the code does. Whether that was intended is **Inferred**.
13. `sendOTP` returns `1` after `NemoEmail::Send()` without checking the mail result. The login page treats that return as "email sent". **Implemented**. A failed SMTP send can still be treated as success. **Inferred** from the return value.
14. Five consecutive `sysLogin` rows of "Login failed: Incorrect password", with no later "Login successful" in that window, are passed to `CheckSecurityLock`. **Implemented**. The lock message and any account change inside that function after the count are in `system.functions.inc.php` beyond the count of 5. The threshold 5 is **Implemented**.
15. Password history, minimum length, restricted words, maximum age, and reset-link lifetime are `sysSettings` keys `PASSWORD_MaxHistory`, `PASSWORD_Strength_Check_MinLength`, `PASSWORD_Restricted_Words`, `PASSWORD_MaxAge`, and `PASSWORD_Reset_Interval`. The password class inserts defaults (history 8, minimum length 8, restricted words `password|123`, max age `false`, reset interval `60 MINUTE`) only when it creates those rows. **Implemented** as defaults in code. Production values are **Unknown**.
16. A supplier who has not accepted terms, or whose password fails the expiry check, sees only the welcome form on `supplier/index.php` until they update. **Implemented**.
17. A new password that matches one of the last stored passwords is rejected with "Password not saved. Your password has already been used." **Implemented**.

## Invoices

18. Status values in the column-change comment are `New`, `Due`, `Overdue`, `Partial Payment`, `Paid`, `Submitted to JDE`, `Unsuccessful match`, and `Successful 100% match in JDE`. Default `New`. **Documented** as SQL in `webadmin/includes/invoice.cls.php`. The application writes those strings. **Implemented**.
19. A new invoice must have an uploaded invoice file, a non-zero excluding amount, a non-zero including amount, and a PO number that is not `0`. Amount and PO checks are client-side script. **Implemented**.
20. PO number entry strips letters on blur (`removeAlpha`). **Implemented**.
21. Delete sets `blnDeleted = 1` and does not change a `Paid` invoice. **Implemented**.
22. A deleted invoice cannot be saved, paid, or queried from the webadmin form. **Implemented**.
23. Webadmin cannot save when status is `Submitted to JDE`, `Successful 100% match in JDE`, or `Unsuccessful match`. On `Unsuccessful match` the submit button can still be used. **Implemented**.
24. The supplier cannot save or upload when status is `Submitted to JDE`, `Successful 100% match in JDE`, or `Paid`. The supplier `Unsuccessful match` case in that switch is commented out, so an unsuccessful match stays editable for the supplier. **Implemented**. This differs from the webadmin rule. **Inferred** that the difference is intentional because the webadmin comment says Pernod can resubmit and not save, while the supplier case for that status is commented.
25. Submit is refused unless `strPurchaseOrderFilename` is not empty. **Implemented**.
26. A JDE response with `PV` present and not `0` is a successful match. `PV` is stored on `strPaymentReferenceNumber`. Any other JSON body is an unsuccessful match. Error text that contains "Company" is shown as "No receipt exist for the purchase order." Text that contains "Fiscal Year Not Set Up" is shown as "Purchase Order cannot be issued after the date of the Invoice." Text that contains "Currency Exchange Rate Not Found" is shown as "No receipt found for the purchase order." **Implemented**.
27. Manual pay sets status `Paid`, sets the payment date to the application `$DT`, and emails "Invoice Paid". It does not pay a row that is already `Paid` or deleted. **Implemented**.
28. Undo payment sets status from `Paid` to `New`. It does not clear `dtPaymentDate` in that update. **Implemented**.
29. Empty due date and payment date on save are stored as SQL `NULL`. A new invoice with an empty invoice date uses `$DT`. **Implemented**.

## Due dates and ageing

30. Due date, when missing, is the month-end of the invoice date plus `tblSupplier.intTerms` days, then adjusted:
    - If the resulting day-of-month is 15 or less, the due day is the 15th of that month, or the 16th if that candidate is a Sunday, or the 17th if it is a Saturday.
    - Otherwise the due date is the month-end of that result, plus 1 day if that month-end is a Sunday, or plus 2 days if it is a Saturday.
    **Implemented** (`cron.daily.php` task 1). The comment states the month-end-plus-terms rule. The 15th and weekend shifts are **Implemented** without a separate written policy.
31. `Due` and `Overdue` use `sysSettings` `DUE_DATE_MODIFIER` and `OVERDUE_DATE_MODIFIER` as dates relative to today (`strtotime` of those values). If that date falls on Sunday, one day is added. If it falls on Saturday, two days are added. **Implemented**. The stored modifier strings are **Unknown**.
32. An invoice that is not `Paid`, not `Partial Payment`, and not deleted is updated when its due date is on or before those thresholds. Overdue overwrites Due when both match. **Implemented**.
33. JDE match statuses are not in the exclusion list for task 2. A submitted or matched invoice can be overwritten to `Due` or `Overdue` by the daily script. **Inferred** from the `NOT IN ('Paid', 'Partial Payment')` filter. Whether that is accepted is **Unknown / Requires Confirmation**.

## JDE payments

34. Imported payment rows are ignored unless currency code is `ZAR` and document company is `430`. **Implemented**.
35. The import walks one day before the start date through one day after today. The default start is yesterday, so the window is three calendar days unless a start date is passed. **Implemented**.
36. Staging rows whose payment reference does not match `tblInvoice.strPaymentReferenceNumber` (trim, case-insensitive) are marked `Warning`. **Implemented**.
37. A payment is inserted once per PO number, payment reference, payment date, amount, and payment number. **Implemented**.
38. An invoice becomes `Paid` when it is not already `Paid` and `ROUND(invoice amount including, 0) + ROUND(sum of payment amounts, 0) <= 0`. **Implemented**.
39. An invoice becomes `Partial Payment` when it is not already that status, the rounded payment sum is not zero, and the same addition is greater than zero. **Implemented**.
40. Both of those updates set `dtPaymentDate` to `CURDATE()` and append a note `Paid in Full:` or `Partial Payment:` plus the date. **Implemented**.
41. The payments exception report lists `tmpPayment` rows in status `Warning` or `Error`, and `tblPayment` rows whose status is not `Processed`. Notes containing "Over Payment" are highlighted. **Implemented** (`payments.exception.rpt.php`).

## Accounts and mail

42. Daily task 4 deactivates supplier users (`strSecurityGroup = 'Supplier'`) whose last `Login successful` is older than `INACTIVE_ACCOUNT_Suspension_Interval` days. The comment says 30 days. The value is a setting. Users with no login row are not selected, because the query inner-joins `sysLogin`. **Implemented**.
43. `cron.inactive-users.php` deactivates active supplier users when the last login in `('Login successful', 'New user created and logged in')` is more than 180 days ago, or when there is no such login and `dtFirstEdit` is more than 6 months ago. **Implemented**. This is a second rule beside task 4. Which one production runs, and whether both run, is **Unknown / Requires Confirmation**.
44. Cron emails for due, overdue, paid invoices, the exception report, and unsuccessful orders are not sent when `$isDebug` is true. The committed `cron.daily.php` sets that flag to true. **Implemented**.
45. Task 8 selects invoices whose invoice date is on or before today minus `sysSettings` `DATE_OLDER_THAN_Interval` days, whose status is `New` or `Submitted`, and whose AP team is active. The 2020-11-26 comment says "older that 3 days". The mail replaces the AP team address with `groupcreditors@pernod-ricard.com` and adds CC `maanie@overdrive.co.za`. **Implemented**. The status string `Submitted` does not appear in the `strStatus` enum comment (`Submitted to JDE`). Whether any row has status `Submitted` is **Unknown / Requires Confirmation**.
46. Email bodies are database templates addressed by these names: "One-Time Password", "Reset Password", "Send Welcome Email", "Invoice Query", "Invoice Paid", "Invoice Successful Match", "Invoice Unsuccessful Match", "Unsuccessful Invoice Orders", "Paid Invoices to Supplier", "Payments Exception Report", "Due Invoices", "Overdue Invoices". **Implemented** as template names. The wording of each template is **Unknown** (stored in the database).
47. Supplier session garbage-collection lifetime is 7200 seconds. **Implemented** (`supplier/system.php`). Webadmin does not set the same value.

## Documents

48. Uploaded names are `{date}_{invoice id}_{original name with spaces replaced by underscores}` under `sysSettings` `ADMIN_Document_Path`. **Implemented**.
49. Download requires a signed-in user. Webadmin download also refuses group `10`. Allowed types are invoice, remittance, and payment. **Implemented**.
50. The supplier invoice checklist on the page states Pernod Ricard address "21, Woodlands Drive, Country Club Estate Woodmead, Building 6, 2191 Johannesburg", telephone `+27 11 802 0600`, and VAT registration number `4670144973`, plus required invoice contents (agreed PO amount, supplier name, address, phone, VAT number if applicable, banking details). **Documented** as on-screen copy in `supplier/my.invoice.php` (comment 2021-02-02 "updated address"). The code does not validate the uploaded file against that list.
