# PRD — sabullion (SA Bullion)

Evidence date: 2026-09-28. Source: `git@bitbucket.org:overdrive/sabullion.git`, `master` `a5cd347` (2024-03-12).

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

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

`origin/development` (`fcabece`, 2021-03-01) is a separate WordPress site and is not the behaviour described here, except where a discrepancy says so.

## Product purpose

SA Bullion lets a person turn cash into physical Krugerrands, keep title to those coins in a vault, and see the holding online. The public story also offers a Halaal facility (HasanaGold) and a direct buy/sell of coins.

**Documented** in `content/home-content.txt` and `content/about-story.txt`. The operating company string in the mandate template is "SA Bullion I.S." (`templates/all/client-mandate.php`).

## Users and roles

| Role | Where it appears | What the code lets them do | Provenance |
|---|---|---|---|
| Anonymous visitor | Public `index.php` pages | Read facilities, pricing, gold price, magazine link, downloads, partner page; start buy/sell and calculators | **Implemented** |
| Applicant | `dashboard/templates/login/client/` and `dashboard/api/signup/` | Submit a client or IFA application | **Implemented** |
| Unapproved client | `dashboard/index.php` when `SYSTEM` is `UNAPPROVED-CLIENT` | Forced to the documents page | **Implemented** |
| Client | Dashboard sidebar `templates/client/sidebar.php` | Portfolio overview, summary statement, detailed statement, PDF downloads | **Implemented** |
| IFA | `templates/ifa/sidebar.php` | Own portfolio, client list, saving calculator, investment calculator | **Implemented** |
| House | `templates/house/sidebar.php` | Overview, advisors, clients, calculators | **Implemented** |
| Global admin (portal) | `templates/global_admin/sidebar.php` | All advisors, all clients | **Implemented** |
| Staff user | `webadmin/`, `sysUser` joined to `sysSecurityGroup` | Back-office modules allowed by the security group | **Implemented**. The group names live in the database and were not read |
| Web-admin session entering the portal | `dashboard/login.php` | If the referrer contains `/webadmin` and `GLOBAL_ADMIN->LoginStatus` is `Success`, the portal session is not destroyed | **Implemented** |

Signup folders, which are the application types the UI implements:

- Client: `sa-natural`, `sa-legal`, `non-sa-natural`, `non-sa-legal`
- IFA: `ifa/natural`, `ifa/legal`

The only entity-type literal found in a form is `SA NATURAL` (`dashboard/templates/login/client/sa-natural/basic.php`). The stored strings for the other five forms were not found as literals. **Unknown** what `strEntityType` they post.

## Major workflows

### 1. Read the public facilities

Trigger: visitor opens a facility URL.

Inputs: none.

Behaviour: the section `index.php` loads site config, helpers, header, a template, and a content file. BullionGold investment overview is `investment/bullion-gold/`. HasanaGold investment overview is `investment/hasana-gold/`. Savings mirrors that split under `savings/bullion-gold/` and `savings/hasana-gold/`. Each facility has overview, pricing-and-features, and calculator pages. BullionGold investment also links a minted-bar calculator.

Output: HTML. Page cache code exists and is off (`$cache = false` in the page controllers, comment "Neil 20171107").

Error: `config/dbconnect.php` echoes the PDO exception message on failure. `investment/bullion-gold/index.php` turns `display_errors` on.

### 2. Apply to become a client or IFA

Trigger: signup forms under `dashboard/templates/login/`, posted to `dashboard/api/signup/signup.php` and the per-type email endpoints.

Inputs observed in code and templates: entity type, personal or legal-entity details, contribution choice (lump sum and/or monthly debit order), bank details, adviser fee percentage (0 to 3.5 in steps of 0.5), payment choice (debit order versus cash balance for the annual fee).

Behaviour: `signup.php` inserts into the web registration data (it writes `strEntityType` and related columns). Staff then work the row in `webadmin/registration.php`.

Validation that was traced:

- Adviser initial-fee dropdown stops at 3.5 (`for ($i = 0; $i <= 3.5; $i += 0.5)`).
- Document categories required before approval: `DocumentBank`, `DocumentPassport`, `DocumentResidentialAddress`.
- Reminder copy for those documents: proof of bank account less than 3 months old, copy of ID or passport, proof of residential address less than 3 months old (`webadmin/cron.documents.reminder.php`).

Expected result: a registration row. Status values seen in PHP: values other than `Active Client` and `Cancelled` stay on the main registration list; `On hold`; `Cancelled`; `Approved` (combined with all three documents approved); `Active Client`; `Active Client - EFT outstanding`.

Error / edge: cancelling sets status `Cancelled` when `blnCancelApplication` is checked. Upload to the access database sets `Active Client`, or `Active Client - EFT outstanding` in the EFT branch (`webadmin/includes/registration.cls.php`).

### 3. Staff approve identity and send the client live

Trigger: staff open `webadmin/id.verification.php` or `webadmin/registration.php`.

Actions implemented on ID verification: Reload, Approve, Upload To Web Access, Resend Application Email To Self, Send EFT Reminder, Save.

`registration.php` allows the approved transition only when bank, passport, and residential documents are all `blnApproved` and status is already `Approved`.

### 4. Client uses the portal

Trigger: successful login. `dashboard/index.php` redirects to `login.php` when `$_SESSION["USER"]->SYSTEM` is missing.

Behaviour by role is the sidebar tables above. Unapproved clients only get the documents screen.

Portfolio data is read from the access database (tables such as `web_client_detail`, `web_client_debit_order`). Clients have an `AurumNumber` column. What that number means operationally is **Inferred** as a client reference; no definition was found in comments.

### 5. Buy or sell Krugerrands, and schedule a drop-off

Buy: `krugerrand/buy/index.php`.

Sell: `krugerrand/sell/index.php` is a wizard. Query `sell-to-us`, `seller-details`, and `sell-review` select the step template.

Drop-off: `scheduler/index.php` sets the title "Schedule Drop Off" and the template `/krugerrand/scheduler/scheduler.php`.

Upload helpers exist: `helpers/upload-krugerrand-buy.php`, `helpers/upload-krugerrand-sell.php`.

### 6. Gold price publication and product price update

Trigger: `webadmin/cron.gold.price.php` and `webadmin/cron.silver.price.update.php` (no crontab is in git, so the schedule is **Unknown**).

Behaviour: both scrape `https://www.sharenet.co.za/v3/spot.php`. The gold script's successful path updates WordPress post meta `_regular_price`, `_price`, and `vat_value`. The silver script reads the "Silver $" and "USD / ZAR" rows, converts to ZAR, and updates product id `1846`, which a comment calls "1 ounce Silver Krugerrand". An after-hours script exists: `webadmin/after_hours.cron.gold.price.php`.

The public gold price page is `gold-pricing/index.php` (template `/all/gold-pricing.php`, content `/gold-pricing.txt`).

### 7. Storage facility delivery

`webadmin/storage.facility.php` lists orders and posts `type=Deliver` to `ajaxFunctions.php`. The UI asks the operator to confirm before marking an order delivered.

### 8. Card payment return (FNB)

`webadmin/validateFNBPayment.php` reads `txnToken`, calls FNB `validateTransaction`, and redirects to `https://sabullion.co.za/checkout` when `responseCode` is `000`, otherwise to `https://sabullion.co.za/cart`. Those two paths are not PHP directories on `master`. **Inferred** they belong to the WordPress/shop tree represented by the `.wpress` backup. **Unknown** whether this script is still wired to a live shop.

### 9. Partner and magazine

Partner overview: `partner/index.php`, with `partner-investment-calc.php` and `partner-saving-calc.php`.

Magazine: link target `/magazine/`, which is the WordPress tree. Theme directory `sa_bullion_magazine` is present.

## Functional requirements

### FR-001 — Public facility pages

The system serves BullionGold and HasanaGold investment and savings pages, each with overview, pricing and features, and a calculator.

Who: anonymous visitor. Trigger: HTTP GET of the section directory. Output: HTML assembled from `templates/` and `content/`.

Evidence: `investment/`, `savings/`, `templates/includes/sitenav.php`, `content/home-content.txt`.

Implementation: **Implemented** on `master`. Confidence: High.

### FR-002 — Facility names

The Halaal facility is presented as HasanaGold. Older Baraka filenames and variables remain (`content/baraka-*.txt`, `bapf_` variables, `haalalinvestmentnav.php`).

Implementation: **Implemented** with leftover names. Confidence: High.

### FR-003 — Client and IFA applications

The system accepts applications for SA natural persons, SA legal entities, non-SA natural persons, non-SA legal entities, and IFA natural and legal applicants.

Evidence: `dashboard/templates/login/` and `dashboard/api/signup/`.

Implementation: **Implemented**. Confidence: High for the six paths. Medium for the exact stored entity-type strings other than `SA NATURAL`.

### FR-004 — Registration documents

An application collects proof of bank account, proof of identity, and proof of residential address. Staff approve or reject each file. A reminder script describes the age rule as less than 3 months for bank and address proofs. The comment on `cron.documents.reminder.php` says the reminder is every 48 hours. No scheduler config proves that interval.

Implementation: **Implemented** for the three categories and approve/reject flags. The 48-hour schedule is a comment only.

### FR-005 — Registration status

Staff can move an application through the statuses named in the registration class: `On hold`, `Cancelled`, `Approved`, `Active Client`, and `Active Client - EFT outstanding`. Becoming an active client is an explicit "upload to web access" action, not an automatic result of saving the form.

Evidence: `webadmin/includes/registration.cls.php`, `webadmin/registration.php`.

Implementation: **Implemented**. Confidence: High.

### FR-006 — Portal access by role

After login the portal shows a different sidebar for global admin, house, IFA, client, and unapproved client. Missing session sends the browser to `dashboard/login.php`.

Evidence: `dashboard/index.php`.

Implementation: **Implemented**. Confidence: High.

### FR-007 — Statements

A client can open a summary statement and a detailed statement and can trigger a PDF download of each.

Evidence: `dashboard/templates/client/sidebar.php`.

Implementation: **Implemented** as navigation and print hooks. The PDF generator body was not fully read. Confidence: Medium.

### FR-008 — Calculators

Investment, savings, minted-bar, and partner calculators exist as pages and as `dashboard/api/calc/index.php` and `dashboard/api/calc/minted-bar-index.php`. The API requires a `currency` query parameter and returns JSON `success: false` when it is missing. The savings path also requires `debit`, `escalation`, and `savingcalc=true`.

Implementation: **Implemented**. Confidence: High.

### FR-009 — Krugerrand buy, sell, and drop-off

The public site has a buy page, a three-step sell wizard, and a schedule-drop-off page.

Evidence: `krugerrand/buy/`, `krugerrand/sell/index.php`, `scheduler/index.php`.

Implementation: **Implemented**. Confidence: High.

### FR-010 — Spot price import

A script scrapes Sharenet spot prices and writes prices onto WordPress product meta. A second script updates the silver Krugerrand product `1846`.

Evidence: `webadmin/cron.gold.price.php`, `webadmin/cron.silver.price.update.php`.

Implementation: **Implemented** as scripts. Whether they are scheduled in production is **Unknown**.

### FR-011 — Staff back office

Staff can sign in, and the code provides screens for users, security groups, registrations (active, cancelled), ID verification, product prices, product stock, gold sales, storage facility, appointments, FAQ, email templates, audit log, settings, documents, and reports.

Evidence: `webadmin/*.php` and `webadmin/includes/*.cls.php`. Which of these appear in the menu depends on `sysSecurityGroup` rows. **Unknown** without the database.

### FR-012 — Audit

`webadmin/audit.log.php` and `includes/audit.log.cls.php` list `sysAuditLog` by date, entity type, entity id, name, and revision.

Implementation: **Implemented**. Confidence: Medium (screen exists; which actions write the log was not fully traced).

### FR-013 — Appointments

The admin home shows a CT calendar and a Bedford calendar (`Dashboard::getCTCalendar`, `Dashboard::getBedfordCalendar`). `webadmin/appointment.php` is the appointment screen.

Implementation: **Implemented**. What "CT" and "Bedford" mean operationally is **Inferred** as two locations. Confidence: Medium.

### FR-014 — File integrity monitor

FIM can record files under a root, compare them to stored rows, and offer Ignore and Update. It sets Unix modes `0755` on directories and `0644` on files when it records them.

Evidence: `fim_2.0/fim.cls.php`.

Implementation: **Implemented**. Which environment config runs in production is **Unknown**.

### FR-015 — VAT

VAT used in pricing is `sysVat.dblVat`, loaded in `config/site.php`. A comment dated 2018-03-26 says the rate moved from 14% to 15% for 1 April 2018. The running rate is the database value, not the comment.

Implementation: **Implemented**. Confidence: High.

### FR-016 — POPI, terms, privacy, mandate

Routes exist: `POPINotice/`, `terms/`, `mandate/`, and `Krugerrand-Fractional-Ownership-Programme/`.

**Discrepancy:** `content/popi-notice.txt`, `content/terms-of-use.txt`, `content/privacy-policy.txt`, and `content/client-mandate.txt` contain a third-party "Harry's / HF Global, Inc." terms and privacy template, including an age rule of 13 and a US arbitration clause. The SA Bullion mandate wording (1% fee, 3.5% adviser cap, 1.5% administration fee, 13:00 cut-off) is in `templates/all/client-mandate.php` and `dashboard/templates/all/mandate.php`. Which text a visitor actually sees is **Requires Confirmation**.

### FR-017 — Magazine

The public nav links to `/magazine/`. That directory is WordPress with a `sa_bullion_magazine` theme.

Implementation: **Implemented** as a tree in git. Whether it is the magazine in production is **Unknown**.

## Integrations

| System | Purpose | Direction | Trigger | Auth | Failure |
|---|---|---|---|---|---|
| MySQL access DB (`$SA`) | Clients, gold prices, VAT, admin users | App reads and writes | Page and cron | Username/password in `config/dbconnect.php` per host. Values omitted here | PDO exception string is echoed |
| MySQL web DB (`$DBH`) | Web registrations and product premium | App reads and writes | Signup, calculators | Same file, second account | Same |
| Sharenet spot page | Gold, silver, USD/ZAR | Inbound scrape | Price cron scripts | None in code. TLS verify is disabled in the scrape helper | Not traced beyond curl return |
| FNB eCommerce v2 | Validate a card transaction token | Outbound GET, then browser redirect to `/checkout` or `/cart` | Request to `validateFNBPayment.php` with `txnToken` | API key header. Value omitted | Non-`000` responseCode redirects to the cart with the status message |
| WordPress tables (`wp_postmeta`) | Published shop prices | Written by the gold-price cron | Cron | Uses the admin DB connection after `switchBD` | Not traced |
| PHPMailer | Application and reminder email | Outbound | Registration and reminder scripts | SMTP settings were not fully read | Not traced |
| Rand Refinery | Named as the vault in marketing and FAQ copy | No code client found | — | — | — |

Live host names in `config/dbconnect.php` for `sabullion.co.za`: `dedi773.jnb3.host-h.net`, databases `sabullio_accessDB` and `sabullio_web_db`. QA and staging use `sql26.cpt3.host-h.net` and `sql29.cpt3.host-h.net`. Dev uses `sql8.jnb1.host-h.net` and `sql38.jnb1.host-h.net`.

## Constraints

- Fees and premiums are not a single source of truth. See business rules. Marketing pages, the mandate template, and the calculator disagree.
- Root `index.php` is gone, so `pageReq` links in the nav and in content have no router on `master`.
- Dashboard "gold price", "investment calculator", and "quarterly gold report" links point at WordPress-style paths that are not in the PHP tree.
- `webadmin/includes/gold.sales.cls.php` declares `class Appointment`, the same class name as `webadmin/includes/appointment.cls.php`. Loading both in one request would be a PHP fatal error. **Requires Confirmation** whether any request includes both.
- `webadmin/cron.client.balance.php` says it refreshes a balances table for the adviser client list, but the query is `WHERE ID = 1436 LIMIT 100`. That looks like a leftover filter. **Requires Confirmation**.
- Display errors are enabled on public page controllers.
- Secrets are committed in PHP config. They are intentionally not copied into this PRD.

## Planned behaviour

No future work is described on `master`. `development` describes itself as the new WordPress website and has not moved since 2021. Treat it as an abandoned branch until someone confirms otherwise. **Unknown**.

## Non-goals of this document

This PRD does not choose between the PHP public site, the `development` WordPress site, and the 2024 `.wpress` export. That choice needs a person who knows what `sabullion.co.za` serves today.
