# PRD — dev.sabullion (SA Bullion WordPress)

Evidence date: 2026-10-17. Source: `git@bitbucket.org:overdrive/dev.sabullion.git`, `master` `c58febf` (2020-07-12).

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No formal PRD existed in the repository or in `silicon_ark/dev.sabullion/` before this file. Requirements below are recovered from code and from copy stored in the repo. They are not a new product decision.

WordPress content (pages, menus, Gravity Forms, WooCommerce products) is not in git. Requirements that depend on that content are marked **Unknown**.

Credential values are omitted. See the project plan.

## Product purpose

SA Bullion lets a person turn cash into physical 1 oz Krugerrands, keep those coins stored, and see the holding online. The public story offers two facilities, BullionGold and HasanaGold (a Halaal facility), plus a direct buy and sell of Krugerrands and an international minted-bar illustration.

**Documented** in `README.md`, `content/home-content.txt`, and `content/about-story.txt`. The README calls the repository "New SA Bullion WordPress website."

The WordPress front is the public site at this commit. A custom PHP portal under `dashboard/` is the signed-in product. Provenance: **Implemented**.

## Users and roles

| Role | Where it appears | What the code lets them do | Provenance |
|---|---|---|---|
| Anonymous visitor | WordPress theme templates | Read facilities, gold price, calculators, buy and sell pages, partnership, downloads, contact | **Implemented** as templates. Which pages are published is **Unknown** |
| Applicant | `dashboard/login.php?client/details/sa-natural` and sibling signup templates; `sab-api` signup scripts | Start a client or IFA application. Client paths: SA natural, SA legal, non-SA natural, non-SA legal. IFA paths: natural, legal | **Implemented** |
| Unapproved client | `dashboard/index.php` when `SYSTEM` is `UNAPPROVED-CLIENT` | Forced onto `templates/client/docs.php`. Login sets this when the email is in `web_client_registration` and not yet in `web_client_detail` | **Implemented** |
| Client | `dashboard/templates/client/sidebar.php` | Portfolio overview, summary statement, detailed statement, documents, password | **Implemented** |
| IFA | `templates/ifa/sidebar.php` | Own portfolio, client list, saving calculator, investment calculator, add assistant | **Implemented**. Add-team route exists and is disabled (`&& false`) |
| House | `templates/house/sidebar.php` | Overview, advisors/team, clients, calculators | **Implemented** |
| Global admin | `templates/global_admin/sidebar.php`; login against `web_global_admin_detail` | Advisors and clients pages | **Implemented**. The full menu was not compared line by line with house |
| WordPress administrator | `wp-admin` | Standard WordPress and plugin admin, if an account exists | **Implemented** by core. Who holds the account is **Unknown** |
| Shopper | WooCommerce 4.1.1 plus PayGate PayWeb3 and a WooPay PayGate plugin | Would check out if WooCommerce is active and products exist | Plugins are **Implemented** in the tree. Activation and catalogue are **Unknown** |

No custom WordPress role is registered by `sab-api`. Portal roles are session strings, not WordPress roles.

## Main workflows

### W1 — Read the public site

Trigger: a visitor opens the host.

1. Apache serves a real file or directory when one exists. Otherwise `.htaccess` sends the request to `/index.php`.
2. `index.php` boots WordPress 5.1.5 with themes enabled.
3. The active theme is a database option. The only theme in git is `sabullion`. Whether it is active is **Unknown**.
4. Page templates in the theme include Our Story, Gold Price, Buy, Buy Request, Sell, Seller Details, Invest SA, Invest International, illustrations (investment, capital, merged, minted bar, international), Partnership, FAQs, Contact, Documents, Download, Schedule, Services, Deal SA, Wholesalers.

Provenance: **Implemented**.

The visitor-facing navigation is whatever menu is stored in WordPress. **Unknown**.

### W2 — See the gold price

Trigger: a visitor opens the page slug `the-gold-price`, if that page exists.

`sab-api` enqueues Morris, Raphael, and `js/gold-pricing.js` on that slug. The script calls `GET /wp-json/api/v1/gold/index/`. With no query, the handler reads the latest `gold_price` row and returns ZAR, USD, EUR, and GBP gold prices, plus a Krugerrand price at gold plus 3 percent. With `currency` and `period`, it returns the series between that date and today.

Provenance: **Implemented**. The page slug must be published. **Unknown** whether it is.

### W3 — Run a calculator and email a PDF

Trigger: a visitor uses a page whose slug is one of `south-african-gold-solutions`, `international-gold-solutions`, `investment-illustration`, `capital-investment-illustration`, `merged-investment-illustration`, or `minted-bar-illustration`.

`sab-api` enqueues the matching script. Scripts call REST routes under `/wp-json/api/v1/calc/`, `/altcalc/`, and `/calc/minted-bar-index/`. Upload routes store the illustration and email it. The minted-bar script forces the currency cookie to `USD` (`js/minted-bar-investments-calc.js`). That matches commit `743280c` ("hardcode minted bar calc to use USD").

The investment upload rejects an amount that is not greater than the latest gold price plus 1 percent plus 1000 currency units (`api/upload/upload-minted-bar-investment.php`). The error text says "insufficient to purchase a Krugerrand" even on the minted-bar upload.

Every `sab-api` REST route sets `permission_callback` to return true. Provenance: **Implemented**. There is no logged-in check on these routes.

Gravity Forms is vendored (2.4.11). A comment in the minted-bar script says "Gravity Form in WP - Investment Potential". Form definitions are not in git. **Unknown** which form IDs are published. A filter on `gform_next_button_4` is commented out in `sab-api.php`.

### W4 — Buy or sell a Krugerrand

Trigger: a visitor opens slug `buy-gold-from-us`, `buy-request`, `sell-gold-to-us`, or `seller-details`.

`sab-api` enqueues `krugerrand-calc-overview.js` or `krugerrand-sell.js`. Upload routes are `GET /wp-json/api/v1/upload/krugerrand/` and `GET /wp-json/api/v1/upload/krugerrand/sell/`.

Older PHP pages remain at `krugerrand/buy/index.php` and `krugerrand/sell/index.php`, plus `index-old.php`, `index-bkp.php`, and `index.php20191119`. `scheduler/index.php` is titled "Schedule Drop Off" and loads the legacy template `/krugerrand/scheduler/scheduler.php`. Page cache in that file is `$cache = false`.

Provenance: **Implemented**. Which of the WordPress templates or the legacy PHP URLs the public menu uses is **Unknown**.

### W5 — Open an account or sign in

Trigger: the visitor uses the theme header or footer.

Header and footer link "Open an Account" to `https://sabullion.overdriveqa.co.za/dashboard/login.php?client/details/sa-natural` and "Sign In" to `https://sabullion.overdriveqa.co.za/dashboard/login.php`. Partnership template links the same actions to `https://sabullion.co.za/...`. Provenance: **Implemented**, and the two hosts disagree.

`dashboard/login.php` keeps an existing global-admin session when the referrer contains `/webadmin` and `GLOBAL_ADMIN->LoginStatus` is `Success`. Otherwise it presents the login templates.

`dashboard/api/login/index.php` accepts JSON `request=login` with email and password. The password is compared as `md5($password)` against `applogin.password`. The line that would prefix a salt is commented out. Lookup order after a password match:

1. `web_ifa_company_detail` → session `HOUSE`
2. `web_ifa_advisor` → session `IFA`
3. `web_client_detail` → session `CLIENT`
4. `web_client_registration` → session `UNAPPROVED-CLIENT`
5. `web_global_admin_detail` → session `GLOBAL_ADMIN`
6. Otherwise "Your account has not been created yet"

An authorization-key check at the top of `dashboard/api/login/login.php` is commented out. Provenance: **Implemented**.

Session files are stored in `dashboard/SESSION`. `session.gc_maxlifetime` is 259200 seconds (3 days). Timezone is `Africa/Johannesburg`.

### W6 — Use the portal

Trigger: a session with `$_SESSION["USER"]->SYSTEM` opens `dashboard/index.php`. Missing session redirects to `login.php`.

`dashboard/helpers/requests.php` defaults `page` to `myportfolio` and switches templates by `SYSTEM` and `page`. Client pages include portfolio, summary, statement, documents, settings, password, gold price. IFA and house pages include clients, team, calculators. Global admin pages include clients and advisors. An unapproved client is forced to the documents template.

Provenance: **Implemented**.

### W7 — Remind an unapproved client about documents

Trigger: something requests `dashboard/helpers/cron.documents.reminder.php`. Nothing in git schedules it.

The file comment says reminders should go out every 48 hours to registrations whose status is not like `%Approved%` and whose document file name is null. The body runs that query and `var_dump`s rows. No mail send is in the file. Provenance: **Implemented** as an unfinished script. **Inferred** that the intended rule is the comment. Whether it ever ran is **Unknown**.

`dashboard/helpers/cron.daily.php` deletes generated pdf, png, and xls files when `PROCESS_ACTIVE_DIR_CLEANUP_ENABLED` is true. Nothing in git shows the scheduler. **Unknown**.

### W8 — Shop (WooCommerce)

Trigger: a visitor opens a WooCommerce URL, if the plugin is active.

WooCommerce 4.1.1, PayGate PayWeb3 1.3.1, WooPay PayGate 1.1.4, a side-cart plugin, and WooCommerce Checkout Manager are in `wp-content/plugins/`. No product, gateway credential, or order flow is configured in git. Provenance: **Implemented** as vendored code. Use on this site is **Unknown**.

## Functional requirements

### FR-001 — Public marketing site

The system serves a WordPress site whose theme can render SA Bullion story, facility, partnership, contact, document, and download templates.

- Actor: anonymous visitor.
- Trigger: HTTP request that is not a real file.
- Output: theme HTML.
- Validation: WordPress template hierarchy. Missing theme file is a Sage `trigger_error` fatal on include.
- Evidence: `index.php`, `.htaccess`, `wp-content/themes/sabullion/template-*.php`.
- Implementation: **Implemented**. Published pages: **Unknown**.

### FR-002 — Two named facilities

Copy presents BullionGold and HasanaGold. HasanaGold is the Halaal facility. Code and filenames still say Baraka.

- Evidence: `content/home-content.txt`, `content/baraka-*.txt`, `content/bullion-*.txt`, theme partials `partials/hasana.php` and `partials/bedrock.php`.
- Implementation: **Documented** and **Implemented** as templates and copy. Database pages that use them: **Unknown**.

### FR-003 — Gold price and Krugerrand price

The latest `gold_price` row is shown in ZAR, USD, EUR, and GBP. The Krugerrand figure is that gold price plus 3 percent.

- Actor: visitor, or any caller of the REST route.
- Inputs: optional `currency`, optional `period`.
- Error: missing currency or period when one of the pair is expected returns `success: false` and a message. The checks run after a combined `isset` test, so the missing-field branches are unreachable when the outer condition failed. Provenance: **Implemented**.
- Evidence: `wp-content/plugins/sab-api/api/gold/index.php`.

### FR-004 — Investment, savings, and minted-bar illustrations

The visitor can submit name, email, amount, currency, and advisor fee. The system stores a row, assigns a reference, and can email a PDF.

- Minted-bar reference format: `SABI` plus a 6-digit number from `MAX(sbic_reference_number)+1` on `sb_invst_minted_bar_calc`.
- Minted-bar currency in the page script is forced to USD.
- Minimum amount rule: see business rules.
- Evidence: `sab-api` upload scripts and `js/minted-bar-investments-calc.js`. Commit `c58febf` adds an index graph to the investment calculator.
- Implementation: **Implemented**.

### FR-005 — Krugerrand buy and sell requests

The visitor can complete a buy request or a sell wizard (sell to us, seller details, review) and upload the request.

- Evidence: theme templates `template-buy.php`, `template-sell.php`, `template-seller-details.php`; legacy `krugerrand/buy` and `krugerrand/sell`; REST upload routes.
- Implementation: **Implemented**.

### FR-006 — Account application

A person can open the SA-natural client application from the public header. Other entity templates exist for SA legal, non-SA natural, non-SA legal, and IFA natural and legal.

- Evidence: theme header; `dashboard/templates/login/`; `sab-api/api/signup/`.
- Implementation: **Implemented**. Which template the query string `client/details/sa-natural` selects is the login front controller. Confirmation of every query variant was not exhaustively traced. **Inferred** from the folder names and the header URL.

### FR-007 — Portal sign-in and role home

A person with an `applogin` row and a matching profile row receives a session for house, IFA, client, unapproved client, or global admin, and sees that role's sidebar.

- Inputs: JSON email and password.
- Validation: both fields required. Wrong password returns "Invalid email address or Password". Password match with no profile returns "Your account has not been created yet".
- Output: JSON `success` and `userType`.
- Evidence: `dashboard/api/login/index.php`, `dashboard/index.php`, sidebars.
- Implementation: **Implemented**.

### FR-008 — Unapproved client sees documents only

When `SYSTEM` is `UNAPPROVED-CLIENT`, `dashboard/index.php` sets the page template to the documents page and the new-client sidebar.

- Implementation: **Implemented**.

### FR-009 — Partner programme copy

The partnership page describes working with IFAs: a 2 percent a year client fee, of which 25 percent is distributed to the partner, billed monthly in arrears by debit order.

- Evidence: `content/partner-page.txt`, `template-partnership.php`.
- Implementation: **Documented**. Whether the portal calculates the 25 percent split was not found in the login and sidebar pass. **Unknown** in code beyond the copy.

### FR-010 — Document reminder

A script selects unapproved registrations with a null document file name. It does not send mail.

- Implementation: **Partial**. See W7.

### FR-011 — Shop checkout

PayGate and WooCommerce are vendored. No requirement in git says what is sold.

- Implementation: **Unknown** / plugins only.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure | Provenance |
|---|---|---|---|---|---|---|
| MySQL access DB and web DB | Out | Page and REST | Gold prices, VAT, logins, clients, IFAs, calculator rows | Username and password in config, per host | PDO exception; a `dbconn-failed.php` file exists. Exact user-facing text was not traced | **Implemented** |
| Office 365 SMTP `smtp.office365.com:587` TLS | Out | Mail from the non-lamp mailer | Calculator and application email. From `business@sabullion.co.za` | SMTP password in `dashboard/config/mailer.php` | PHPMailer exception. Peer verification is disabled | **Implemented** |
| `lamp` sendmail | Out | Same mailer when host is `lamp` | Same | none in file | debug output HTML | **Implemented** |
| Bitbucket Pipelines `git-ftp` | Out | Push to `master` | Whole tree | `FTP_USER`, `FTP_PWD`, `FTP_HOST` | Pipeline failure. `git ftp init` is commented out; `git ftp push` remains | **Implemented** |
| Rand Refinery, SARB, LBMA, Lloyd's, Nedbank | Named in copy | Not an HTTP client in the files read | Marketing and FAQ claims | n/a | n/a | **Documented** only |

Gold prices are read from table `gold_price`. No feed client that writes that table was found in this pass. How rows arrive is **Unknown**.

## Constraints

- PHP timezone for the dashboard is `Africa/Johannesburg`.
- VAT is read from `sysVat.dblVat` at bootstrap. A 2018 comment says the country rate moved from 14 percent to 15 percent and the code must use the table. Provenance: **Implemented** read; the rate itself is data.
- Public REST under `api/v1` is unauthenticated.
- Portal passwords are stored and compared as unsalted MD5.
- `wp-config.php` is gitignored. WordPress database settings for the CMS itself are **Unknown**.
- Theme Composer constraint is PHP `>= 5.4`. Comments mention PHP 7 and 7.2. The runtime that was certified is **Unknown**.
- No automated test suite is in the repository.

## Planned behaviour

No roadmap, TODO issue list, or "planned" document is in the repository. Commented-out facility routes in `helpers/requests.php` (2019-01-28, "new URL's") are retired code paths, not a written plan. Provenance: **Unknown** as future work.

## Discrepancies

- Header and footer account links use the QA host. The partnership template uses `sabullion.co.za`.
- `content/terms-of-use.txt`, `content/privacy-policy.txt`, `content/popi-notice.txt`, and `content/client-mandate.txt` are Harry's / HF Global templates, not SA Bullion legal text.
- Partner FAQ says an annual vault audit by Deloitte, and another answer says a quarterly audit by PwC (`content/partner-faq.txt`). Both cannot be confirmed from code.
- `sell-krugerrand-overview.txt` contains HasanaGold faith copy inside the sell overview file.
- The onboarding scan listed `sonar-project.properties` and `sonarqube_scan.bat`. They are not in `c58febf`.
- Silicon Ark `sabullion/` and `sabullion_current_live/` describe other remotes. Do not merge their later behaviour into this commit.
