# Technical documentation — printafoil (Printafoil Portal)

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

Secrets in `config.php`, `config.bak.php`, and `b2b/wp-config.php` are not repeated here.

## What this repository is

Two PHP applications in one git tree, plus a database dump and a file store of operational documents.

1. **Printafoil Portal** at the repository root. Generated by AppGini 22.14 (a few screens still carry the comment "AppGini 22.13"). Custom code is small relative to the generated screens. The clearest product-specific hook is `hooks/__global.php`.
2. **WordPress 6.1.1** in `b2b/`, with **WooCommerce 7.0.0**. Permalink base `/b2b/`.
3. **phpMyAdmin 5.2.0** in `phpmyadmin/`.
4. **SQL dump** `Database/printafoil_backup_20230111.sql` (about 95 MB), produced 11 January 2023 by phpMyAdmin 5.2.0. Header: MariaDB 10.3.35, PHP 8.1.14. It contains `CREATE` statements for 406 tables across databases named in the dump as `bluolive`, `printafoil`, and `printafoil_b2b`.

There is no `composer.json` at the portal root, no `package.json` for the portal, and no `bitbucket-pipelines.yml`.

## Repository history

| Commit | Date | Notes |
|---|---|---|
| `85a07505` | 2023-01-11 | Initial commit, 1 file |
| `72ec57ca` | 2023-02-20 | Bulk import, 46,987 files |
| `058841e9` | 2023-02-24 | 477 files, almost all under `images/` and `stats/` |

Only branch: `master`.

A checkout on Windows without long-path support fails inside vendored WordPress plugins (`Filename too long` under Beaver Builder, Facebook for WooCommerce, Google Listings and Ads, Jetpack, and WooCommerce). The git objects are complete; the working tree needs long paths enabled for those files.

## Portal layout

| Path | Role |
|---|---|
| `index.html` | Meta refresh to `index.php` |
| `index.php` | Logout, login, or homepage |
| `definitions.php` | App constants |
| `config.php` | Database, host, SMTP, admin account, date formats |
| `lib.php` and `incCommon.php` | AppGini runtime. `incCommon.php` is very large because it embeds per-table SQL |
| `admin/incFunctions.php` | `getTableList()`, `calculated_fields()`, admin helpers. About 1 MB |
| `*_view.php`, `*_dml.php`, `*_autofill.php` | Generated table UI. 233 catalogue tables, plus screens listed under "Not in the catalogue" |
| `hooks/` | Hook scripts, and also a large copy of generated PHP, filter pages, and `incCommon.php`. About 1,006 files. `hooks/README.html` is stock AppGini text |
| `resources/` | Initializr / Bootswatch CSS used by the AppGini UI |
| `images/` | `baseUploadPath`. Also the drawings and quality PDFs added in `058841e9` |
| `templates/` | AppGini templates |
| `import-csv/` | CSV import support |
| `stats/` | Webalizer output for 2023-01 and 2023-02, plus `webalizer.hist` and `dns_cache.db` |
| `.usermin/` | Directory present. Its role was not determined. **Unknown** |

`hooks/` containing full copies of generated pages (for example `hooks/clients_view.php` and `hooks/incCommon.php`) is unusual for a normal AppGini hooks folder, which is meant to hold small hook files. Provenance: **Implemented** as files. Whether those copies are executed is **Unknown**. The running include from a root screen is typically `hooks/<table>.php`, not `hooks/<table>_view.php`.

## Runtime configuration

From `definitions.php`:

| Constant | Value |
|---|---|
| `APP_TITLE` | Printafoil Portal |
| `APP_VERSION` | 22.14 |
| `SESSION_NAME` | Printafoil_Portal |
| `DATABASE` | mysqli |
| `mysql_charset` | utf8 |
| `TIMEZONE` | Africa/Johannesburg |
| `MULTI_TENANTS` | false |
| `MULTIPLE_SUPER_ADMINS` | false |
| `datalist_max_records_per_page` | 2000 |
| `datalist_filters_count` | 20 |
| `datalist_max_records_dv_print` | 100 |
| `datalist_image_uploads_exist` | true |
| `DEBUG_MODE` | false |

From `config.php` (names only):

| Setting | Value |
|---|---|
| Database server | `localhost` port `3306` |
| Database name | `printafoil_web_portal` |
| `$host` | `printafoil.bluolive.co.za` |
| Admin username | `admin` |
| Anonymous group / member | `anonymous` / `guest` |
| Sign-up | `defaultSignUp` = `0` |
| Mail | `mail_function` = `smtp`, server `mail.silvaitservices.co.za`, port `587`, encryption empty |
| Sender | name "Membership management", address `manny@printafoil.co.za` |
| Upload path | `images` |
| PHP date format | `j/n/Y` |
| MySQL date format | `%d/%m/%Y` |

WordPress `b2b/wp-config.php`:

| Setting | Value |
|---|---|
| `DB_NAME` | `printafoil_b2b` |
| `DB_HOST` | `localhost` |
| `DB_CHARSET` | `utf8mb4` |
| User | same account name as the portal config (`bluolive`). Password not recorded here |

The dump's database names (`bluolive`, `printafoil`, `printafoil_b2b`) do not include `printafoil_web_portal`. The dump is older than the bulk commit (11 January versus 20 February 2023). **Unknown / Requires Confirmation** which name production used after the dump.

A filesystem path in the dump points at `/home/bluolive/domains/printafoil.bluolive.co.za`. Provenance: dump content (a Wordfence file list). That is evidence of a hosted layout on 11 January 2023, not proof of the current server.

## Request flow

1. `index.html` or `index.php`.
2. `index.php` includes `lib.php`.
3. Sign-out calls `logOutUser()` and redirects to `index.php?signIn=1` (multi-tenant branch is off, so the SaaS login URL is not used).
4. Sign-in failure sends HTTP 403 and includes `login.php`.
5. Otherwise `home.php` loads `hooks/links-home.php` if present, then `hooks/home-custom.php` if that file exists (it would replace the default homepage and exit). The default homepage builds cards from `get_table_groups()` and `get_tables_info()`.
6. Opening a table runs the matching `*_view.php`, which checks `getTablePermissions()` and renders a `DataList`.
7. Saving a row runs the `*_dml.php` script. Calculated columns are refreshed by `ajax-update-calculated-fields.php` (HTTP) or `cli-update-calculated-fields.php` (CLI). Both call `calculated_fields()`.

Authentication is AppGini membership (session `Printafoil_Portal`), not WordPress users. Nothing in the portal include path loads `b2b/wp-load.php`.

## Data model

233 tables are registered. The menu grouping and captions are in `features.md`. Physical columns are the detail views and the SQL fragments in `get_sql_fields()` inside `incCommon.php`.

Membership tables used by code: `membership_grouppermissions`, `membership_userpermissions`. Admin code also uses the usual AppGini membership tables (groups, users, usersessions). Those definitions are the AppGini schema, created in the database, not a migration folder.

Calculated columns (every field whose value is a `SELECT` in `calculated_fields()`):

- `purchase_order`: `Subtotal`, `Total`, `approvedby`
- `po_items`: `price`
- `suppliers`: `supplierevalcompleted`, `approvedby`
- `supplierevaluation`: `approvedby`
- `finish_stock`: `price_unit`, `total_qty_finstock`, `total_qty_excstock`, `total_qty_new`, `total_qty_spoilstock`, `calc_total_qty_finished`, `calc_total_qty_excess`, `calc_total_qty_spoilage`, `calc_adjust_in_finished`, `calc_adjust_in_excess`, `calc_adjust_in_spoilage`, `calc_dispatch_qty`, `total_value_finstock`, `total_value_excess`, `total_value_all`, `old_sys_fs_stk_add`, `old_sys_ex_stk_add`, `old_sys_fs_stk_out`, `old_sys_ex_stk_out`, `old_sys_fs_total`, `old_sys_ex_total`
- `finish_stock_move`: `calc_stk_value`
- `colourstd`: `colourstdappstatus`
- `consumablesmanage`: `ReorderNotice`, `TotalAvailable`, `TotalAvailableMainBld`, `TotalAvailableNo12`, `TotalAvailableALL`
- `iso_non_conformance`: `ctn_percentage`
- `plates`: `QtyNEW`, `QtyDTPerror`, `QtyAdjArt`, `QtyRemadePress`, `QtyDamaged`, `QtyNoStanding`, `QtyTotalMade`
- `printingpflight`: `qty_finished`, `qty_excess`, `qty_spoils`, `calc_sum_prod_qty`, `qty_wip_percentage`
- `sales_enquiry_order`: `OrderedTotalAmount`, `TotCartonsDel`, `Qty_Ordered_v_Delivery`, `PercentageOU`
- `stk_item`: `Qty_Stock_Printafoil`, `Qty_Stock_PeterPaper`, `Qty_Stock_JakPaper`, `Qty_Stock_GerberPaper`, `Qty_Stock_PaperSmith`, `Qty_Stock_Total`
- `stk_laminate`: `Quantity`
- `dispatch_sys`: `qty_tot`, `qty_picked`, `btftotalqty`, `join_del_numbers`
- `inks_stock`: `qtyinkg`, `qtyinkg_2`, `qtyinkg_total`
- `production_finishing`: `totalqty`, `totalqty_1`, `fulltotalqty`
- `production_status`: `totalqty`
- `dispatch_pick`: `Qty_totalwithovers`, `pick_qty_all`, `dispatch_note_details`
- `dispatch_pick_qty`: `qty_picked`, `loose_qty_full`, `loose_qty_odd`, `pallets_total`, `qty_of_boxes`, `qty_of_box`, `qty_of_box1`, `qty_on_pallets`
- `new_stk_item`: location quantities and paid-in/out for Peter Paper, Jak Paper, Gerber Paper, and Paper Smith; `Qty_Stock_Printafoil`, `Qty_Stock_Total`, `lam_total_qty`, `lam_pricepersqm`, `lam_totalvalue_rand`, `bp_totalvalue_rand`
- `wip_hud`: per-operation counters (`op_mb_*`, `op_fd_store9_*`, `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_*`)
- `wip_movement` and `wip_operations`: `max_qty`

Formulas for the purchase order, stock roll-up, and consumable totals are in `business-rules.md`.

### Not in the catalogue

Generated view files whose `TableName` is absent from `getTableList()` (28):

`b2b_order_rfq`, `b2b_order_sys`, `b2b_order_sys_detail`, `customer_survey`, `dispatch_pick_label`, `group_client_1701`, `group_client_charlotte`, `new_stk_move_type`, `pnd_operator_name`, `pnd_product_detail_material`, `pnd_production_operation`, `pnd_production_operators`, `pnd_production_process`, `pnd_production_resource`, `pnd_production_transactions`, `pnd_resource_name`, `prod_activity`, `prod_operation_list`, `prod_start_stop`, `prod_wip_function`, `stk_grade`, `stk_material`, `wip_hud_pd`, `wip_hud_transact`, `wip_movement_pd`, `wip_operations_pd`, `wip_operations_transactions`, `wip_type_id`.

Opening one of these URLs still calls `getTablePermissions()` for that table name. If the membership tables have no row, access is denied. **Unknown** whether the physical tables remain in MySQL.

### Alternate views of catalogue tables

| File | Table |
|---|---|
| `finish_stock_client_view.php`, `finish_stock_full_view.php` | `finish_stock` |
| `wip_hud_cnc_view.php`, `wip_hud_completed_view.php`, `wip_hud_embos_view.php`, `wip_hud_foil_view.php`, `wip_hud_glue_view.php`, `wip_hud_onmachine_view.php`, `wip_hud_plan_view.php`, `wip_hud_q_view.php`, `wip_hud_q_cnc_view.php`, `wip_hud_q_handw_view.php`, `wip_hud_q_sorting_view.php`, `wip_hud_qty_view.php`, `wip_hud_sort_view.php`, `wip_hud_strip_view.php`, `wip_hud_wip_view.php` | `wip_hud` |

## Hooks that change behaviour

Most `hooks/<table>.php` files are the AppGini stub (empty header, footer, CSV, and batch-actions functions). Per-table filter pages such as `hooks/finish_stock_filter.php` are generated filter UI.

`hooks/__global.php`:

- `login_ok()` redirects group `client_Charlotte_Rhyse`.
- `login_failed()` inserts IP, time, username, and password into `log_failed_logins`.
- `child_records_config()` sets child grids to 20 records per page.
- `member_activity()` and `sendmail_handler()` are empty.

## WordPress application

| Item | Detail |
|---|---|
| Version | WordPress 6.1.1 (`b2b/wp-includes/version.php`) |
| Commerce | WooCommerce 7.0.0, requires WordPress 5.8 and PHP 7.2 |
| Rewrite | `b2b/.htaccess` `RewriteBase /b2b/` |
| Themes | 13 themes under `b2b/wp-content/themes/`. Active theme is a database option. **Unknown** |
| Plugins | Listed in `features.md`. Active plugins are a database option. **Unknown** |
| Uploads | `b2b/wp-content/uploads/` includes WooCommerce, PDF invoice, Contact Form 7, and user-registration log directories |
| Cron | `b2b/wp-cron.php` is stock WordPress. No custom cron job was identified |
| Background workers | None in the portal. WordPress Action Scheduler tables (`wp_actionscheduler_*`) are in the January dump, which is WooCommerce's queue. Whether it runs is **Unknown** |

No portal `.htaccess` is in git. Directory indexing and rewrite behaviour for the portal depend on the host. **Unknown**.

## Auth, mail, storage, monitoring

- Portal auth: AppGini membership session. Password storage for members is AppGini's (the admin hash in config is a bcrypt `$2y$` hash). Member password algorithm for ordinary users was not separately verified beyond that admin hash.
- Shop auth: WordPress, plus Ultimate Member and User Registration if active. Wordfence is vendored.
- Mail: portal SMTP as configured. WordPress mail is whatever plugin is active (MailPoet is vendored). **Unknown** which one sends shop mail.
- Storage: local disk `images/` for the portal; `b2b/wp-content/uploads/` for WordPress. No S3 or other object store is referenced in the portal config.
- Monitoring: Webalizer static reports for two months in 2023. No application log framework, APM, or health endpoint was found in the portal.
- Jobs: `cli-update-calculated-fields.php` can be run from the command line. No cron definition is in the repository. **Unknown** whether anyone schedules it.

## Security notes for reviewers

These are observations from the tree, not a penetration test.

- Passwords and the admin hash are committed in three PHP config files.
- Failed login attempts store the password in clear text.
- phpMyAdmin is inside the web tree.
- `xmlrpc.php` is the stock WordPress file under `b2b/`.
- The SQL dump is large, starts with `DROP DATABASE`, and includes application data and a WordPress file list. It should not be treated as a safe sample database.
- Customer and quality documents (artwork PDFs, purchase orders, HR and ISO files) are committed under `images/`.
- `DEBUG_MODE` is false.

## Development and testing

No test suite, linter config, or CI file was found. Regenerating the portal means regenerating from an AppGini project file. No `.axp` (AppGini project) file was found at the repository root during this pass. **Unknown** where the AppGini project file is kept. Without it, the generated PHP is the source of truth and hand edits in generated files can be overwritten if someone regenerates later.

`setup.php` is the AppGini first-run installer (database connection test and admin password). `config.php` already exists, so a normal request does not need to run setup.

## Deployment

No Dockerfile, no Elastic Beanstalk config, no deploy script. Evidence of a past host is the domain `printafoil.bluolive.co.za` and the home path `/home/bluolive/domains/printafoil.bluolive.co.za`. How a new server would be built is **Unknown / Requires Confirmation**.
