# PRD — knowmore_current (Knowmore)

Evidence date: 2026-10-11. Source: `git@bitbucket.org:overdrive/knowmore_current.git`, `master` at `98e19240` (2025-04-04).

There is no prior PRD in the repository or in Silicon Ark for this remote. Requirements below were recovered from the static HTML and the PHP. Provenance is marked on each item.

`knowmore_php84` and `knowmorescan` are different repositories. Their Silicon Ark documents are not requirements for this tree.

## Product purpose

Knowmore gives financial planners, support staff, and business owners learning content and online business tools for the financial profession in South Africa. After login, a learner can follow courses, workshops, and publications, and download a CPD report to submit as proof of CPD earned through Old Mutual Wealth.

Provenance: **Documented** in `about.html`, `terms-and-conditions.html`, `index.html`, and `cpd-points.html`. The learning and tool behaviour behind those pages is **Implemented** under `training/`, `tools/`, and `webadmin/`.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Anonymous visitor | Reads marketing and legal pages | Root `*.html` | Implemented (static) |
| Prospect / new learner | Opens `register.html`. The matching `register.php` is marked unused and exits before it saves | `register.html`, `register.php` | Documented page. Save path is not live in `register.php` |
| Learner | Signs in with email and password, lands in `training/` | `login.php`, `training/index.php` | Implemented, with defects noted under W2 |
| Learner with more than one access flag | Would be sent to `portal.php` | `login.php` | Implemented branch. `portal.php` is not in the tree |
| Webadmin user | Signs in through the same `login.php` when `sysUser` matches and the security group is active. Lands in `webadmin/index.php` | `login.php`, `webadmin/` | Implemented |
| Practice advisor | Uses `tools/` while `$_SESSION["ADVISOR"]` is set | `tools/index.php` | Implemented |
| Old Mutual SSO user | Clicks "LOGIN WITH OLD MUTUAL SSO" | `login.html` | Documented on the page. The link target is missing. See W3 |
| Cron | Sends course, survey, licence, AML, and IBC mail | `webadmin/cron.*.php`, `tools/cron.*.php` | Implemented scripts. Scheduler is Unknown |

Menu items inside webadmin are not fixed in code. `webadmin/_nemo.menu.cls.php` loads `sysMenuLevel1` / `sysMenuLevel2` for the signed-in security group and shows a level-2 item only when `sysSecurity.blnView` is set. Provenance: **Implemented**. The actual menu labels live in the database and were not read.

## Main workflows

### W1 — Read the public site

Trigger: visitor opens `https://www.knowmore.co.za/` (canonical URLs in the HTML).

The header menu is Home, About, Business Tools, Courses, Publications, Workshops, CPD Points, and an external COVID-19 link to `https://sacoronavirus.co.za/`. Footer links include legal pages. Login and Register are called out in the page copy.

`.htaccess` redirects HTTP to HTTPS with a 301. A rule that would append `.html` is commented out, and it would have rewritten under `/knowmore_current/` if enabled.

Provenance: **Documented** and **Implemented** as static files. WordPress is not serving these pages in this tree. The HTML is an export (Elementor, GeneratePress, and "export-wp-page-to-static-html" stylesheets). WordPress core was removed in September 2024.

### W2 — Sign in with a password

Trigger: `login.html` posts to `login.php`.

Inputs: `Action`, `strUsername`, `strPassword`.

Behaviour in `login.php`:

1. `Action=Redirect` looks up `sysUser` joined to `sysSecurityGroup` by email and `strPasswordMD5`. The MD5 variable is not set in this request. The lookup as written cannot match. Provenance: **Implemented** code path. **Inferred** defect.
2. `Action=Login` loads an active-security-group `sysUser` row and a `tblLearner` row for the same email.
3. If more than one of webadmin, learner, and tools access is set, the script stores `$_SESSION["ACCESS"]` and redirects to `portal.php`. `$rowTools` is never queried, so the tools flag is not set by this file. `portal.php` is not in the repository.
4. Otherwise, if the webadmin user is active, `NemoPassword::checkPassword("User", ...)` must succeed. Success writes `$_SESSION["USER"]` (ID, username, email, security group) and redirects to `webadmin/index.php`. Failure writes a login row and redirects to `comms/log-in/`, which is not a file in this tree.
5. The learner branch below that uses `$row2` and `$row`, not the `$rowLearners` result loaded above. As written, a learner-only login does not use the learner row that was fetched. Provenance: **Inferred** defect.

Successful learner login, where the variables do line up in older comments, writes `$_SESSION["LEARNER"]` (ID, user type, user category, username, display name, email, active flag, region, typography, picture path) and redirects to `training/index.php`.

Inactive webadmin users are refused and a login row is inserted ("Inactive User").

### W3 — Sign in with Old Mutual SSO

Trigger: the button on `login.html` links to `oauth/init_oauth.php`.

That path does not exist. The start script is `init_oauth.php` at the repository root. Provenance: **Implemented** files, **Inferred** broken link.

`init_oauth.php` loads `bootstrap.php`, which:

1. Loads Composer and `webadmin/_system.php`.
2. Loads a `.env` file from the repository root with Dotenv `load()`. No `.env` is committed. A missing file makes this script fail before OAuth starts.
3. Builds a League OAuth2 generic provider from `PING_*` system settings, with PKCE S256.

The start script stores `oauth2state` and the PKCE verifier in the session and redirects to the authorization URL.

`authorize.php` rejects a missing or mismatched `state`, exchanges the code, requests the resource-owner URL, prints the HTTP body, and `die()`s. It does not write `$_SESSION["LEARNER"]` or `$_SESSION["USER"]`.

`process_auth_code.php` is a second callback. It prints the access token, refresh token, expiry, and `var_export` of the resource owner. It reads `RESOURCE_SERVER_URL` from the environment.

`phpazure/` is a separate Azure AD sample (Katy Nicholson, updated 2021). `phpazure/inc/_config.inc` says "RENAME TO config.inc" and then defines OAuth constants from `$SystemSettings` (`_OAUTH_LOGOUT`, `_OAUTH_SCOPE`, and further keys in that file). It is not what `login.html` links to.

Provenance: **Implemented**. A completed SSO login that opens the learner or admin area was not found. Whether production uses a path outside this tree is **Unknown / Requires Confirmation**.

### W4 — Learner home

Trigger: signed-in learner opens `training/index.php`.

The page reads `$_SESSION["LEARNER"]`, loads `tblLearner` with user type and category, and loads the learner's practice. If the practice has no `refWPSID`, the code uses `-10`. The user-category block and the WPS block are forced hidden (`display: none`), with a 2017 comment that this was a permanent hide.

The home embeds an iframe of `latestblogposts/?limit=2` via `getTypoIframe2`. That URL was a WordPress page. WordPress is no longer in this repository. Whether the host still serves that path is **Unknown / Requires Confirmation**.

The same page queries `dwfBatResults_2020` for the current calendar year and questionnaire 4, and tallies red / amber / green pillar results for the learner's practice.

Provenance: **Implemented**.

### W5 — Courses, workshops, publications, assignments, forums

Trigger: learner navigation under `training/`.

Entry points:

- Courses: `home.courses.php`, `course.php`, `purchase.courses.php`.
- Workshops: `home.workshops.php`, `workshop.php`, `purchase.workshops.php`.
- Publications / Wealth Intelligence: `home.wi.php`, `module.wi.php`. A 2019 comment in the CPD page says Wealth Intelligence was renamed Articles and then Publications.
- Assignments: `assignments.php`, `my.assignments.php`.
- Forums: `forum.php`, `general.forum.php`.
- Profile: `profile.php`, `learner.profile.php`.
- Invoices: `my.invoices.php`, `transactions.php`.
- Purchase: `purchase.php`, `purchase.omw.php`.

`webadmin` mirrors this with `course.php`, `module.php`, module-part editors (`mod.assignment.php`, `mod.video.php`, `mod.forum.php`, `mod.workbook.php`, `mod.practical.php`, `mod.question.php`, `mod.resources.php`, `mod.note.php`), `workshop.php`, `wealth.intelligence.php`, `articles.php`.

Provenance: **Implemented** as screens. Enrolment rules that live only in SQL were not extracted row by row.

### W6 — CPD report

Trigger: learner opens `training/my.cpd.points.php` or downloads a PDF from it.

Documented behaviour (`cpd-points.html`): many learning activities have verifiable CPD points for FPI and FSCA requirements. Activities without verifiable points can be claimed for FPI membership as non-verifiable CPD only when they meet FPI rules. The learner tracks points on the dashboard after login, downloads the CPD report, and submits that file as proof of CPD earned through Old Mutual Wealth. The site does not describe sending the report to FPI itself.

Implemented behaviour:

- Calls `matchCPDPoints` for the learner.
- The current cycle variables are hardcoded: start `2024-06-01`, end `2025-05-31`.
- Two comparison windows are also hardcoded: `2023-06-01`–`2024-05-31` and `2022-06-01`–`2023-05-31`. The variable names still say 2018 and 2019.
- An automatic "if today is before 31 May, use last 1 June through this 31 May" block is commented out. A comment dated 2019-05-02 says that block should automate the current period.
- Comments record a rule that every third Wealth Intelligence item is 0.5 CPD and the others are 0, replacing an earlier 0.1667 split. The PDF uses `dblPointsFPI`.
- The report includes an ID number and a CPD cycle label (comment, 2019-04-17).

Provenance: **Documented** for the learner-facing promise. **Implemented** for the hardcoded windows. On the evidence date (2026-10-11) the hardcoded current window has ended. Whether production was patched outside git is **Unknown / Requires Confirmation**.

### W7 — Register

`register.html` is a static "REGISTER NEW LEARNER REGISTRATION" page. No `form action` pointing at a PHP file was found in it.

`register.php` contains the comment "THIS CLASS IS NOT IN USE ANYMORE" and `print_rr($_REQUEST); die;` before the `Action=Submit` save. The save code after that line (spam check for `googlemail.com`, course dates, reCAPTCHA keys from system settings) does not run.

Other entry points still in the tree: `register.changes.php`, `register.courses.php`, `register.invite.php`, `register.ph3.php`, `register.risk.php`, `register.rsvp.php`, `register.special.php`, `register.tools.php`, `register.typo.php`, `register.wi.php`. Which of these the live site still posts to is **Unknown / Requires Confirmation**.

Password recovery entry points: `forgot-pw.php`, `forgot.password.php`, `forgot.password.wp.php`. Reset-link expiry default is `60 MINUTE` when the password tables are first created. See business rules.

### W8 — Contact

`contact-us.html` is the static contact page (address above). `contact.us.php` is a separate Nemo page that still presents Celestis telephone, Skype, and postal address. It was not traced to a form on the static page.

Provenance: **Documented** (static) and **Implemented** (PHP). The two pages disagree. See the project plan.

### W9 — Business tools

Trigger: an advisor session opens `tools/index.php`. If `$_SESSION["ADVISOR"]` is missing, the script sends the browser to `../index.php` (a path that is not in this tree; the public home is `index.html`).

The tools home can save practice industry and subcategory onto `tblPractice`, then iframes `$SystemSettings["LiveURL"]/hidden/business-tools`.

Marketing pages describe:

- Business Assessment Tool, exclusive to partner businesses, against six elements of a financial-planning business (`business-assesment-tool.html`, `business-tools.html`). The filename uses "assesment".
- Client Satisfaction Survey, exclusive to partner businesses, independent and anonymous (`client-satisfaction-survey-tool.html`).
- TCF gap analysis, with a report and a CPD-points claim (`gap-analysis.html`).
- Valuation service led by a business coach (`valuation-service.html`).

Implemented engines under `tools/`:

- `bat.engine.php`, `bat.engine2.php`, `bat.engine3.pwm.php`, `bat.class.php`, `bat.chart.php`.
- `bat_2020.engine.php`, `bat_2020.class.php`. Learner home reads `dwfBatResults_2020`.
- `css.survey.php` and root `css.survey.php` / `cat.unregister.php`.
- Gap engines: `gap.TCF`, `gap.RDR`, `gap.PoPI`, `gap.SCP` (each `.engine.php` and `.class.php`).
- Income and expense capture: `ie.engine.php`, `ie.income.sav.php`, `ie.expenses.sav.php`, `ie.practice.setup.sav.php`, `ie.staff.setup.sav.php`.
- Invoices and PDF: `invoicePDF.php`, `dtPDF.php`, `product_pdf.php`, `product_excel.php`.

Provenance: **Documented** on the marketing pages for BAT, client survey, TCF gap, and valuation. **Implemented** as the PHP engines. "Exclusive to partner businesses" is **Documented**. The code check that enforces exclusivity was not isolated; treat the exclusivity rule as documented, not as a verified query.

### W10 — Pay for a licence

`tools/purchase.php`, `tools/purchase.netcash.php`, `tools/purchase.renewal.php`, and `tools/payGateResults.php` are the payment files.

`purchase.netcash.php` documents six steps in comments: update `tblTransaction`, insert an invoice, generate licences, load the licence, activate it, go to the message page. The first executable statement after includes is `print_rr('yes include'); die;`. Those six steps do not run.

Provenance: **Implemented** as a short-circuited script. Whether Netcash still posts to a different file is **Unknown / Requires Confirmation**.

### W11 — Webadmin

Trigger: `$_SESSION["USER"]` and a security group.

Screens present as PHP files include learners, courses, modules, workshops, practices, clients, licences, invoices, vouchers, products, venues, regions, countries, user types and categories, security groups, email templates, articles, adverts, CPD, reports, SLA quotes and services, content distribution, and manual email sending.

`gotoBusinessTools.php`, `gotoBusinessToolsBAT2.php`, and `gotoAcademyWebadmin.php` are hand-off scripts into the other areas.

Provenance: **Implemented**.

### W12 — Scheduled mail

`webadmin/cron.daily.php` with `Action` null runs, among other tasks:

- Course-start email for courses whose `dtStartDate` is five days ahead, licence status Active, learner and course active. Course type 6 uses the template "Course Start EA". The comment says Alida recommended five days on 2016-08-24. The same comment says a one-week lead was the older rule.
- Module-available email, assignment reminders (the Thursday before the due date and the due date, if the assignment is not already uploaded), course-end email, and a CSS reminder. These are named in comments at the top of that switch.
- AML learner processing and IBC learner processing exist as explicit `Action` values and as sections dated 2018.
- Deactivating learners four weeks after the last course end date is commented as switched off after the tools merger (2013-10-01, pj).

`tools/cron.daily.php` comments describe licence-renewal mail at about six weeks, two weeks, three days, and the day after expiry, a CAT survey reminder, and an import of consulting hours from `ConsultingHours.xls`.

Hourly scripts `webadmin/cron.hourly.php` through `cron.hourly8.php`, plus weekly scripts under `tools/`, and `fim_2.0/fim.cron.php`, are in the tree. Nothing in git schedules them.

Provenance: **Implemented** in the scripts. The schedule is **Unknown**.

## Functional requirements

### FR-001 — Public marketing site

The site presents Knowmore as Old Mutual Wealth learning and business tools, with the menu in W1.

Evidence: root HTML. Implementation: static export. Confidence: high for the pages in git. Confidence in live hosting: unknown.

### FR-002 — Legal identity

The privacy policy states the owner. The terms state the operating subsidiary and the South African audience. Copyright on the pages is Knowmore's. Personal information is used for the purpose requested, to meet obligations to the user, and to meet legal obligations, and is not disclosed to third parties unless the law requires it or the user authorised it.

Evidence: `privacy-policy.html`, `terms-and-conditions.html`, `copyright.html`, `disclaimer.html`. Implementation: static text. Confidence: high.

### FR-003 — Password login

A user identified by email can attempt login. Webadmin access requires an active security group and a `NemoPassword` match, then a `USER` session. Learner access is intended to create a `LEARNER` session and open `training/index.php`.

Evidence: `login.php`, `webadmin/_nemo.password.cls.php`. Implementation: present, with the variable defects in W2. Confidence: high that the file does what W2 describes.

### FR-004 — SSO

The login page offers Old Mutual SSO. The repository contains an OAuth authorization-code + PKCE start and two callbacks that display tokens or the identity HTTP body.

Evidence: `login.html`, `init_oauth.php`, `authorize.php`, `process_auth_code.php`, `bootstrap.php`. Implementation: partial. Confidence: high that no session is created in these callbacks.

### FR-005 — Learner learning record

A learner can open courses, workshops, publications, assignments, forums, invoices, and a profile from `training/`.

Evidence: `training/*.php`. Implementation: screens exist. Confidence: medium. Each screen was not executed.

### FR-006 — CPD report download

The learner can download a CPD PDF for the windows hardcoded in `training/my.cpd.points.php` and is told to submit it as proof. The platform does not submit it to the FPI.

Evidence: `cpd-points.html`, `training/my.cpd.points.php`. Implementation: present. Confidence: high for the hardcoded dates. Confidence that those dates are still the business rule: requires confirmation.

### FR-007 — Practice tools

An advisor with an `ADVISOR` session can open BAT, BAT 2020, the client survey, and the TCF, RDR, PoPI, and SCP gap engines, and can store practice industry fields.

Evidence: `tools/`. Implementation: present. Confidence: medium.

### FR-008 — Partner exclusivity

Marketing copy says the Business Assessment and the Client Satisfaction Survey are available exclusively to partner businesses.

Evidence: `business-assesment-tool.html`, `client-satisfaction-survey-tool.html`. Implementation of the gate: not isolated. Confidence: high for the copy, low for the enforcement.

### FR-009 — Webadmin permissions

A webadmin user's menu is the set of level-1 and level-2 items granted to their security group, and a child item is shown only when `blnView` is true.

Evidence: `webadmin/_nemo.menu.cls.php`, `webadmin/security.group.php`. Implementation: present. Confidence: high.

### FR-010 — Scheduled communications

Daily cron sends course-start mail five days before `dtStartDate` for active licences, plus the other mail tasks named in W12, when the script is invoked with an empty action.

Evidence: `webadmin/cron.daily.php`. Implementation: present. Confidence: high for the script, unknown for the schedule.

### FR-011 — HTTPS

HTTP requests are redirected to HTTPS.

Evidence: `.htaccess`. Implementation: rewrite rules. Confidence: high for the file. The server must have `mod_rewrite`. That was not verified on a host.

### FR-012 — Deploy master by FTP

A push to `master` runs a pipeline that installs `git-ftp` and pushes the repository to `ftp://$FTP_HOST`.

Evidence: `bitbucket-pipelines.yml`. Implementation: pipeline definition. Confidence: high for the file. Whether the variables are set in Bitbucket was not checked.

## Integrations

| System | Purpose | Direction | Auth | Failure behaviour |
|---|---|---|---|---|
| MySQL (`mysqli`) | Application data | App to database | Username and password in `webadmin/system.php`, selected by `HTTP` host | Not wrapped in these documents. Credentials omitted |
| OAuth authorization server | SSO | Browser redirect, then token POST | Client id and secret from `PING_*` settings | `authorize.php` exits with the provider exception message |
| SMTP | Course, survey, and security mail | App to mail server | `SMTP Send As`, `SMTP BCC`, `SMTP Send As OMW` from settings | `NemoEmail::Send()` result is printed in cron |
| SMS | Security warning path in `system.functions.inc.php` | App to SMS gateway | `smsUsername`, `smsPassword`, `smsLive` from settings | Not traced end to end |
| CloudFront `d1ogvodmq23438.cloudfront.net` | Public URL for an `s3://` resource | Rewrite in `aws_url()` | None in that function. The comment says "signed"; the code prefixes the CloudFront host and returns | Empty input is returned unchanged |
| Amazon S3 | `aws_put_file()` putObject, ACL `public-read` | App to S3 | `aws_access_key`, `aws_secret_key`, `aws_region` from settings | SDK class is referenced and is not in `vendor/` |
| Netcash | Licence purchase callback | Payment gateway to `tools/purchase.netcash.php` | Not reached | Script dies immediately |
| PayGate | `tools/payGateResults.php` | Gateway callback | Not read in full | Unknown |
| reCAPTCHA | `register.php` includes `recaptchalib.php` and reads `PublicKey` / `PrivateKey` | App to Google | Keys from settings | Unreachable because `register.php` dies first |
| Wordfence | `.user.ini` auto-prepend | Host PHP | Path outside the repo | If the file is missing, PHP startup fails when this ini is applied |
| Bitbucket Pipelines / git-ftp | Deploy `master` | Pipeline to FTP | `FTP_USER`, `FTP_PWD` | Not described in the YAML beyond the push command |
| SonarQube | Declared scan step, project key `knowmore_sso` | Pipeline to Sonar | `SONAR_TOKEN` | Step is not referenced by the `master` pipeline |
| sacoronavirus.co.za | Public menu link | Browser | None | External site |

## Constraints

- South African financial-profession audience, as stated in the terms.
- Personal-information limits in the privacy policy (FR-002).
- CPD proof is a downloaded report, not an FPI submission (FR-006).
- Webadmin visibility is per security group (FR-009).
- Password policy defaults are created only when the password tables are absent. After that, `sysSettings` wins. See business rules.
- Database choice is the HTTP host name. `www.qa-celestis.co.za` is labelled environment `live` and points at database `knowmore_2016`, the same database name as `www.knowmore.co.za`. The `localhost` key also points at that database's host. Treat local use of this tree as capable of touching the live-named database. Provenance: **Implemented**.

## Error and edge behaviour

- SSO state mismatch: response body is the text `Invalid state`.
- OAuth provider error: response body is the exception message. Tokens are not stored.
- `authorize.php` and `process_auth_code.php` print token material to the browser on success.
- Inactive webadmin user: login row "Login failed: Inactive User", redirect to `comms/log-in/`.
- Unknown webadmin user or inactive security group: login row "User not found or SG not active", same redirect.
- Learner home with no practice WPS: WPS id `-10`.
- Learner home with no category in session: category id `-1`.
- Tools home without `ADVISOR`: redirect to `../index.php`.
- `register.php`: dumps the request and stops.
- `purchase.netcash.php`: prints `yes include` and stops, so a gateway callback does not create an invoice.
- Missing `.env`: OAuth bootstrap does not start.
- Missing AWS SDK: `aws_put_file()` cannot construct the client with the current `vendor/` tree.
- Duplicate login areas: the portal redirect targets a file that is not in git.
