# Business rules — dev.sabullion (SA Bullion WordPress)

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

Labels:

- **Documented** — written in copy or a comment in the repository.
- **Implemented** — enforced by code on this commit.
- **Inferred** — a reasonable reading of code, not stated as a rule.
- **Conflict** — two statements in the repo disagree.
- **Unknown** — not established.

Rules below are what the repository shows. They are not a decision that the business should keep them.

## Identity and facilities

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-001 | The product name in code is "Sa Bullion" / "SA Bullion" | **Implemented** and **Documented** | `SITENAME` in `config/site.php`; `README.md` |
| BR-002 | Two facilities are offered: BullionGold, and HasanaGold as a Halaal facility. Older code calls HasanaGold "Baraka" | **Documented** | `content/home-content.txt`, `content/baraka-*.txt` |
| BR-003 | The firm deals in newly minted 1 oz bullion Krugerrands only. FAQ text says smaller denominations carry higher premiums (7 percent, 9 percent, 11 percent) and are not offered | **Documented** | `content/partner-faq.txt` question on denominations. Not re-checked as a hard stop in the buy script |
| BR-004 | Non-residents may become clients. FAQ says SA Bullion is approved by the South African Reserve Bank for non-resident business | **Documented** | `content/partner-faq.txt`. Signup templates exist for non-SA natural and non-SA legal persons. The approval itself is not in code |
| BR-005 | Registered legal name | **Unknown** | `content/client-mandate.txt` is a Harry's terms template, not a mandate |

## Pricing and fees (copy)

These figures are in marketing and FAQ files. Calculator code was confirmed only where the evidence column says so.

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-010 | Krugerrands are priced at the LBMA afternoon gold fix plus a 3 percent refining and minting premium, paid to Rand Refinery | **Documented** | `content/bullion-pricing-feat.txt`, `content/partner-faq.txt` |
| BR-011 | The displayed Krugerrand price is `gold + gold * 3 / 100` for ZAR, USD, EUR, and GBP | **Implemented** | `wp-content/plugins/sab-api/api/gold/index.php` |
| BR-012 | Initial fee on deposits into the facilities is 1 percent of the amount | **Documented** | `content/bullion-pricing-feat.txt` |
| BR-013 | Annual fee is 2 percent a year for vaulting, insurance, and administration, described as VAT exclusive, levied monthly in arrears on market value | **Documented** | `content/bullion-pricing-feat.txt`, `content/partner-faq.txt` |
| BR-014 | Fees are negotiable above R1 million | **Documented** | `content/bullion-pricing-feat.txt` |
| BR-015 | An accredited advisor may charge an initial fee of up to 3.5 percent, agreed with the client | **Documented** | `content/bullion-pricing-feat.txt`. Upload fields include `calc_advisorfee` |
| BR-016 | A partner receives 25 percent of fees earned. The client pays 2 percent a year. Partner text says this is billed monthly in arrears by debit order | **Documented** | `content/partner-page.txt`. The 25 percent split was not found as a calculation in the portal files read |
| BR-017 | Cash held in the facility earns a preferential rate, stated as 5 percent, paid by Nedbank | **Documented** | `content/partner-faq.txt`. Not found as a code constant in this pass |
| BR-018 | Sale to the South African Reserve Bank is at full market value with no charges. Settlement into the selected bank account is 3 to 4 business days | **Documented** | `content/partner-faq.txt` |
| BR-019 | Physical delivery is by insured courier and is for the client's account. A quotation is provided first | **Documented** | `content/partner-faq.txt` |
| BR-020 | Insurance is Lloyd's of London and is included in the annual premium | **Documented** | `content/partner-faq.txt` and `content/about-story.txt` ("world's leading underwriter") |
| BR-021 | VAT used by the application is `sysVat.dblVat`, not a hardcoded 14 or 15 | **Implemented** | `config/site.php` and `dashboard/config/site.php`. Comment dated 2018-03-26 says the country rate became 15 percent on 2018-04-01. The number in the table was not read |

## Calculator rules (code)

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-030 | Minted-bar illustration currency is USD, ignoring a currency selector | **Implemented** | `sab-api/js/minted-bar-investments-calc.js` sets `ip-currency` to `"USD"`. Commit `743280c` |
| BR-031 | A minted-bar (and the same function's) contribution must be greater than the latest gold price in that currency, plus 1 percent, plus 1000 | **Implemented** | `upload-minted-bar-investment.php`. Amounts at or below that threshold return `success: false` |
| BR-032 | The rejection message calls the threshold a Krugerrand minimum, including on the minted-bar upload | **Implemented** | Same file. The message and the function name do not match. **Conflict** inside one function |
| BR-033 | A stored minted-bar illustration gets reference `SABI` plus a zero-padded 6-digit sequence | **Implemented** | `MAX(sbic_reference_number)+1` on `sb_invst_minted_bar_calc` |
| BR-034 | Investment chart requests use an example amount of 100000 and currency ZAR when the page script asks for the default series | **Implemented** | `sab-api/js/site-functions.js` |
| BR-035 | Gold history for the investment calculator looks up `gold_price` on calendar anniversaries (10 years back, then 9, and so on) | **Implemented** | `sab-api/api/calc/index.php` |
| BR-036 | Missing amount or currency on the calc route returns `success: false` and a message | **Implemented** | `api/calc/index.php`. As in the gold route, some missing-field branches sit inside a condition that already requires the fields |

## Portal access

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-040 | Sign-in requires email and password. The password is stored and checked as MD5 with no salt | **Implemented** | `dashboard/api/login/index.php`. Salt prefix is commented out |
| BR-041 | After a password match, the first matching profile wins, in this order: house (`web_ifa_company_detail`), IFA (`web_ifa_advisor`), client (`web_client_detail`), unapproved registration (`web_client_registration`), global admin (`web_global_admin_detail`) | **Implemented** | Same file |
| BR-042 | A password match with no profile returns "Your account has not been created yet" | **Implemented** | Same file |
| BR-043 | An unapproved client session can open only the documents page | **Implemented** | `dashboard/index.php` replaces the template when `SYSTEM` is `UNAPPROVED-CLIENT` |
| BR-044 | No session on `dashboard/index.php` redirects to `login.php` | **Implemented** | `dashboard/index.php` |
| BR-045 | Portal session lifetime is 3 days (259200 seconds), files under `dashboard/SESSION` | **Implemented** | `dashboard/config/site.php` |
| BR-046 | If the referrer contains `/webadmin` and the global-admin login status is `Success`, the portal login does not destroy that session | **Implemented** | `dashboard/login.php` |
| BR-047 | IFA add-team is switched off | **Implemented** | `dashboard/helpers/requests.php` condition includes `&& false` |
| BR-048 | An authorization key check on the older login script is commented out, so it does not run | **Implemented** | `dashboard/api/login/login.php` |
| BR-049 | Public calculator, upload, email, and gold REST routes do not require a WordPress user | **Implemented** | `permission_callback` returns true in `sab-api.php` |

## Registration documents

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-050 | A reminder should be sent every 48 hours when registration status is not like "Approved" and the document file name is null | **Documented** in a comment | `dashboard/helpers/cron.documents.reminder.php` |
| BR-051 | That script selects the rows and dumps them. It does not send mail | **Implemented** | Same file. The 48-hour schedule is not in the file. **Unknown** whether anything calls it |
| BR-052 | Generated pdf, png, and xls files are deleted when the daily cleanup flag is true | **Implemented** | `dashboard/helpers/cron.daily.php`. Caller **Unknown** |

## Custody statements that disagree

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-060 | Holdings are reconciled with Rand Refinery daily. One FAQ answer says Deloitte does an annual physical stock audit. Another says PwC does a quarterly physical stock audit | **Conflict** | `content/partner-faq.txt` answers on audit. Code does not choose either |
| BR-061 | A client may phone the treasurer at Rand Refinery to confirm holdings | **Documented** | Same file |

## Legal copy that is not SA Bullion

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-070 | Terms, privacy, POPI notice, and the file named client mandate contain Harry's (HF Global, Inc.) terms, including a US arbitration clause and a minimum age of 13 | **Documented** as the file body, and a **Conflict** with the filenames | `content/terms-of-use.txt`, `privacy-policy.txt`, `popi-notice.txt`, `client-mandate.txt` |
| BR-071 | Those files are not evidence of SA Bullion's terms, privacy notice, or POPI notice | **Inferred** | The body names `www.harrys.com` and HF Global. Do not treat them as the firm's rules |

## Host and environment behaviour

| ID | Rule | Label | Evidence |
|---|---|---|---|
| BR-080 | Live and unknown host names use `https://sabullion.co.za`. QA uses `https://sabullion.overdriveqa.co.za`. `localhost`, `lamp`, and `dev.overdrive.co.za` use http and a path prefix | **Implemented** | `config/site.php`, `dashboard/config/site.php` |
| BR-081 | Public header "Open an Account" and "Sign In" ignore the live host and always link to the QA dashboard | **Implemented** | `wp-content/themes/sabullion/templates/header.php` and `footer.php`. **Conflict** with BR-080 and with `template-partnership.php`, which links to `sabullion.co.za` |
| BR-082 | When `$_SERVER["USER"]` is `sabullionco`, the dashboard forces `SERVER_NAME` to `sabullion.co.za` before the host switch | **Implemented** | `dashboard/config/site.php` |
| BR-083 | Non-lamp mail uses Office 365 SMTP as `business@sabullion.co.za`. `lamp` uses sendmail | **Implemented** | `dashboard/config/mailer.php` |
| BR-084 | SMTP peer and name verification are disabled | **Implemented** | `SMTPOptions` in the same mailer |

## What was not treated as a rule

- WooCommerce prices, stock, and PayGate settings. They are not in git.
- WordPress roles and capabilities beyond core defaults. `sab-api` does not add a role.
- The 5 percent Nedbank rate, the R1 million fee negotiation, and the 3 to 4 day settlement. They are copy only.
- Any rule that exists only in `silicon_ark/sabullion/` or `silicon_ark/sabullion_current_live/`. Those documents describe other commits.
