# Technical documentation — BuildingTomorrow

Evidence date: 2026-10-19.

Source: `master` at `0c7d5dfbf438458c250e03ba76ca8729e0241179` (2024-10-23, "Merged in qa (pull request #122)").

Depth-1 clone. Parent commits of that merge were not fetched. Secret values from `env` and from JWT defaults are described, not copied.

## Stack

| Item | Evidence |
|---|---|
| Language | PHP `^8.0` (`composer.json`, `README.md`) |
| Framework | `codeigniter4/framework` `4.3.8` (exact pin, not a range) |
| Package name | `codeigniter4/adminstarter`, description "Admin Overdrive", homepage `https://www.overdrive.co.za`, license MIT |
| Database | MySQLi. Template: host `localhost`, database `buildingtomorrow`, user `root`, empty password (`env`) |
| Session | `CodeIgniter\Session\Handlers\DatabaseHandler`, table `sessions`, cookie `ci_session`, `sessionMatchIP` false, `sessionTimeToUpdate` 1440, `sessionRegenerateDestroy` true |
| Environment flag in the committed `env` | `CI_ENVIRONMENT = production` |
| Front end | Bootstrap 5, jQuery 3.3.1, DataTables, Feather-style icons, Inter font, gulp-sass. Layout title "Building Tomorrow" |
| Tests | PHPUnit `^9.1`. Suites are the framework samples |
| Node | `gulpfile.js` compiles `public/assets/scss/*.scss` to `public/assets/css/sassCompiled`. `node_modules` is present in the clone |

`vendor/` is present in the clone (the tree is about 6 388 files). `scripts/start_server` still runs `composer update --no-dev` on the server.

## Application structure

```text
app/Controllers          staff, USSD, voice, Salesforce, reports, developers
app/Models               one model per area, plus SalesforceModel queries
app/Views/ussd           USSD and voice behaviour lives in these views, not only in controllers
app/Views/logicbuilder   flow builder
app/Helpers              common_helper.php (session and scoring), salesforce_helper.php, aws_helper.php
app/Filters              Authentication, Authorization
app/Libraries            JwtLibrary
app/Commands             CronjobCommand (spark custom:cronjobcommand)
app/Database/Migrations  2022-11 through 2024-05
app/Database/Seeds       roles, menus, access, users, sample content
public                   web root expected by the vhost example in README.md
scripts                  CodeDeploy hooks
builds                   CodeDeploy packaging area (present at the top level)
```

Controllers under `app/Controllers` (excluding `vendor`):

`Welcome`, `Home`, `Ussd`, `Assessments`, `Assessments_events_handler`, `Household`, `Students`, `Lessons`, `Levels`, `Questions`, `Language`, `Media`, `Settings`, `Profile`, `Users`, `Email`, `EmailTemplate`, `Logs`, `Salesforce`, `Organisations`, `ChatGPT`, `DbAdmin`, `Studentengagementreport`, `Studentanswerreport`, `Studentanswerrpt`, `Droppedcallsreport`, `Redialledreport`, `Performancetracking`, `Initiatedassessments`, and `Developers\` (`MenuManagement`, `CRUDGenerator`, `DbTableDataView`, `Cronjob`).

`Ussd`, `Assessments`, and `Assessments_events_handler` only render a view. The request handling is PHP at the top of `app/Views/ussd/*.php`, which reads `$_POST` directly.

`Routes.php` turns auto-routing on, default controller `Home`, default method `index`. Explicit routes cover login, the phone callbacks, user and menu mutations, developer tools, `organisations` as a resource, and a few report shortcuts. `app/Config/Routes_old.php` and `Filters_old.php` are leftover copies.

## Routing notes

| Path | Handler | Auth |
|---|---|---|
| `GET /`, `POST GetLogin` | `Welcome::index` | Public |
| `GET register`, `POST register` | `Welcome::register`, `registration` | Public |
| `GET ussd` | `Ussd::index` | Public. Africa's Talking is expected to POST; the route is declared GET. CodeIgniter may still deliver POST body to a GET route depending on server rewrite. **Requires Confirmation** on the live vhost |
| `GET assessments` | `Assessments::index` | Public |
| `GET assessments_events_handler` | `Assessments_events_handler::index` | Public |
| `GET cronjob/runScheduledJobs` | `Developers\Cronjob::runScheduledJobs` | Public |
| `resource organisations` | `Organisations` | Public at the filter. Methods check a JWT themselves, except `login` |
| `GET chatGPT`, `GET chatGPT/sent` | `ChatGPT` | Login required. Grant filter skips `chatGPT/*`. The helper and view are missing |
| Other staff URLs | Auto-routed controller/method | Login and menu grant |

CSRF is not enabled, so POST bodies are accepted without a token.

## Data

Database name in the template: `buildingtomorrow`.

Tables created by migrations that are not fully commented out:

| Table | Migration | Role |
|---|---|---|
| `users` | `2022-11-20-124016_Users` | Staff accounts |
| `sessions` | `2022-11-20-124435_Session` | CI sessions |
| `user_role`, `user_access`, `user_menu`, `user_submenu`, `user_menu_category` | 2022-11-20 and 2022-11-25 | Grants |
| `organisations` | `2022-11-20-124016_Organisations` | API clients (name, api_key, address fields) |
| `lessons`, `level`, `question` | 2023-03-06 | Content. Lesson columns include `pass_score`, `type`, `order`, `time_limit`, `level_id`, `language_id` |
| `assessment`, `answer_log`, `enrol`, `contact`, `user_contact`, `cronjob` | 2023-03-06 | Progress and an earlier cron table |
| `student` | 2023-04-03 | Learner. Salesforce id, language, level, lesson, question, name, household, age, gender, `opt_out`, reminder and call times, `active` |
| `household` | 2023-04-18 | Phone, name, language, `opt_out`, `active`, Salesforce id |
| `lesson_flow` | 2023-04-17 | Question graph for a lesson |
| `language`, `log` | 2023-04-03 | Languages and a generic log |
| `salesforce_log`, `settings`, `scheduled_cronjob` | 2023-04-18 | Sync log, key/value settings, cron definitions |
| `sending_queue_log` | 2023-05-04 | Email and voice call queue rows |
| `media`, `templates` | 2023-05-24 | Audio metadata and message templates |
| `africas_talking_log` | 2023-05-25, plus later column migrations | USSD and voice session state |
| `question` language link | `2023-05-25-100100_QuestionLanguage` | Per-language question content |

Later migrations add columns: lesson and lesson-flow extras (2023-08-10), Africa's Talking log fields (2023-08-10), answer-log extra (2023-10-06), lesson-flow response flag (2024-01-18), answer-log number recognition (2024-03-13), `africas_talking_log.call_redialled` (2024-05-01).

`2023-08-10-124016_StudentExtraFields2.php` wraps its `addColumn` calls in a block comment, so a fresh migrate does not add the student Salesforce placement columns. PHP still references `initial_assessment_placement_level_salesforce_id`.

`africas_talking_log.message` stores a JSON session: action, question ids, flow ids, service code, and the digit string. `ussd_menu` stores the last `CON`/`END` text.

## Auth

Staff: password hash, database session, two filters. See business rules BR-001 to BR-007.

Organisations API: custom HMAC JWT in `app/Libraries/JwtLibrary.php`, not a Composer JWT library. Algorithm claim HS256. Default secret parameter is the literal `secret`. Lifetime 6000 seconds from `Organisations::login`.

No captcha, rate limit, or row-level security implementation was found. The README does not claim those controls for this repository.

## Africa's Talking

USSD contract read from `$_POST`: `sessionId`, `serviceCode`, `phoneNumber`, `text`. Response is `text/plain`, lines starting with `CON` or `END`.

Voice playback reads `sessionId`, `isActive`, `callerNumber`, `destinationNumber`. Active calls return `application/xml` with `GetDigits` and `Play`. Inactive calls read `durationInSeconds`, `currencyCode`, `amount`, `status`.

Outbound calls: Africa's Talking SDK voice `call`, username `siliAdmin`, API key from settings, from-number from settings.

SDK package: `africastalking/africastalking` `^3.0`.

## Salesforce

Helper `app/Helpers/salesforce_helper.php` loads SSM parameters at include time:

- `SALEFORCE_BASE_URL` (spelling in code)
- `SALEFORCE_CLIENT_ID`
- `SALESFORCE_CLIENT_SECRET`
- `SALESFORCE_LOGIN_URL`

Grant type `client_credentials`. Token cached in `settings`. Bulk API helpers: `createJob`, `uploadJobRecords`, `closeOrAbortJob`, `getJob`, `getJobResults`.

`Salesforce` sets `set_time_limit(0)` and polls job status inside `handleBulkJobResultsCallback`.

`buildingtomorrow.wsdl` is an Enterprise WSDL, API 57.0, generated 2023-04-07. The header lists Nonprofit Success Pack and related packages. No PHP file references it. **Inferred** it was generated for a SOAP client that this tree does not contain.

TLS peer verification is disabled in the helper.

## AWS

`app/Helpers/aws_helper.php`: SSM `GetParameter`, S3 put and delete, signed URL, secrets manager get. Call sites in `MediaModel` use `MEDIA_UPLOAD_BUCKET` and `MEDIA_UPLOAD_CLOUDFRONT_END_POINT`.

Region and key source for the SDK client are inside `MediaModel` and the helper. Values are not copied here. **Requires Confirmation** of the region and whether the instance role or static keys are used in production. The pipeline exports `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from repository variables for the deploy step only.

## Deployment

`bitbucket-pipelines.yml` image `python:3.5.7`.

| Branch | Step name | Variables |
|---|---|---|
| `master` | Deploy to Prod | `PROD_AWS_ACCESS_KEY_ID`, `PROD_AWS_SECRET_ACCESS_KEY`, `QA_AWS_DEFAULT_REGION`, `PROD_APPLICATION_NAME`, `PROD_DEPLOYMENT_GROUP_NAME`, `PROD_S3_BUCKET`, `DEPLOYMENT_CONFIG` |
| `qa` | Deploy to QA | `QA_AWS_*`, `QA_APPLICATION_NAME`, `QA_DEPLOYMENT_GROUP_NAME`, `QA_S3_BUCKET`, `DEPLOYMENT_CONFIG` |

The production step uses `QA_AWS_DEFAULT_REGION` for the region. That may be intentional sharing of one region variable. **Requires Confirmation.**

Both steps: `apt-get install zip`, `pip install boto3==1.3.0`, `zip -r /tmp/artifact.zip *`, `python codedeploy_deploy.py`.

`appspec.yml`: Linux, source `/` to `/var/www/html`, owner `www-data`, overwrite. Hooks: `BeforeInstall` `scripts/install_dependencies` (commented yum line only), `ApplicationStop` `scripts/remove_archives.sh`, `ApplicationStart` `scripts/start_server`.

`start_server`: `composer update --no-dev`, start httpd if a process exists, start apache2 if a process exists, `php spark migrate`. The process checks assign with spaces around `=` (`isExistApp = \`pgrep\``), which is not valid bash. **Inferred** the service-start lines do not run. Migrate still runs if the script is invoked with `bash` and reaches that line. **Requires Confirmation** against a real deploy log.

`README.md` also documents a WAMP vhost `adminOverdrive.local` with document root `public`. That is a local starter instruction, not the CodeDeploy path.

SonarQube pipe `sonarsource/sonarqube-scan:2.0.1` is commented out. `sonar-project.properties` sets `sonar.projectKey=overdrive_buildingtomorrow_AYpSmPCcQY-eSvjrqMos`.

`sonarqube_scan.bat` is at the repository root for a local scan. It was not executed.

## Background work

There is no queue daemon in the repository.

Two entry points call the same Salesforce loop:

- HTTP `Developers\Cronjob::runScheduledJobs`
- CLI `php spark custom:cronjobcommand` (`app/Commands/CronjobCommand.php`)

The CLI command defines `BASEPATH` itself and requires `vendor/autoload.php` again. It does not go through spark's usual bootstrap beyond what `BaseCommand` already did. **Requires Confirmation** that `spark custom:cronjobcommand` still constructs `Cronjob` without a request object. The HTTP path is the one the filters know about.

`sending_queue_log` stores outbound email and call rows. `Email::resendEmail` resends from the admin screen. No worker was found that polls the table.

## Front end build

`gulpfile.js` task `sass` compiles `public/assets/scss/*.scss`. The layout links `assets/css/sassCompiled/logicbuilder.css` and, on `/lessons/logicbuilder` only, Select2 4.0.6-rc.0 from cdnjs.cloudflare.com.

AdminKit-style class names (`navbar-bg`, sidebar toggle) are in the layout. The README calls the project Admin Overdrive. A specific AdminKit version pin was not found in `package.json` because that file is not at the repository root (a `package-lock.json` is listed in the task's original tree; this clone's lockfile was not opened).

## Testing and local run

`README.md` setup:

1. Copy `env` to `.env`.
2. `php spark db:create`
3. `php spark migrate`
4. `php spark db:seed users`
5. `php spark key:generate`
6. `php spark serve`

PHPUnit config: `phpunit.xml.dist`, bootstrap from the framework, coverage on `./app` excluding views and `Routes.php`. The App suite includes the example tests only. The Bitbucket pipeline does not run PHPUnit.

## Security notes found in the tree

These are observations, not a penetration test.

- Organisations JWT default secret is a short literal.
- Salesforce cURL disables certificate checks.
- Cron HTTP endpoint is unauthenticated and runs every active sync.
- Self-registration assigns the Developer role.
- CSRF filter is off.
- `env` is committed with `CI_ENVIRONMENT = production` and an encryption key.
- Voice callback rewrites one caller number.
- USSD and voice views use `$_POST` and echo provider responses with limited validation of the caller.
- `assessments.php` `var_dump`s exception messages to the response.

## File map for the next reader

| Question | Start here |
|---|---|
| What does a handset see? | `app/Views/ussd/ussd.php` |
| How is a learner saved? | `app/Models/USSDModel.php` `ussd_enrol_user`, `createNewHouseholdStudent` |
| How is a call scored? | `app/Views/ussd/assessments_events_handler.php` |
| How does Salesforce get data? | `app/Controllers/Salesforce.php` |
| Where are credentials loaded? | `app/Helpers/salesforce_helper.php`, `app/Helpers/aws_helper.php` |
| Who may open a screen? | `app/Config/Filters.php` |
| How is it deployed? | `bitbucket-pipelines.yml`, `appspec.yml`, `scripts/start_server` |
