# PRD — printafoil (Printafoil Portal)

Evidence date: 2026-10-06. Source: `git@bitbucket.org:overdrive/printafoil.git` at `058841e9` (`master`, 2023-02-24).

There is no prior PRD in the repository or in Silicon Ark. Requirements below were recovered from code, config, and the SQL dump header. Provenance is marked on each item.

Credentials, password hashes, and SMTP secrets are present in git. They are not copied into this PRD.

Content that staff type into the portal (orders, stock, people) lives in MySQL. It is not fully described here. The SQL dump is a snapshot from 11 January 2023 and was not mined for personal data.

## Product purpose

Printafoil Portal is the logged-in system Printafoil staff use to run finishing work: sales through to dispatch, tooling (dies, blocks, plates), materials, finished goods, and the quality and HR documents that sit beside production.

Provenance: **Implemented** (`APP_TITLE` in `definitions.php`; table captions and homepage descriptions in `getTableList()`).

A WordPress and WooCommerce site is stored in the same repository under `b2b/` and is served from URL path `/b2b/`. Provenance: **Implemented**. Its role relative to the portal (public shop, client ordering, or unused copy) is **Unknown / Requires Confirmation**. Menu group `Order (B2B)` has no tables in the catalogue. Generated files `b2b_order_sys`, `b2b_order_sys_detail`, and `b2b_order_rfq` still exist and ask for those table permissions, but the tables are not in `getTableList()`.

## Users and roles

| Role | What they can do | Evidence | Provenance |
|---|---|---|---|
| Signed-in member | Opens tables their group or their own record allows. Insert, view, edit, and delete are separate flags | `getTablePermissions()` reads `membership_grouppermissions`, then overwrites with `membership_userpermissions` | Implemented |
| Super admin | One admin account. `MULTIPLE_SUPER_ADMINS` is false. Admin area covers groups, members, ownership, CSV import, mail, backup, thumbnails, field rebuild, maintenance mode | `definitions.php`; `admin/`; `config.php` `adminUsername` is `admin` | Implemented. Who uses that account is Unknown |
| Anonymous / guest | Group name `anonymous`, member id `guest` | `config.php` `anonymousGroup`, `anonymousMember` | Implemented. Which tables a guest can open is database state. Unknown |
| New visitor | Cannot self-register. `defaultSignUp` is `0`. The admin adds users | `config.php` | Implemented |
| Charlotte Rhys client user | If the member's group name is exactly `client_Charlotte_Rhyse`, login sends them to a filtered finished-stock screen instead of the normal homepage | `hooks/__global.php` `login_ok()` | Implemented. Whether that group still exists is Unknown |
| WordPress shop user | Can use WooCommerce, Ultimate Member, and User Registration only if those plugins are active | Plugins are in `b2b/wp-content/plugins/` | Vendor code. Activation is Unknown |

Membership group names other than `anonymous` and the hard-coded `client_Charlotte_Rhyse` are **Unknown**. They are rows in `membership_groups`, not PHP.

Homepage sections (Admin, Sales, Production System, and the rest in `project-plan.md`) are menu groupings of tables. They are not the same thing as membership groups.

## Main workflows

### W1 — Sign in and land on the homepage

Trigger: a person opens the site.

`index.html` refreshes immediately to `index.php`. `index.php` logs the user out when `signOut` is set, shows `login.php` when `signIn` or `loginFailed` is set (failed login responds HTTP 403), and otherwise shows `home.php`.

`home.php` lists tables the member may access, grouped by the AppGini table group. Tables whose names are exactly `Management` or `Reports` are hidden on the homepage. The code also names four tables (`Order_Create_Die`, `ConsumDetails`, `ConsumDetailsMainBld`, `ConsumDetailsNo12`) as allowed to show an "add new" link from the homepage. Catalogue keys are lowercase (`order_create_die`, `consumdetails`, and so on). `array_search` is case-sensitive, so those four exceptions do not match the catalogue keys.

After a successful login, `login_ok()` returns a URL only for group `client_Charlotte_Rhyse`. That URL opens `finish_stock_client_view.php` sorted on field 6, filtered to client name `Charlotte Rhys`, two "like Yes" filters, and a quantity greater than 0. Every other group gets the normal homepage.

Session name is `Printafoil_Portal`. Timezone is `Africa/Johannesburg`.

Provenance: **Implemented** (`index.php`, `home.php`, `definitions.php`, `hooks/__global.php`).

Failed login writes the username and the attempted password into table `log_failed_logins`, with IP and timestamp. Provenance: **Implemented** (`login_failed()`). This is a security defect, not a product requirement to keep. See `business-rules.md`.

### W2 — Work a table

Trigger: a member opens a table they can access.

Each catalogue table has `*_view.php` (list), `*_dml.php` (detail), and usually `*_autofill.php` (lookup). Permissions gate insert, view, edit, and delete. List page size defaults in code to a maximum of 2,000 records per page. Up to 20 filters. Print of a table view is capped at 100 records. Dates display as `j/n/Y` in PHP and `%d/%m/%Y` in MySQL (`config.php`). Lookup autocomplete asks for up to 1,000 rows.

Child grids show 20 records per page (`hooks/__global.php` `child_records_config()`).

Image fields upload under `images` (`baseUploadPath`).

Provenance: **Implemented** (AppGini runtime and `definitions.php`). The fields on each table are the generated detail screens. This PRD does not restate every column.

### W3 — Sell and confirm

Trigger: sales staff record an enquiry or a confirmation.

Catalogue tables:

- `sales_enquiry_carton` — caption Sales Enquiry (group Sales).
- `sales_enquiry_order` — caption Sales Order Confirmation. Calculated fields include `OrderedTotalAmount`, `TotCartonsDel`, `Qty_Ordered_v_Delivery`, and `PercentageOU`.
- `salesassist` — caption Sales Assistant Portal (group Sales Admin).
- `crm_salesopportunities` and `client_contacts` (group Sales Force CRM).
- `clients` — caption Printafoil Clients (group Admin).

Supporting lookups (statuses, sales people, glueing, thickness, delivery cost, repro cost, quote priority, amendments) are in the System group.

Provenance: **Implemented** captions and calculated-field names. The commercial rules that decide price, margin, or whether an enquiry may become an order are **Unknown**. They are not written as validation in the hook files that were sampled; most per-table hooks are empty AppGini stubs.

### W4 — Raise a purchase order

Trigger: a buyer creates a purchase order.

`purchase_order` (group Purchase Orders) has lines in `po_items` and a supplier from `suppliers`. Calculated behaviour:

- Line `price` is `ROUND(unit_price * qty, 0)` (zero decimal places).
- Header `Subtotal` is the sum of `unit_price * qty` across lines, rounded to 2 decimal places, or 0.00 if null.
- Header `Total` is `Subtotal * (1 - Discount/100) * (1 + Tax/100)`, rounded to 2 decimal places. Discount and tax are treated as percentages.
- Header `approvedby` is copied from `po_approval.Person_Approving` for that PO number. The query has no `ORDER BY` or `LIMIT`.

`po_quotes` stores up to three supplier-quote files. `po_department`, `po_status`, and `po_invto` (company to invoice) are lookups.

Provenance: **Implemented** in `calculated_fields()`. Whether zero-decimal line prices are intentional is **Unknown / Requires Confirmation**.

### W5 — Tooling: dies, blocks, plates, colour

Trigger: prepress or finishing staff record tooling.

- Dies: `order_create_die` (caption "Die's - Manage": order a die, find a die, manage location), `die_line_management`, `die_line_manage`, `die_line_range`. Movement and status tables are in System.
- Blocks and colour pass bags: `foilingblock`, `colorpassbag`, `embossingblock` and `debossingblock` (the last two are filed under System), with movement tables.
- Plates: `plates`, `jetplate`, `platesimaged`. Plate quantities (`QtyNEW`, `QtyDTPerror`, `QtyAdjArt`, `QtyRemadePress`, `QtyDamaged`, `QtyNoStanding`, `QtyTotalMade`) are sums of `platesimaged.QtyMade`.
- Colour standard: `colourstd` and movement, supplier, press sheet, sorting carton, and approval tables. `colourstd.colourstdappstatus` reads `colourstdapproval.status`.
- Inks: `inks_pantone`, `inks_stock`, `inks_manager2` (caption Passbag Colour Mix). Stock kilograms are calculated (`qtyinkg`, `qtyinkg_2`, `qtyinkg_total`).

Provenance: **Implemented**.

### W6 — Produce and see WIP

Trigger: production records work against a work ticket.

- `production_finishing` — caption WT Production. Calculated `totalqty`, `totalqty_1`, `fulltotalqty`.
- `wip_hud` — caption Live WIP Board - FD. Calculated columns track queue, in, out, waste, complete, and on-machine counts for finishing operations. Field prefixes: `op_mb_` (main building, inferred from the "Main Bld" captions elsewhere), `op_fd_foil_`, `op_fd_foilem_`, `op_fd_emboss_`, `op_fd_ccem_`, `op_fd_cnc_`, `op_fd_strip_`, `op_fd_glue_`, `op_fd_sort_`, `op_fd_handw_`, `op_fd_cover_`, `op_fd_stay_`, `op_fd_window_`, `op_fd_store9_`. The prefix expansion is **Inferred** from captions such as "Consumable at 9 Glosderry" and "Consumable Details at Main Bldg". The SQL itself only shows the column names.
- Extra screens on the same `wip_hud` table: CNC, completed, emboss, foil, glue, on machine, plan, several queue screens, quantity, sort, strip, and a WIP screen (`wip_hud_*_view.php`).
- `wip_movement` and `wip_operations` move and describe WIP. Both have a calculated `max_qty`.
- One report table per finishing operation (`group_foiling`, `group_emboss`, and the other `group_*` tables). Each caption says "Allow Group Menu for Reports".
- Digital / print-and-deliver: `digital_work`, `pnd_orders` (caption "Vpress Print and Deliver Orders"), `paf_digital_sample_request` (caption "HP Printer print a Sample Request").

Provenance: **Implemented**, with the prefix reading marked **Inferred**.

### W7 — Finished stock, stores, and dispatch

Trigger: stores or dispatch staff receive, adjust, pick, or ship finished goods.

`finish_stock` (Finish Stock Manager) calculates finished, excess, and spoilage quantities from `production_finishing`, adjustments from `finish_stock_adjusting`, dispatch quantity from `dispatch_pick`, and values from `fin_stock_costing.price`. Older movement columns (`old_sys_*`) sum `finish_stock_move` in and out. Alternate screens: `finish_stock_client_view.php` and `finish_stock_full_view.php`, both on table `finish_stock`.

Also in Store Manager: goods return vouchers, spoilage reasons, packers and checkers, pick slips (`dispatch_pick`, `dispatch_pick_qty`), stores receiving, stores released, and finish-stock pictures.

`dispatch_sys` (Dispatch Portal) calculates `qty_tot`, `qty_picked`, `btftotalqty`, and `join_del_numbers`.

Provenance: **Implemented** field list in `calculated_fields()`. The join filters inside each query were not copied line by line into this PRD; they live in that function.

### W8 — Materials and consumables

Trigger: stores record board, paper, laminate, foil, or consumables.

`new_stk_item` (Material Stocks) keeps quantity at Printafoil and at four merchants named in the field names: Peter Paper, Jak Paper, Gerber Paper, and Paper Smith, including paid-in and paid-out columns, plus laminate quantity, price per square metre, and rand value. `stk_item` has the same location quantities without the paid-in/out columns.

Consumables: `consumablesmanage` totals amounts from three location tables — finishing (`consumdetails`), main building (`consumdetailsmainbld`), and 9 Glosderry (`consumdetailsno12`). `TotalAvailableALL` adds those three totals. `ReorderNotice` is the sum of `TotalAvailableALL - MinOrderQty + MinOrderQtyMainBld + MinOrderQtyNo12`. Provenance: **Implemented**. Whether that reorder expression is the intended rule is **Unknown / Requires Confirmation**, because it subtracts one minimum and then adds two others.

### W9 — Samples, documents, people, and compliance

Trigger: the relevant department files a record.

- Samples: `samplerequest`, sample numbers, sample maker, status.
- Documents: `document_management`, HR policies, job descriptions, training, draft and template documents.
- Equipment and ISO / HSE: equipment register and service history, ISO manual, procedures, SOPs, records, audits, non-conformance, waste items, HSE action list and documents, SEDEX policy documents. Non-conformance `ctn_percentage` is `(ctn_spoil_qty / ctn_ordered_qty) * 100`. Division when ordered quantity is 0 is **Unknown**.
- FSC: audits, training, policy, manual, procedures, SOPs.
- IT assets, key register, maintenance and repair requests.
- Customer survey tables sit in menu group `Manny Test` (`customer_survey_create`, `customer_survey_detail`, `fin_stock_costing`). A parent screen `customer_survey` exists as files but is not in the catalogue.

Provenance: **Implemented**.

### W10 — Shop under /b2b/

Trigger: a browser requests `/b2b/`.

WordPress pretty permalinks rewrite to `/b2b/index.php`. WooCommerce 7.0.0 is present, so a shop can list products, take checkout, and (if the PDF plugin is active) produce invoices and packing slips. Themes present include Best Shop, Cartsy Lite, Storefront-related themes, and the Twenty Twenty series. Which theme is active is **Unknown**.

Provenance: **Implemented** as vendored software and rewrite rules. Shop catalogue, prices, and payment gateway are database and plugin settings. **Unknown**.

## Integrations

| System | Direction | What is exchanged | Evidence | Provenance |
|---|---|---|---|---|
| MySQL / MariaDB | Portal and WordPress read and write | Portal tables; WordPress tables | `config.php` database `printafoil_web_portal`; `b2b/wp-config.php` database `printafoil_b2b`; dump header MariaDB 10.3.35 | Implemented. Live database name for the portal conflicts with the dump. Requires Confirmation |
| SMTP `mail.silvaitservices.co.za:587` | Outbound mail | Membership approval and admin "mail all users" | `config.php` `mail_function` = `smtp`. Encryption string is empty | Implemented. Whether the server requires TLS is Unknown |
| Vpress | Named only | `pnd_orders` caption says "Vpress Print and Deliver Orders" | Caption text | Inferred that orders are recorded for Vpress work. No API client was found |
| HP printer | Named only | Digital sample request caption says an HP printer prints a sample | Caption text | Inferred. No printer integration code was found |
| Facebook, Google, MailPoet, Jetpack | Present as WordPress plugins | Shop marketing and mail, if active | Plugin directories | Vendor. Activation Unknown |

No payment gateway is configured in the portal. Any WooCommerce payment method would be in the WordPress database. **Unknown**.

## Constraints

- Single tenant. `MULTI_TENANTS` is false.
- One super admin.
- Public sign-up is off.
- Interface language file is `defaultLang.php` (English). The file tells translators to edit `language.php` instead. A custom translation was not reviewed line by line.
- Uploaded files are stored on the web server under `images/`, and a large set of those files is committed in git (commit `058841e9`).
- The portal has no root `.htaccess` in the repository. The shop does.

## Requirements discovered from the implementation

| ID | Requirement | Provenance |
|---|---|---|
| FR-001 | The product title shown to users is Printafoil Portal | Implemented |
| FR-002 | A member can open only tables where insert, view, edit, or delete is allowed for their group, unless a user-specific row overrides that | Implemented |
| FR-003 | Visitors cannot sign themselves up. The admin creates members | Implemented |
| FR-004 | Failed sign-in returns HTTP 403 and shows the login form | Implemented |
| FR-005 | Group `client_Charlotte_Rhyse` is redirected after login to finished stock filtered for Charlotte Rhys | Implemented |
| FR-006 | A purchase order total is subtotal after a percentage discount, then plus a percentage tax | Implemented |
| FR-007 | Finished-stock quantities roll up from production, adjustments, and picks | Implemented |
| FR-008 | Material stock is tracked at Printafoil and at Peter Paper, Jak Paper, Gerber Paper, and Paper Smith | Implemented |
| FR-009 | Consumable quantities are kept at finishing, the main building, and 9 Glosderry | Implemented |
| FR-010 | The WIP board keeps per-operation queue, in, out, waste, complete, and on-machine figures | Implemented |
| FR-011 | WordPress with WooCommerce is available at path `/b2b/` | Implemented. Business use Requires Confirmation |
| FR-012 | Membership mail is sent by SMTP, and the approval template links to `https://printafoil.bluolive.co.za` | Implemented. Live host Requires Confirmation |

## Behaviour that is not a confirmed requirement

- Logging the failed password in clear text (`login_failed`). Implemented, and it should be reviewed as a defect.
- Menu groups with no tables: Charlotte Rhys, 1701 Nougat, Order (B2B), Sales Reports.
- 28 generated table screens that are not in `getTableList()` (listed in `technical-documentation.md`).
- The `Manny Test` menu group.
- Homepage "add new" exceptions that do not match real table keys because of letter case.
