# PRD — sabullion_current_live (SA Bullion WordPress snapshot)

Evidence date: 2026-10-05. Source: `git@bitbucket.org:overdrive/sabullion_current_live.git`, `master` at `6cbf6777` (2022-10-01).

There is no prior PRD in the repository or in Silicon Ark for this remote. Requirements below were recovered from the theme, the `sab-api` plugin, plugin headers, and the single commit. Provenance is marked on each item.

Pages, products, orders, form entries, and the active-plugin list are stored in WordPress options and tables. They are not in Git. Requirements that depend on that data are **Unknown / Requires Confirmation**.

Secrets in `wp-content/themes/sabullion/config/mailer.php` are not copied into this document.

## Product purpose

The tree is a WordPress site whose theme constant is `Sa Bullion` and whose public links point at `https://sabullion.co.za`. Visitors can read marketing and content templates (gold price, buy and sell, investment illustrations, news categories) and, when WooCommerce is active, buy products from a shop. A custom plugin exposes calculators, a gold-price chart, file uploads, and illustration emails.

Provenance: **Implemented** in theme templates, `functions.php`, and `sab-api.php`. A written statement of the business goal was not found. Provenance: **Unknown**.

The parent theme header still says "Rocket Theme", author Grant Barnard, Mustard (`https://mustard.agency`), "a custom theme built on Sage 8". `package.json` is Sage 8.5.2. The SA Bullion behaviour is code added on top of that starter. Provenance: **Implemented**.

## Users and roles

No custom WordPress role is registered in the theme or in `sab-api.php`.

| Role | What the code allows | Evidence | Provenance |
|---|---|---|---|
| Public visitor | Reads pages that do not require login. Calls `sab-api` REST routes; every registered route sets `permission_callback` to a function that returns `true` | `sab-api.php` | **Implemented** |
| Shopper who is not logged in | Opening the cart or checkout redirects to `/my-account` | `functions.php` `wpse_131562_redirect` | **Implemented** |
| Logged-in customer | WooCommerce account. Registration sends them to `/my-account/edit-account/`. After that save, a notice says a password was emailed and they are sent to the My Account page. The My Account dashboard URL `/my-account/` redirects to `/shop` | child `functions.php`; parent `account_redirect` | **Implemented** |
| WordPress administrator | Standard `wp-admin`, if they have an account | WordPress core in `wp-admin/` | **Implemented** by core. Who holds an account is **Unknown** |
| Editor, author, contributor, subscriber | Standard WordPress capabilities | WordPress core | **Implemented** by core. Which roles are in use is **Unknown** |
| Retail coins mailbox | CC on illustration emails when `SERVER_NAME` is `sabullion.co.za` | `api/email/index.php`, `minted-bar-email.php` | **Implemented** |

Sign-in screens that are in the tree are WooCommerce My Account templates in the child theme (`form-login.php`, lost-password, reset-password). No separate project login was added. Provenance: **Implemented** as template overrides. Whether those templates are the ones WooCommerce loads depends on the active theme. **Unknown** which theme is active.

## Main workflows

### WF-001 — Read the public site

Trigger: a visitor opens a URL.

Behaviour that is in code:

1. WordPress would load `index.php` at the document root. That file is not in this commit. Provenance: **Unknown** how the live host boots.
2. The active theme is a database option. The only project themes are `sabullion` and `sabullion-child`. The child names the parent `sabullion`.
3. The parent registers one menu location, `primary_navigation`, and sidebars `sidebar-primary` and `sidebar-footer`. `display_sidebar()` returns false for the front page, pages, singles, archives, home, and 404, so the primary sidebar is suppressed on those views.
4. The child enqueues the parent `style.css` and then its own `style.css`, and enqueues `js/functions.js` with `WPURLS.siteurl`.
5. `sab-api` always enqueues `css/sab-api.css` and SweetAlert. Calculator scripts load only on the page slugs listed in FR-006.

What the visitor reads (page titles, Elementor layouts, menus) is **Unknown**.

### WF-002 — Shop for a product

Trigger: a visitor opens `/shop/` or a product, with WooCommerce active.

1. If the user agent contains `Trident/7.0` (Internet Explorer 11) and the request URI is `/shop/`, the theme prints a script that navigates to `https://sabullion.co.za/shop-not-compatible-with-internet-explorer/`.
2. In-stock products print "In Stock". Out-of-stock products print "Awaiting Stock".
3. The loop add-to-cart button is removed. A link labelled "Buy" or, when out of stock, "Read More" points at the product permalink. The single-product button text is "Buy" except the out-of-stock non-variable case, which uses "Read more".
4. The product editor has three extra price fields: Metal Value, Premium Value, VAT Value. They are stored as post meta `metal_value`, `premium_value`, and `vat_value`.
5. Cart heading text is "{n} products in your cart" plus a link "GO BACK TO PRODUCTS" to `https://sabullion.co.za/shop/`. The breadcrumb home URL is that same shop URL. On a single product the second breadcrumb label is set to an empty string.
6. Cart and checkout require a logged-in user (WF-003).

Provenance: **Implemented** in `wp-content/themes/sabullion/functions.php`. Whether the shop page slug is `/shop/` in the database is **Inferred** from the hardcoded URI checks. **Requires Confirmation**.

### WF-003 — Account, cart, and checkout

Trigger: a visitor opens cart, checkout, or My Account.

1. Anonymous users on `is_cart()` or `is_checkout()` are redirected to `/my-account` and the request exits.
2. A logged-in user whose request URI is exactly `/my-account/` is redirected to `/shop`.
3. Request URIs `/my-account/orders/`, `/my-account/edit-address/`, `/my-account/edit-account/`, and `/my-account/my-wishlist/` are left in place when the user is logged in. The `else` branch of `account_redirect` does not redirect other account URLs either; only the exact dashboard URI redirects. Provenance: **Implemented**.
4. WooCommerce registration redirect sets `$_SESSION["isRegistering"]` and returns `/my-account/edit-account/`. The child theme calls `session_start()`.
5. On `woocommerce_save_account_details`, if that session flag is true, the flag is cleared, a notice says the account was created and a password was sent, and the browser is sent to the My Account permalink.
6. While the session flag is true, `woocommerce_account_menu_items` returns an empty menu. Otherwise the menu order is Dashboard, Orders, Download, Addresses, Account Details, Logout.
7. `woocommerce_created_customer_notification` for the new-account email is removed. `send_password_change_email` is forced false.
8. Every WooCommerce email header is given `Cc: <business@sabullion.co.za>`.
9. Order notes are removed (`woocommerce_enable_order_notes_field` false, and `order_comments` unset). The place-order button label is `SUBMIT ORDER`. The "Billing & Shipping" string is replaced with "Billing Details" when that exact translated text is seen.
10. Checkout reloads when the selected payment method changes.

Provenance: **Implemented**.

### WF-004 — Fees on the order

Trigger: WooCommerce calculates cart fees, or the cart totals template renders.

See business rules BR-010 and BR-011. The code adds a "Card Fee" when the gateway id is `cod` or the session payment is `cod`, and a "Delivery Fee" when the chosen shipping rate id is `free_shipping:1`. Provenance: **Implemented** as fee calculations. Which gateways are enabled in WooCommerce settings is **Unknown**. Treating the Cash on Delivery gateway id as a card fee is what the code does. Whether that matches the intended tender is **Requires Confirmation**.

### WF-005 — Gold price and Krugerrand figure

Trigger: `GET /wp-json/api/v1/gold/index/` (route `api/v1` `/gold/index/`). The route is public.

`gold_index` loads `config/site.php`, which opens PDO connection `$SA`.

- If `currency` and `period` are both present, it reads `gold_price` rows from `period` through `CURDATE()` and returns date and value lists. The currency value is placed into the SQL select list.
- Otherwise it reads the latest `gold_price` row and returns rounded `ZAR`, `USD`, `EUR`, and `GBP` gold figures. Each Krugerrand figure is that gold figure plus 3 percent.

Provenance: **Implemented** in `wp-content/plugins/sab-api/api/gold/index.php`. The table is not created by a migration in this repository. Provenance of the table shape: **Inferred** from the query. Whether the table still exists is **Unknown**.

A second route, `GET /wp-json/api/v1/gold/alt/`, calls `gold_alt`. Its body was not line-reviewed in this pass beyond the route registration.

### WF-006 — Calculators and illustration email

Trigger: a public GET (or, for `/functions/`, GET or POST) on the `sab-api` routes, or a visit to a page slug that enqueues a calculator script.

Page slugs that load scripts (`sab_api_scripts`):

| Slug | Script intent from the file name |
|---|---|
| `the-gold-price` | Morris chart, `gold-pricing.js` |
| `buy-gold-from-us`, `buy-request`, `seller-details` | `krugerrand-calc-overview.js` |
| `buy-silver-from-us`, `silver-buy-request`, `silver-seller-details` | `silver-krugerrand-calc-overview.js` |
| `identity-verification` | `customer-file-upload.js` |
| `sell-gold-to-us`, `seller-details` | `krugerrand-sell.js` |
| `sell-silver-to-us`, `silver-seller-details` | `silver-krugerrand-sell.js` |
| `international-gold-solutions` | minted-bar investment calc |
| `south-african-gold-solutions` | savings calc |
| `minted-bar-illustration` | minted-bar illustration overview |
| `investment-illustration` | savings illustration overview |
| `capital-investment-illustration` | capital illustration overview |
| `merged-investment-illustration` | merged illustration overview |

Provenance: **Implemented** as `is_page()` checks. Those slugs must exist as WordPress pages for the scripts to load. The pages are not in Git. **Unknown** whether they exist.

Registered REST routes, all with `permission_callback` returning true:

| Method | Route | Callback |
|---|---|---|
| GET | `/api/v1/gold/alt/` | `gold_alt` |
| GET | `/api/v1/gold/index/` | `gold_index` |
| POST | `/api/v1/upload/customer-file/` | `upload_shop_customer_file` |
| GET | `/api/v1/upload/customer-file-status/` | `get_customer_file_status` |
| POST | `/api/v1/upload/customer-id-file/` | `upload_shop_customer_id_file` |
| GET | `/api/v1/calc/index/` | `calc_index` |
| GET | `/api/v1/calc/silver-index/` | `silver_calc_index` |
| GET | `/api/v1/altcalc/index/` | `investment_calc_index` |
| GET | `/api/v1/calc/minted-bar-index/` | `minted_bar_index` |
| GET | `/api/v1/email/index/` | `email_index` |
| GET | `/api/v1/email/silver-index/` | `silver_email_index` |
| GET | `/api/v1/email/minted-bar-email/` | `minted_bar_email` |
| GET | `/api/v1/upload/krugerrand/` | `upload_krugerrand` |
| GET | `/api/v1/upload/minted-bar-investment/` | `upload_minted_bar_investment` |
| GET | `/api/v1/upload/savings/` | `upload_savings` |
| GET | `/api/v1/upload/investment/` | `upload_investment` |
| GET | `/api/v1/upload/krugerrand/sell/` | `upload_krugerrand_sell` |
| GET | `/api/v1/upload/krugerrand/silver-sell/` | `upload_silver_krugerrand_sell` |
| GET and POST | `/api/v1/functions/` | `ajax_functions` |

`GET /api/v1/pdf-and-email/download/` is commented out and the matching `require_once` is commented out. Provenance: **Implemented** as disabled.

Illustration email (`email_index`), when it sends:

- Subject `SA Bullion Investment Overview - Reference Number ({ref})`.
- Body greets `name` and `last_name`, attaches the wording "Please find your SA Bullion Investment Illustration attached", and links to `https://sabullion.co.za/dashboard/login.php?client/details/sa-natural`.
- Sign-off is "The Communications Team", "SA Bullion Investor Services", telephone `021 447 3240`.
- On `sabullion.co.za`, CC `coins@sabullion.co.za` and BCC `imran.obrien@gmail.com`. On `lamp`, BCC `rachel@overdrive.co.za`.

Provenance: **Implemented** in `api/email/index.php`. The same telephone and application link appear in `minted-bar-email.php`.

### WF-007 — Identity document and utility-bill shortcodes

Trigger: a post contains `[show_loggedin_as]` or `[show_id_loggedin_as]`.

If a user is logged in, the shortcode prints hidden inputs with that user's ID, first and last name, and email, under field names `utility_bill_*` or `identity_document_*`. Provenance: **Implemented** in `functions.php`. Where the shortcodes are placed is **Unknown**.

### WF-008 — Publish content

Trigger: a staff user uses `wp-admin`.

The parent theme ships category templates `category-company-news.php`, `category-gold-nuggets.php`, `category-goldnuggets.php`, `category-position-papers.php`, `category-press-coverage.php`, `category-press-releases.php`, `category-quarterly-report.php`, `category-updates.php`, and `category-videos.php`, plus the page templates listed in `features.md`. Custom Post Type UI is vendored. Which post types and categories exist in the database is **Unknown**.

Elementor 3.7.7 and Elementor Pro 3.7.7 are in the tree. Layouts they store are in the database and in `wp-content/uploads/elementor`. Those upload files were not opened.

## Functional requirements

### FR-001 — Site identity

The theme defines `SITENAME` as `Sa Bullion`. Mail From is `business@sabullion.co.za` with the display name `SA Bullion`. Email artwork and shop links use `https://sabullion.co.za`.

Provenance: **Implemented**.

### FR-002 — Two database connections

On theme load, `config/site.php` includes `config/dbconnect.php`, which opens:

- `$SA` with `DB_ACCESS_HOST`, `DB_ACCESS_USER`, `DB_ACCESS_PASSWORD`, `DB_ACCESS_NAME`
- `$DBH` with `DB_WEB_HOST`, `DB_WEB_USER`, `DB_WEB_PASSWORD`, `DB_WEB_NAME`

A comment dated 2018-12-10 calls `$DBH` the web database. Both use PDO MySQL and `ERRMODE_EXCEPTION`. On failure the exception message is echoed.

Neither constant is defined in the theme or in `sab-api`. `wp-config.php` is not in the commit. Provenance: **Implemented** as reads of undefined-in-git constants. Where they are set is **Unknown / Requires Confirmation**.

### FR-003 — VAT rate from `sysVat`

After the connection opens, `site.php` runs `SELECT dblVat from sysVat` on `$SA` and stores `round(dblVat, 2)` in `$SystemSettings["VAT"]`. A comment says VAT moved from 14% to 15% on 2018-04-01 and that the code should use this column. The query is the behaviour. Provenance: **Implemented**. The row contents are **Unknown**.

### FR-004 — Shop requires an account

Anonymous cart and checkout redirect to `/my-account`. Provenance: **Implemented**.

### FR-005 — Product price split

Administrators can store metal, premium, and VAT amounts on a product. Provenance: **Implemented** as admin fields and meta. The front end was not shown, in the functions that were read, to add those three numbers into the WooCommerce price. Whether the shop displays them is **Unknown**.

### FR-006 — Calculator scripts follow page slugs

See WF-006. Provenance: **Implemented**.

### FR-007 — Public REST API

Every `sab-api` route allows unauthenticated access. Provenance: **Implemented**.

### FR-008 — Krugerrand display price

When the latest gold row is used, Krugerrand ZAR, USD, EUR, and GBP equal the gold figure plus 3 percent, rounded to 2 decimals. Provenance: **Implemented**.

### FR-009 — Illustration email

See WF-006. Provenance: **Implemented**.

### FR-010 — Order emails carry a bank PDF

`woocommerce_email_attachments` appends `https://sabullion.co.za/wp-content/uploads/2022/03/FNB-Bank-Confirmation.pdf` to every WooCommerce email. Provenance: **Implemented** as a URL string. Whether that file is in the `uploads/2022` tree was not confirmed file-by-file. **Unknown** if the object exists.

### FR-011 — Logged-in identity fields

See WF-007. Provenance: **Implemented**.

### FR-012 — South African English files

`wp-content/languages` contains `en_ZA.po`, `en_ZA.mo`, admin packs, and JSON translation files. Provenance: **Implemented** as files. `WPLANG` is not in Git because `wp-config.php` is absent. Whether the site locale is `en_ZA` is **Unknown / Requires Confirmation**.

## Integrations

| System | Direction | Trigger | Data | Auth in repo | Failure |
|---|---|---|---|---|---|
| MySQL `$SA` | Out | Theme load; gold and calculator code | VAT; `gold_price` rows | Constants not in Git | PDO exception message echoed |
| MySQL `$DBH` | Out | Theme load | Comment: web database. Call sites were not fully traced | Constants not in Git | Same |
| Office 365 SMTP `smtp.office365.com:587` TLS | Out | Mailer default branch | From `business@sabullion.co.za` | Username in Git. Password in Git, omitted here | Not handled beyond PHPMailer |
| `lamp` sendmail | Out | `SERVER_NAME` is `lamp` | Same mailer object | None | Debug output set to HTML |
| `https://sabullion.co.za/dashboard/login.php` | Link only | Illustration email body | Application wizard URL | None in this repo | Not a call. The file is not in this commit |
| Google Analytics `UA-1288630-36` | Out from browser | If `analyticstracking.php` is included | Page view | Tracking id in the file | Not applicable |
| WooCommerce, Elementor, Gravity Forms, HubSpot, Moosend, Facebook for WooCommerce, Jetpack, Wordfence, Yoast, YITH, PDF invoices | In process | If the plugin is active | Plugin-specific | Settings in the database | **Unknown** which are active |
| FNB | File URL only | WooCommerce email attachment | PDF URL under uploads | None in this code | **Unknown** |

No payment-gateway class was found in the custom theme. WooCommerce Payments 4.4.0 is vendored (`readme.txt` stable tag) but its directory has no root plugin PHP file (`readme.txt`, `templates/`, `vendor/`, and an Apple Pay domain file). `woo-eftsecure-gateway` is an empty file, not a plugin directory. Provenance: **Implemented** as those tree entries. Which gateway charges cards is **Unknown**. The fee code assumes a gateway id `cod`.

## Constraints

- Cart and checkout are login-gated in the theme.
- REST calculator and upload routes are not login-gated.
- Root WordPress bootstrap files are not in the commit, so this Git tree is not a complete document root.
- Database credentials for WordPress itself (`DB_NAME` and the rest of `wp-config.php`) are not in Git.
- The theme `composer.json` allows PHP 5.4. Several vendored plugins declare higher floors. The runtime version is **Unknown**.
- One Git commit. There is no branch that shows a later release.

## Error and edge behaviour found in code

- Missing `currency` or `period` on the gold history query returns JSON `success: false` with "Missing currency to make calculation" or "Missing time period to make calculation". Those checks sit inside a condition that already requires both parameters, so they do not run when a parameter is absent. The absent case uses the latest gold row instead. Provenance: **Implemented**.
- PDO connection failure echoes `$e->getMessage()`.
- `destroy_sessions` on `wp_logout` calls `WP_Session_Tokens::get_instance( $user_id )` without setting `$user_id`. Provenance: **Implemented**. The result at runtime is **Unknown**.
- Card-fee and delivery-fee code is duplicated. See `business-rules.md`. The amount a shopper is charged when more than one hook runs was not executed. **Unknown**.
- The delivery-fee branch `subtotal >= 150000` and the `else` branch compute the same amount.
- Child theme file `woocommerce/cart/cart.php2000` sits beside `cart.php`. WordPress will not load the `php2000` name as a template. Provenance: **Implemented** as an extra file.

## Acceptance notes for a future change

A change to shop fees, login redirects, calculator routes, or VAT loading can be checked against the file paths in this PRD. It cannot be checked against live pages, prices, or plugin activation until someone confirms the database and the host.
