# PRD — knowmore_php84 (Knowmore)

Evidence date: 2026-10-11. Source: `git@bitbucket.org:overdrive/knowmore_php84.git`, `master` at `2e5a13d0` (2020-10-19).

Requirements below are recovered from the implementation. Each one is labelled:

- **Documented** — stated in a comment, commit message, or in-repo text that describes the behaviour.
- **Implemented** — present in code on `master`.
- **Inferred** — a reasonable reading of that code, not written down as a requirement.
- **Planned** — present only on an unmerged branch, or described as future work.
- **Unknown** — not established.

There is no product requirements document in the source repository.

## Product purpose

Knowmore lets learners in financial-planning practices register, sign in, take courses and workshops, submit assignments, and collect CPD evidence. Practices use online assessments (BAT, client survey, gap analyses) that produce reports. Staff run the catalogue, learners, licences, invoices, and email from a Nemo web admin. A WordPress site is the public front.

Provenance: **Inferred** from the modules listed in this PRD. No purpose statement was found in a README.

## Users and roles

| Role | How the code treats them | Evidence | Provenance |
|---|---|---|---|
| Public visitor | WordPress theme and registration pages before a learner session exists | `index.php`, `register*.php` | Implemented |
| Learner | Row in `tblLearner`. Session object `$_SESSION[LEARNER]`. Must be active (`blnActive != 0`) unless a registration-purchase redirect is in progress | `login.php` | Implemented |
| Learner user type and category | `tblUserType` and `tblUserCategory` copied onto the session at login (`USERTYPEID`, `USERCATEGORYID`) | `login.php` comment 2016-11-21 | Implemented |
| Practice / advisor | Tools area expects `$_SESSION[ADVISOR]`. Missing session sends the browser to `../index.php` | `tools/index.php` | Implemented |
| Web-admin user | Row in `sysUser` joined to `sysSecurityGroup`. Session `$_SESSION[USER]` with `SECURITYGROUPID`. Group must be active | `webadmin/login.php`, `webadmin/_nemo.cls.php` | Implemented |
| Security group | Menu and page rights `blnView`, `blnSave`, `blnDelete`, `blnSpecial` from `sysSecurity` | `webadmin/_nemo.menu.cls.php` | Implemented |
| Coach | Commit `dcc1d970` (2020-04-08) is "Coach webadmin login changes". A distinct coach role table was not confirmed | Commit message | Requires confirmation |

Anonymous access to `training/` is rejected. `training/_nemo.basic.cls.php` redirects to `../second.wp.php?MID=LD9DHR` when `$_SESSION[LEARNER]` is missing. Provenance: **Implemented**.

Anonymous access to `webadmin/` pages that construct `Nemo` is rejected. The constructor redirects to `SessionExRedirect` when `$_SESSION[USER]` is missing. Provenance: **Implemented**. The redirect target is loaded from `sysSettings`, so the URL is **Unknown** without the database.

## FR-001 — Public WordPress site

**Who:** visitor.

**Trigger:** request to `/` or a WordPress permalink.

**Behaviour:** `index.php` sets `WP_USE_THEMES` and loads `wp-blog-header.php`. The active theme in the tree is `wp-content/themes/knowmore` (theme name Knowmore, version 1.0). Menus render with `wp_nav_menu` for theme location `primary` (`header.php`). Page templates include home, page, single, category, search, and `page-latestblogposts.php`.

**Output:** HTML from the theme.

**Constraints:** `.htaccess` redirects HTTP to HTTPS and applies the WordPress front controller. Directory indexes are off. Direct access to `wp-config.php` and `.user.ini` is denied.

Provenance: **Implemented**.

Which pages exist in the WordPress database is **Unknown**. The menu is not hardcoded.

## FR-002 — Learner registration

**Who:** a person without a learner session, or a person following an invite.

**Trigger:** one of the root register scripts.

| Script | What the file says it is |
|---|---|
| `register.php` | Registration with reCAPTCHA and password-validation comments (2013) |
| `register.ph3.php` | Phase 3 access-control registration (comment 2016-11-08). Later comments record security, validation, and 2020-03-05 registration changes |
| `register.changes.php` | Same Phase 3 comment as `register.ph3.php` |
| `register.courses.php` | Course registration. Comment 2013-08-01 on course start date, end date, and start time |
| `register.workshops.php` | Workshop registration, same date comment |
| `register.wi.php` | Copied from `register.courses.php` on 2014-07-10 for WI |
| `register.tools.php` | Tools registration, same reCAPTCHA header as course registration |
| `register.rsvp.php` | RSVP registration. Sets practice to null for a new learner (comment 2016-10-25) |
| `register.special.php` | Same as RSVP, except it uses `tblCourse.strSpecialCode` as an always-open voucher code |
| `register.invite.php` | Destroys any existing session, then continues. Further rules were not fully traced |
| `register.risk.php` | Destroys any existing session. Product rules beyond that were not fully traced |
| `register.typo.php` | Typography variant of the older registration form |

**Inputs (main `register.php`):** posted learner fields, a financial-planner-code yes/no, a staff-code yes/no, an ID-number yes/refused choice, and reCAPTCHA challenge and response fields.

**Behaviour:** reCAPTCHA is checked with `recaptcha_check_answer`. An incorrect solution is treated as an error (`incorrect-captcha-sol`). On success the code inserts a `tblLearner` row and sets `blnActive = 1`.

**Validation:** reCAPTCHA must pass. Country and region queries on the form load active rows (`blnActive = 1`). Detailed field rules beyond that are inline in each script and were not exhaustively listed.

**Error:** failed captcha stays on the form. Database errors depend on `NemoDatabase` and were not separately specified.

Provenance: **Implemented**. Which of these scripts is still linked from the live WordPress menu is **Unknown**.

## FR-003 — Learner login

**Who:** learner.

**Trigger:** `Action=Login` on `login.php`.

**Inputs:** `strUsername` (matched to `tblLearner.strEmail`), `strPassword`.

**Behaviour:**

1. Load the learner and user type by email.
2. If no row, login fails.
3. If `blnActive` is 0 and this is not a registration-purchase redirect, record "Inactive User" and tell the user the account is inactive.
4. Otherwise check the password with `NemoPassword::checkPassword("Learner", LearnerID, password)`.
5. On success, fill `$_SESSION[LEARNER]` (id, user type, category, name, display name, email, active flag, region, typography, picture path) and redirect to `training/index.php`.
6. On bad password, record the failure and offer `forgot.password.php`.

**Special trigger:** `Action=Redirect` looks up the learner by email and `strPasswordMD5`, then forces `Action=Login` and sends the browser to `training/purchase.omw.php?CID=...` instead of the home page. Comment history in the file shows an older redirect to `purchase.php`. Provenance: **Implemented**.

**Output:** session and redirect, or a warning message.

Provenance: **Implemented**. The MD5 column is still read on the redirect path. Normal login uses `NemoPassword`, and the MD5 comparison is commented beside it.

## FR-004 — Password policy

**Who:** learner or admin user going through `NemoPassword`.

**Trigger:** login, reset, or the first run that creates `sysPassword`.

**Behaviour:** `webadmin/_nemo.password.cls.php` stores `hash(protocol, salt + password)`. If `sysPassword` does not exist, the class creates it and inserts `sysSettings` keys, then stops with "PASSWORD SYSTEM INITIATED. REFRESH TO CONTINUE".

Default settings written by that init (provenance: **Implemented** as the bootstrap, **Unknown** as the values actually stored in production):

| Setting | Value written by init |
|---|---|
| `PASSWORD_Enable_Policy` | `1` |
| `PASSWORD_Strength_Check` | `true` |
| `PASSWORD_Strength_Check_MinLength` | `8` |
| `PASSWORD_MaxAge` | `false` |
| `PASSWORD_MaxHistory` | `8` |
| `PASSWORD_Restricted_Words` | `password\|123` |
| `PASSWORD_Reset_Interval` | `60 MINUTE` |

Messages in the class cover mismatch, inactive user, locked account, expiry, reuse, and reset success.

The bootstrap `Type` enum is `'User', 'Supplier'` only. Login calls the checker with type `"Learner"`. Whether the live table accepts `Learner` is **Unknown / Requires Confirmation**.

Reset link expiry is the setting `PASSWORD_Reset_Interval` (comment 2016-10-27). `forgot.password.php` and `webadmin/resetPassword.php` are the entry points.

Admin login (`webadmin/login.php`) shows a notice that passwords were reset and the user should use Forgot Password. Comment in `webadmin/system.php` dated 2017-11-02: "HetznerBreach Reset all password on LIVE and QA". Provenance: **Documented** in that comment. Whether that notice is still appropriate is a product question, not something this PRD decides.

## FR-005 — Learner home

**Who:** signed-in learner.

**Trigger:** `training/index.php`.

**Behaviour:**

- Sets `$System = "Overview"`.
- Shows last login from `sysLogin` when the latest row for that username is not today.
- Loads user category and practice. If the practice has no `refWPSID`, WPS id defaults to `-10`.
- User-category block and WPS block are permanently hidden (comment 2017-03-08, "KM: Feedback PH 3.docx").
- Embeds latest articles with `getTypoIframe2("latestblogposts/?limit=2")`.
- Contains a BAT 2020 pillar query. The practice id in that query is hardcoded to `643`. Provenance: **Implemented**. Whether that id is a sample left in the page is **Unknown / Requires Confirmation**.

## FR-006 — Courses, modules, workshops, WI

**Who:** learner.

**Trigger:** learner navigates the training scripts.

| Journey step | Script |
|---|---|
| Course list | `training/home.courses.php`, `training/course.php` |
| Module, video, assignment | `training/module.php`, `training/module.wi.php` |
| Workshops | `training/home.workshops.php`, `training/workshop.php` |
| Wealth Intelligence / AVP pages | `training/home.wi.php`, `training/__home.wi.php`, `avp.wp.*.php` |
| Tools entry from the learner area | `training/home.tools.php`, `training/login.business_tools.php` |
| Purchases | `training/purchase.php`, `purchase.courses.php`, `purchase.workshops.php`, `purchase.omw.php` |
| Transactions | `training/transactions.php` (records a SagePay process label) |

**Module content:** outcome text, verifiable CPD points (`dblCPDPointsFPI`), FPI CPD type (`refCPDCategory`). IWP modules show extra start copy. A comment marks some CPD as not applicable. Videos can be downloadable files (Flowplayer / HTML5) or YouTube (`strYouTubeID`). One video branch still points at Big Buck Bunny sample media. Provenance: **Implemented**. Whether that sample is still shown to learners is **Unknown / Requires Confirmation**.

## FR-007 — Assignment upload

**Who:** learner.

**Trigger:** `Action` case `Submit` on `training/module.php`, file field `fuAssignment`.

**Inputs:** one or more files, keyed by assignment id. Empty filenames are skipped.

**Behaviour:**

1. Read `modAssessment.strFilename` for this module, assignment, and learner.
2. If no file is stored, allow the upload and set the counter to 1.
3. If a file is stored, take the text before the first `_` in the filename. If that text is not numeric, or it is numeric and less than 3, allow the upload and increment the counter (non-numeric is treated as a new counter of 1).
4. If the numeric counter is already 3 or more, do not upload.
5. On allow, move the file to `DownloadableContentDirLearner` and set `modMilestone.strStatus` to `Submitted`.

The filename pattern is `{counter}_{assignmentId}_{learner}_{original}`.

**Error:** a fourth upload is refused by leaving `allowUpload` at 0. No separate user-facing message was confirmed in the lines that implement the counter.

Provenance: **Implemented**. Commit `313c4dd2` describes this as "ability to upload assignments up to 3 times for IWP courses". The counter in `module.php` is not wrapped in an IWP check in the submit case. Whether the limit is IWP-only is **Unknown / Requires Confirmation**.

`training/my.assignments.php` lists assignments and, by a 2019-03-29 comment, limits the list to the past two years.

## FR-008 — CPD points

**Who:** learner.

**Trigger:** `training/my.cpd.points.php`.

**Behaviour:** the page presents CPD evidence, including an IWP package table (Induction, Wealth Integrator, Initial Engagement and Goal Setting, Developing Scenarios, Joint and Risk Planning, Review) with point values and FPI references in comments. The visible table in the portion read omits the FPI reference column that the commented HTML still has.

Provenance: **Implemented** as a page. The exact query rules were not fully extracted. CPD category enum values also appear on the unmerged succession-service table and match the categories used by courses.

## FR-009 — Forums and profile

**Who:** learner.

**Scripts:** `training/forum.php`, `training/general.forum.php`, `training/profile.php`, `training/learner.profile.php`, `training/message.php`, `training/survey.php`.

Profile pictures are stored under `training/profilepictures` and shown from `Profile:PicturePath` / `ProfileImageDirLearner`.

Provenance: **Implemented**. Forum moderation rules were not extracted.

## FR-010 — Business tools

**Who:** signed-in advisor (`$_SESSION[ADVISOR]`).

**Trigger:** `tools/index.php`.

**Behaviour:** if the advisor session is missing, redirect to `../index.php`. The page can save practice industry fields (`refIndustryID`, `refSubCategoryID`) onto `tblPractice`. The main pane is an iframe of `{LiveURL}/hidden/business-tools`.

Provenance: **Implemented**. `LiveURL` comes from system settings.

### Tool engines on `master`

| Engine | Classes / scripts | Notes |
|---|---|---|
| Business Assessment Tool | `tools/bat.class.php`, `bat.engine.php`, `bat.engine2.php`, `bat.engine3.pwm.php`, `bat_2020.class.php`, `bat_2020.engine.php` | BAT 2020 is the later questionnaire (`tblQQ_2020`, `dwfBat_2020`, `tblCluster_2020`, `tblPillar_2020`). Comments say the BAT 1 / BAT 2 database switch was deprecated (2016-05-06) |
| Client survey (CAT / CSS) | `tools/cat.class.php`, `cat.engine.php`, `cat.pdf.php`, `cat.export.php` | Client satisfaction survey. Optional questions, completion check, invite emails from a template table. Custom multi questions limited to 5 options (comment 2010-06-28) |
| TCF gap | `tools/gap.TCF.class.php` | Breadcrumb label "TCF Gap Analysis" |
| RDR gap | `tools/gap.RDR.class.php` | Same scoring comment pattern as TCF |
| PoPI gap | `tools/gap.PoPI.class.php` | |
| Succession-planning gap | `tools/gap.SCP.class.php` | Comment 2017-02-14: created from the PoPI class. Action-plan section hidden (2017-04-03) |
| Income and expense | `tools/ie.class.php`, `ie.*.sav.php`, `practice.setup.php` | Practice and staff setup saves |
| Other | `lit.class.php`, `bch.class.php`, `bpw.registration.php`, `unj.registration.php` | Present. Product meaning of LIT, BCH, BPW, and UNJ was not confirmed from a requirements note |

Reports: PDF via html2fpdf / FPDF / FPDI, Excel under `tools/Excel` and `webadmin/Excel`. Cover pages include name, surname, and ID (comment 2019-04-03).

## FR-011 — Web admin catalogue

**Who:** admin user with `blnView` on the page.

**Trigger:** scripts under `webadmin/`. The menu is data-driven. These scripts exist and construct Nemo list/detail screens:

Learners, courses, course types, modules, workshops, practices, licences, invoices, vouchers, users, user types, user categories, security groups, CPD points, email, email templates, articles, adverts, clients, contacts, countries, regions, venues, products, RSVP, SLA quotes, SLA services, wealth intelligence, usage log, settings, notes, forums, assignments, practicals, questions, videos.

`webadmin/index.php` shows today's bookings when the user can view `booking.php`, and a two-day usage log (datetime, result, username, system user, active flag, security group) when the user can view `usage.log.php`. Password is not selected. Comments from 2017-11-03 and 2018-07-27 say the password column was removed from this log.

Provenance: **Implemented**. Labels in the menu depend on `sysMenuLevel1` / `sysMenuLevel2` and are **Unknown** without the database.

Toolbar save on detail screens follows `blnSave`. Close and reload are shown. Provenance: **Implemented** in `NemoDetails`.

## FR-012 — Scheduled email

**Who:** the server, via cron hitting the PHP scripts. Scheduler configuration is not in the repo.

**Daily** (`webadmin/cron.daily.php`), from comments and case labels:

- Course start mail.
- Module reminder mail, with an alternate template when the module is feedback.
- CSS reminder, linked to tools.
- AML learner processing and a different email for AML "General Awareness" (2019-03-19).
- IBC learner processing (2018-11-22).

**Hourly** (`webadmin/cron.hourly.php`) case labels:

- `ImportClients`
- `BTInvoice`, `BTOrder`, `Invoice`, `Order` — comment: encrypt and email invoice/orders
- `CPD Learner Import 201612`
- `Certificate Notification Email`
- `Course Start Email`, `Course Complete Email`
- `AdHoc`
- `Workshop Launch`, `Workshop RSVP`, `Workshop IFA Private`
- `Notice`, `Apology`
- `Tools CSS`, `Tools CSS Reminder`
- `CPDPointsNotification`
- `Email Register IBC`, `Email Register AML`

`tools/cron.daily.php`, `cron.hourly.php`, and `cron.weekly.php` also exist, as do `zcron.*` copies that include `webadmin/include/system.inc.php`. Which file the host crontab calls is **Unknown**.

The daily script contains `mail()` calls used as a test send near the top. Provenance: **Implemented**. Leaving them enabled would send on every daily run. Whether production still includes them is **Unknown / Requires Confirmation**.

## FR-013 — Payments

**Who:** learner or practice completing a purchase.

**Behaviour found:**

- `training/transactions.php` labels a process `purchase: SagePay`.
- `webadmin/system.php` selects a Pay Now service key from `$SAGEPAY_SETTINGS` by host (comment 2017-08-25). Keys are credentials and are not recorded here.
- `tools/payGateResults.php` writes `PayGateTransactionID`, `strResults`, and `strDateTime` on `tblTransaction` for the returned `REFERENCE`.
- `tools/purchase.netcash.php`, `tools/unj.netcash.php`, and debit-order wording in `tools/dtPDF.cls.php` name Netcash (Pty) Ltd.

Provenance: **Implemented** as code paths. Which provider is used for which product in production is **Unknown / Requires Confirmation**.

## FR-014 — File integrity (FIM)

**Who:** cron, and every request that includes `webadmin/system.php`.

**Behaviour:** `NemoFIM` sets directories to `0755` and files to `0644` when they differ. `system.php` also runs `find` `chmod` on `training/dcontent` and checks `training/dcontent`, `webadmin/SESSION`, and `webadmin/documents/SLA`. Config files `fim_2.0/fim.config_dev.php`, `fim.config_qa.php`, `fim.config_live.php`, and `fim.config.php` exist.

Provenance: **Implemented**.

## FR-015 — Succession services (not on master)

**Who:** web-admin user, on the unmerged branches only.

**Trigger:** `webadmin/successionservice.php` and `webadmin/successionservice.type.php`, added on `origin/successionservive`.

**Data:** SQL on that branch creates `tblsuccessionservicetype` and `tblSuccessionService`. A service has a type, HTML name, system-assigned code, release mode (`Time Based`, `Module Based`, or `Free`), cost, individual-module flag, start and end dates, pass rate default 60, workshop / WI / RSVP-only / special-code flags, FPI reference, CPD category, CPD class of business, FPI and FSB point fields, and an active flag. Voucher alteration SQL adds `refSuccessionServiceTypeID`.

Provenance: **Planned** relative to `master`. **Implemented** on `successionservive` and on `feature/manual-adhoc-invoice`, which contains those commits. Not merged to `master`.

## Integrations

| System | Direction | What is exchanged | Auth | Failure |
|---|---|---|---|---|
| MySQL | App → database | Learners, courses, tools, settings, mail queues | Username and password in `webadmin/system.php`, selected by HTTP host | Connection failure sets a critical `SystemMessage` and prints it. `display_errors` is on |
| WordPress | Same codebase | Public pages and `latestblogposts` iframes | WordPress keys in `wp-config.php` | Standard WordPress errors. Config in this commit points `WP_HOME` at a localhost path |
| reCAPTCHA | Register pages → Google | Challenge and response | Private key in the register script | `incorrect-captcha-sol` message |
| Sage Pay Pay Now | Purchase → provider | Service key per host | Key in `system.php` | Not traced beyond the key selection |
| PayGate | Provider → `tools/payGateResults.php` | Transaction id, result description, reference | Not confirmed in the lines read | SQL update by reference |
| Netcash | Debit-order copy and purchase scripts | Mandate text names Netcash (Pty) Ltd | Not extracted | Not extracted |
| Mail | App → local mailer | Course, workshop, survey, certificate, AML, IBC | PHP `mail` / `NemoEmail` | Send result stored in `$rst` in some paths. Retry policy not found |
| FTP deploy | Bitbucket Pipelines → `knowmore.overdriveqa.co.za` | Full tree via `git ftp push` on `master` | `FTP_*_LIVE` variables | Pipeline failure is Bitbucket's. No health check in the YAML |
| Wordfence | Request prepend | WAF file at the path in `.user.ini` | Plugin | Unknown if that absolute path exists outside the original host |
| YouTube / Flowplayer | Module page → video host | YouTube id or local file | None in code | Fallback object/embed markup |

## Important constraints

- Host name selects the database. Bare `knowmore.co.za` and `qa-celestis.co.za` are rewritten to the `www` host. A missing `SERVER_NAME` becomes `www.knowmore.co.za` (comments cite cron and Hetzner). Provenance: **Implemented** in `webadmin/system.php`.
- Session lifetime is set to 7200 seconds in that file.
- HTTPS is forced by `.htaccess`.
- Admin rights are per security group and per menu item, not a single admin flag.
- Secrets are stored in PHP source. This PRD does not repeat them.
- No automated test suite covers the Nemo behaviour. Regression safety is **Unknown**.

## Workflows

### Learner enrols and studies

1. Visitor opens a register script and completes reCAPTCHA.
2. A `tblLearner` row is stored active.
3. Learner signs in on `login.php`.
4. Home loads under `training/index.php`.
5. Learner opens a course and module, watches video, uploads an assignment (up to three stored filenames under the counter rule).
6. Milestone status becomes `Submitted`.
7. Cron may send start, reminder, and completion or certificate mail.
8. Learner opens `my.cpd.points.php` for the CPD list.

### Practice runs a tool

1. Advisor session must exist.
2. `tools/index.php` shows the business-tools iframe.
3. BAT, CAT, or a gap engine reads and writes practice results.
4. PDF or Excel report is generated.
5. CSS invites and reminders can also be sent by the hourly cron.

### Admin operates the catalogue

1. Admin signs in on `webadmin/login.php`.
2. Menu is built from the security group.
3. List and detail screens save when `blnSave` is set.
4. Usage log on the home page shows two days of logins without passwords.

## Behaviour that needs a human decision

- Hardcoded practice id `643` on the learner home BAT query.
- Test `mail()` calls in `cron.daily.php`.
- Big Buck Bunny URLs inside `training/module.php`.
- `wp-config.php` localhost URLs beside live host settings in `system.php`.
- Password type `Learner` versus the bootstrap enum `User` / `Supplier`.
- Whether succession services on the 2026 branches are approved scope.
