# Business rules — findndrive_webportal

Evidence date: 2026-10-15. Source: `git@bitbucket.org:overdrive/findndrive_webportal.git`, `master` at `54b9de7`.

Rules are what the code enforces. A rule marked **Inferred** is a reasonable reading of that code, not a signed policy. Secret values are omitted.

## Identity and access

### BR-001 — Staff password check

A staff login succeeds only when `users.username` equals the posted email and `password_verify` accepts the posted password against `users.password`.

Provenance: **Implemented** (`Welcome::index`).

There is no lockout, expiry, or second factor in this method.

### BR-002 — Same error for unknown user and bad password

Both failures set the flash message "Your ID or Password is Wrong" (the unknown-user string has no space before the closing bang).

Provenance: **Implemented**.

### BR-003 — Session is the staff credential

Later staff pages trust `session.isLoggedIn`, `session.username`, and `session.role`. The session handler in the sample `env` is the database handler, cookie `ci_session`, match IP on, regenerate-destroy on.

Provenance: **Implemented** in `env`. `App` config may override this when `.env` is absent. Which file production uses is **Unknown**.

### BR-004 — Registration assigns Developer

`inputRole` posted by the browser is ignored. The insert always uses role `1`.

Provenance: **Implemented**. Product approval: **Requires Confirmation**.

### BR-005 — Username must be unique and passwords must match

Registration validates `is_unique[users.username]` and `matches[inputPassword]`. Password strength is only `required`.

Provenance: **Implemented**.

### BR-006 — First URL segment must be a granted menu

If segment 1 is not on the except list:

- no menu row for that URL → redirect `/`;
- menu exists but `user_access` has no row for this role and menu → redirect `blocked`.

Provenance: **Implemented**.

### BR-007 — Exceptions that skip the grant

`/`, `getLogin`, `register`, `blocked`, `home`, `Welcome/*`, `organisations`, `organisations/*`, `chatGPT/*`, `carapi`, `carapi/*`.

`home` skips the grant check, so the dashboard does not require a Dashboard menu row for that role.

ChatGPT skips the grant check but does not skip the login check. Organisation and car API prefixes skip both.

Provenance: **Implemented**.

### BR-008 — Developer is the only seeded role

`user_role` seed inserts id 1, name `Developer`. The users seed then calls access, menu, and role seeders.

Provenance: **Implemented** seed. Live roles: **Unknown**.

### BR-009 — Seeded staff user

The users seeder inserts full name "Developer Tester", username `jaco@overdrive.co.za`, role 1, and a hash of a short numeric password. The password is not repeated here.

Provenance: **Implemented** seed. Whether this account exists in production: **Unknown**.

## Organisation API

### BR-010 — Login is name plus api_key

`Organisations::login` loads JSON `name` and `api_key` and selects a row where both match. Success returns a bearer JWT. Failure returns 404 "No user found".

Provenance: **Implemented**. The api_key is stored and compared in plaintext.

### BR-011 — Token lifetime

JWT payload is `name` and `exp = time() + 6000` (100 minutes). Algorithm header is HS256. The HMAC key is the library default, not an environment value.

Provenance: **Implemented**.

### BR-012 — Organisation writes require a valid JWT

Index, show, create, update, and delete call `is_jwt_valid`. Invalid or missing tokens return "Unauthorized Access".

Provenance: **Implemented**.

### BR-013 — Seeded organisation key

The organisations seeder inserts `silicon overdrive` with a short numeric `api_key`. The value is not repeated here.

Provenance: **Implemented** seed.

### BR-014 — Success copy says Employee

Create, update, and delete messages say "Employee", while the table is `organisations`.

Provenance: **Implemented** text. **Inferred** copy leftover, not a second entity.

## Simulator

### BR-015 — Steps do not check the bearer token

Each `step*` method sets `$is_jwt_valid = true`. The call to `is_jwt_valid` is commented out. A caller who can reach the host can post a step without a token.

Provenance: **Implemented**.

### BR-016 — Step 0 is two fixed accounts

`step0_login` returns `true` only for username `developerTest` or `absaTestUser` with the passwords written in `CarApi.php`. Any other pair returns `false`. No database user is read. No session row is created.

Provenance: **Implemented**. Partner meaning of the Absa username: **Requires Confirmation**.

### BR-017 — A new simulator session id is `uniqid` of `app_id`

`step1_range_car` sets `session` to `uniqid($json->app_id)` and inserts country, country code, city, state, zip, and ip.

Provenance: **Implemented**.

### BR-018 — Later steps update by session string

`updateAppSessionSpesific` runs `UPDATE app_session SET <posted fields> WHERE session = <posted session>`.

If no row matches, the update affects zero rows and the method still returns catalogue data when the catalogue query is non-empty.

Provenance: **Implemented**. **Inferred**: a mistyped session does not stop the response.

### BR-019 — The server does not calculate affordability

`max_installment`, `repayment`, `deposit`, `payment_term`, `balloon`, `est_repayment`, `interest_rate`, `net_salary`, `total_expenses`, `debt_expenses`, `living_expenses`, and `savings` are stored as supplied. No tax, credit score, or instalment formula exists under `app/`.

Provenance: **Implemented**. **Inferred**: the client owns the numbers.

### BR-020 — Type filter is a comma-separated OR

`getCarModel` and `getCarRecomendedModel` split `car_type_list` on commas and build `car.car_type="…" OR car.car_type="…"`. The fragments are concatenated into SQL.

Provenance: **Implemented**. **Inferred** risk: the list is not bound as parameters.

### BR-021 — Model match is exact

`getSelectedCars` and `getRecommendedCar` compare `car.car_model` to the posted model string.

Provenance: **Implemented**.

### BR-022 — Maintenance is the newest category A row; services are the newest category B row

Both queries filter `car_id`, `ORDER BY id DESC`, `LIMIT 1`. Category C (body parts) is not returned by these methods.

Provenance: **Implemented**. The labels "maintenance" and "services" are the PHP variable names, not column titles.

### BR-023 — Empty catalogue is a 404

When the catalogue query returns an empty result, the step responds `failNotFound`. `step2_model_km` does not query the catalogue; it returns the posted object whenever that object is non-empty.

Provenance: **Implemented**.

### BR-024 — Apply and fix-my-credit are stored flags

`apply` is written from the JSON field `apply`. The dashboard treats any non-null `apply` as applied, and any non-null `fix_my_credit` as fix-credit. It does not inspect the flag's value.

Provenance: **Implemented** for the read. The write in `step4_fix_my_credit` uses the key `fix_my_credit ` with a trailing space, so the column `fix_my_credit` may stay null. **Requires Confirmation** against a stored row.

### BR-025 — Listing every session is open

`GET` on the `carapi` resource returns all `app_session` rows with no JWT check.

Provenance: **Implemented**.

### BR-026 — `new_session` mints a JWT for a known session string

If a row with that `session` exists, the method returns a JWT for the posted `name`. It does not check `step0` credentials.

Provenance: **Implemented**.

## Catalogue

### BR-027 — Import identity is vehicle name plus type

`importToCar` selects `name` + `car_type`. A hit updates. A miss inserts. `created_by` and `updated_by` on this path are `0`.

Provenance: **Implemented**.

### BR-028 — Import starts at row 5

Rows 1–4 are skipped. A null vehicle cell is skipped.

Provenance: **Implemented**.

### BR-029 — Import persists a subset of columns

`createCar` / `updateCar` write name, price, car type, insurance, and tiers A–D. Petrol consumption, `car_model`, and tier image paths are placed in the import array and then ignored by those two methods.

Provenance: **Implemented**.

### BR-030 — Cost rows are inserted when the same value set is not already present

Category A, B, and C imports select by the full value array. If a row exists, the code path does not insert another. The duplicate check includes `car_id`.

Provenance: **Implemented** (`importToCatagoryA` and the B/C twins).

### BR-031 — Tier letters A–D

Image update switches on `A`, `B`, `C`, or `D` and writes the matching `tier*_image_path`. Other letters do nothing (the switch has no default).

Provenance: **Implemented**.

### BR-032 — Seeded body styles

Car type seed names are Sedan, SUV, Hatchback, and Coupe. The simulator's type list is the distinct `car.car_type` strings, which the spreadsheet writes from the vehicle-type column. Those strings are not required to match the seed names.

Provenance: **Implemented** seed and **Inferred** disconnect between `car_type` table and `car.car_type`.

### BR-033 — Cost category meaning

From the columns, not from a glossary:

- Category A: service-interval parts (filters, plugs, wiper, coolant, steering fluid).
- Category B: wear items (cam belt, brakes, shocks, fan belt, radiator, battery, tyres).
- Category C: body and glass, plus battery charger and electric motor.

Provenance: **Inferred** from column names. No document in the repo defines the letters.

## Dashboard arithmetic

### BR-034 — Fix-credit percentage

`count(sessions where fix_my_credit IS NOT NULL, grouped by app_id) / count(all sessions) * 100`, formatted with 0 decimal places.

`getAppSessionWhere` groups by `app_id` by default. `getAppSession` does not group. The numerator and denominator are not the same kind of count.

Provenance: **Implemented**. **Inferred**: the percentage is not "share of sessions" in the plain sense.

### BR-035 — Today means the database current date

Active sessions use `date(created_at) = CURDATE()`. Charts use `created_at` after `CURDATE() - 7 days`.

Provenance: **Implemented**. Time zone of the database is **Unknown**.

### BR-036 — Did-not-finish

A session counts as did-not-finish when both `fix_my_credit` and `apply` are null.

Provenance: **Implemented** in `getChartOutcome`.

## Other behaviour that acts as a rule

### BR-037 — CORS is open

Every response is prepared with `Access-Control-Allow-Origin: *`. `OPTIONS` ends the request.

Provenance: **Implemented**.

### BR-038 — CSRF is off

The CSRF filter alias exists and is not in the global before list.

Provenance: **Implemented**.

### BR-039 — ChatGPT prompt is sent as typed

`chatGPT/sent` and master-file processors pass the message or the record name plus the words "tasting notes" to the completions API. Results for the master file are written back onto the row. Token counts go to `chatgpt_log`.

Provenance: **Implemented**. A usage cap was not found.

### BR-040 — Mail from address

`sendMail` sets From to `php@overdrive.co.za`, display name "Admin Overdrive". A previous from-address is commented out. No active controller calls `sendMail`.

Provenance: **Implemented** helper, inactive call sites.

## Conflicts

| Topic | Code | Other evidence |
|---|---|---|
| Product name | Composer description "Find And Drive" | README title "Admin Overdrive Starter" |
| Organisation messages | Table `organisations` | JSON messages say "Employee" |
| JWT on the simulator | `JwtLibrary` and comments in `Routes.php` describe a bearer token | Step methods force the check to true |
| Affordability | Session stores salary, expenses, interest, balloon | No calculation in this repo |
| Database secrets | `aws_helper.php` can read Secrets Manager in `us-east-1` | `Database.php` does not call it; credentials are empty |
| Fix-my-credit | Dashboard reads `fix_my_credit` | Writer key has a trailing space |

These are recorded, not resolved.
