# Business rules — pernod_ricard

Evidence date: 2026-10-07.

Source: `master` at `24c1e12`.

Rules below are taken from code and from comments that state a rule. **Inferred** means the code behaves this way and no comment states it as policy. Live `sysSettings` can change several of the numeric rules without a code change.

## Access

| Rule | Label | Evidence |
|---|---|---|
| A user can sign in only when `sysUser.blnActive` is 1 and `sysSecurityGroup.blnActive` is 1 | Implemented | Both login pages |
| Supplier password check uses password type `Supplier`. Webadmin uses type `User` | Implemented | `NemoPassword::checkPassword` |
| Security group id 10 cannot stay in webadmin | Implemented | `webadmin/login.php`. Message text is the rule a supplier sees |
| Supplier login does not reject group 10 | Implemented | `supplier/login.php` has no group-id test |
| A page built with `Nemo` requires the matching session | Implemented | `_nemo.cls.php` |
| `blnView` must be 1 for the current script, or the framework blocks the page | Implemented | Security query in `_nemo.cls.php` |
| Group id `<= 3` may mark invoices paid | Documented in a comment as "AP Manager and up". Implemented as the numeric test | `webadmin/invoice.php` |
| Group id `<= 2` may undo a payment on a paid invoice | Documented in a comment as "Admin (and up) only". Implemented as the numeric test | `webadmin/invoice.php` |
| When `blnSpecial` is 0, lists exclude suppliers in department `HR` unless the signed-in user's own department is `HR` | Implemented | `whereDepartmentHR` |
| `checkDepartmentHR` allows the row when the supplier department is not `HR`, or when it equals the user's department | Implemented | `webadmin/project.functions.inc.php`. Comment dated 2016-06-29: "Removed department check [D != HR]" |
| A supplier invoice list with `blnSpecial` 0 is limited to `refCompanyID` of the session | Implemented | `whereCompanyID` |
| A supplier cannot open an invoice for another company | Implemented | Redirect to `message.php?MID=r11` |
| Saving a supplier invoice always writes `refSupplierID` from the session company | Implemented | `MyInvoice::Save` |
| Cron runs as user id `-1`, security group 1 | Implemented | `cron.daily.php` |

The group names behind ids 1, 2, 3, and 10 are **Unknown / Requires Confirmation**, except the on-screen words for group 10 and the comments for the pay and undo checks.

## Passwords

| Rule | Label | Evidence |
|---|---|---|
| Stored password is a salt plus a hash. The class comment says `hash(protocol, salt + password)` | Documented in the class comment. Implemented in `NemoPassword` | `_nemo.password.cls.php` |
| Seed defaults if the settings rows are inserted by the class: policy enabled, minimum length 8, max age `false`, history 8, restricted words `password` and `123`, reset link `60 MINUTE` | Implemented as SQL in the constructor. Live rows may differ | Same file |
| When max age is `false`, the password is not treated as expired | Implemented | `checkExpiry` |
| When max age is not `false`, a password older than that many days fails the expiry check | Implemented | `checkExpiry` |
| A new password that matches one of the last `PASSWORD_MaxHistory` passwords is rejected with "Your password has already been used." | Implemented | `checkLastPasswords` |
| The change form requires the strength widget class `strong`, matching confirm value, and the policy JavaScript (length, upper and lower case, a digit) | Implemented | `supplier/index.php` |
| Five failed passwords since the last success set the user inactive and email them | Implemented | `CheckSecurityLock`. The number 5 is in code, not in `sysSettings` |
| An inactive user is told "User account is inactive." | Implemented | Login pages |
| A reset link past `PASSWORD_Reset_Interval` is rejected | Implemented | `resetPassword.php` |
| Welcome email replaces the supplier password and stores the password date as `2010-01-01` | Implemented | `Supplier::sendWelcomeMail`. The comment calls that date a default aged date |
| The welcome email contains the new password | Implemented | Template substitution `Password` |

## Invoices

| Rule | Label | Evidence |
|---|---|---|
| A new supplier invoice with an empty status is saved as `New` | Implemented | `MyInvoice::Save` |
| Empty invoice date becomes today. Empty due date and payment date are SQL `NULL` | Implemented | `MyInvoice::Save` |
| The amount the user types is stored in `dblInvoiceAmountExcluding` | Implemented | Column name |
| The screen calls that amount "Total Invoice Amount" and the prompt says it must include VAT if VAT applies | Documented on the page. Implemented as the label and `confirm()` text | `supplier/my.invoice.php` |
| Zero or blank amount fails client-side validation | Implemented | `jsValidate` replacement |
| The checklist requires invoice number, date, PO number, the Pernod Ricard South Africa legal name and Cape Town address, VAT `4670144973`, the agreed PO amount, supplier name, address, phone, VAT number if applicable, and banking details | Documented on the page | Same file |
| Status `Paid` cannot be edited or uploaded by the supplier, and cannot be soft-deleted | Implemented | Status switch and `DELETE` SQL |
| Webadmin pay sets status `Paid`, payment date to now, and last user `PayInvoice()` | Implemented | `Invoice::PayInvoices` |
| Undo sets a paid invoice back to `New` | Implemented | `Invoice::UndoPayment` |
| Payment import sets `Paid` when the rounded sum of payments equals the rounded invoice amount, and `Partial Payment` when the rounded sum is greater than 0 and lower than the invoice amount | Implemented | `ImportPayments::process_TMP` |
| A negative payment amount is a warning and is not described as a credit | Implemented | Note text "Payment.Amount is nagative" |
| An unmatched invoice number or PO number is process status `Warning` | Implemented | `process_TMP` |
| Task 2 may replace a non-paid status, including `Partial Payment`, with `Due` or `Overdue` | Implemented | `cron.daily.php`. **Inferred** that this can undo a partial-payment status. No comment says that is intended |
| Due date is month-end of the invoice date plus `tblSupplier.intTerms`, with Saturday and Sunday moved forward | Implemented | Task 1. The comment example is "30june = MonthEnd(1-31May) + 30 days" |
| If that calculated day of month is on or before the 15th, the due day is forced to the 15th, or the 16th or 17th when the original day is Sunday or Saturday | Implemented | Task 1 |
| Task 2's due and overdue cutoffs come from `DUE_DATE_MODIFIER` and `OVERDUE_DATE_MODIFIER` | Implemented | The setting values are **Unknown** |
| Soft-deleted invoices are excluded from lists and from the daily status updates | Implemented | `blnDeleted = 0` |

## Department and company

| Rule | Label | Evidence |
|---|---|---|
| Webadmin session stores `strDepartment` and `refCompanyID` | Implemented | `webadmin/login.php` |
| Supplier session stores `refCompanyID` and does not set a department | Implemented | `supplier/login.php` |
| `getDepartment()` on the supplier side reads `$_SESSION[SUPPLIER]->DEPARTMENT` | Implemented | `supplier/project.functions.inc.php`. Login never assigns that property |

## Mail the daily script sends

| Rule | Label | Evidence |
|---|---|---|
| Due invoices are emailed to each AP team's `strEmail` | Implemented | Task 3, template "Due Invoices" |
| Overdue invoices are emailed to each AP team, and a combined overdue list goes to `OverdueCC` when the count is not zero | Implemented | Task 3 |
| Payments exception mail goes to `OverduePaymentsExceptionCC` only when the report has data | Implemented | Task 7. Comment 2018-10-29 names a change of a person's mailbox to `delia.herringer@pernod-ricard.com`. The recipient in code is the setting, not that address |
| Off the production host, task 6 would address `pj@overdrive.co.za` instead of the supplier | Implemented | The send itself is commented out |
| Task 6 still marks payments `Processed` | Implemented | Update runs before the commented send |

## Files

| Rule | Label | Evidence |
|---|---|---|
| Invoice upload file name is `Y-m-d` plus invoice id plus the original name, with spaces turned into underscores | Implemented | `MyInvoice::Save` |
| An existing file with that name is deleted before the new file is moved | Implemented | `unlink` then `move_uploaded_file` |
| Other-file upload requires a file type when a file is chosen | Implemented | `jsValidateOtherFileUpload` |
| The other-file type is written into the column named by `strOtherFileType` (`strCreditNoteFilename`, `strGRFilename`, `strPurchaseOrderFilename`, `strSupplierStatementFilename`, `strOtherFilename`) | Implemented | Dynamic `UPDATE`. **Inferred** that a crafted type could name another column. No allow-list is in the PHP |

## Deployment rule visible in the pipeline

| Rule | Label | Evidence |
|---|---|---|
| `master` is pushed by FTP to the QA path `public_html/prsa.overdriveqa.co.za` | Implemented in YAML | `bitbucket-pipelines.yml`. The first-time `git ftp init` line is commented |
| `development` would run `git ftp init` against `prsa.overdriveqa.co.za` without `public_html`, and its push line is commented | Implemented in YAML | That branch does not exist on the remote |
| `bitbucket-pipelines.yml` is not uploaded by git-ftp | Implemented | `.git-ftp-ignore` |

## Rules that must not be treated as confirmed policy

- Password max age, history, and minimum length in production. The seed and the database can disagree.
- That group ids 2 and 3 are "Admin" and "AP Manager". Only the comments say so.
- That 30 days of inactivity suspends a supplier. The comment says so. The code does not run it.
- That suppliers submit invoices into JDE from this portal. The button does not.
- That paid invoices are emailed to suppliers by the daily script. The send call is commented out.
