# Technical documentation — findndrive_pwa

Evidence date: 2026-10-15. Source: `git@bitbucket.org:overdrive/findndrive_pwa.git`, `master` at `d61d13b` (2023-08-08). `origin/HEAD` is `master`. No other remote branches and no tags were present.

The onboarding issue listed `sonarqube_scan.bat` in a scan-host snapshot. That file is not tracked in this revision. The issue's `src\nvite.config.js` line is `src/` plus `vite.config.js`.

## Summary

Browser SPA. React 18 renders a login gate and a four-step vehicle-loan simulator. Formulas run in the browser. Each screen also POSTs JSON to the Find And Drive `CarApi`. Vite builds a static `dist/` folder. `master` uploads that folder to S3 and invalidates CloudFront.

There is no database, no server process, no queue, and no background worker in this repository.

## Stack

| Item | Value | Evidence |
|---|---|---|
| Language | JavaScript (ES modules) | `package.json` `"type": "module"` |
| UI | React 18.2.0, react-dom 18.2.0 | `package.json` |
| Routing | react-router-dom 6.11.2 | `package.json`, `App.jsx` |
| Styling | styled-components 6.0.0-rc.3, a vendored `bootstrap.min.css`, react-bootstrap 2.7.4 | `package.json`, `App.jsx` |
| HTTP | axios 1.4.0 | `ApiCall.js` |
| IDs | react-uuid 2.0.0. The `uuid` package is also a dependency and is not imported | `package.json`, `AppContext.jsx` |
| Icons and font | react-icons 4.9.0, `@fontsource/source-sans-pro` 5.0.5 | `package.json`, `main.jsx` |
| Build | Vite 4.3.9, `@vitejs/plugin-react` 4.0.0 | `package.json`, `vite.config.js` |
| Lint | ESLint 8.38 with `eslint-plugin-react`, react-hooks, react-refresh | `.eslintrc.cjs` |
| Package name / version | `fnd` / `0.0.1` | `package.json` |
| Page title | Find&Drive | `index.html` |

Scripts: `dev` (`vite`), `build` (`vite build`), `lint` (`eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0`), `preview` (`vite preview`). There is no `test` script and no test directory.

`@types/react` and `@types/react-dom` are dev dependencies. The source is JavaScript, not TypeScript.

## Application structure

```text
index.html                  mounts /src/main.jsx into #root
vite.config.js              React plugin and /CarApi dev proxy
bitbucket-pipelines.yml
sonar-project.properties
public/                     vite.svg, findndrive_icon.png
src/main.jsx                providers, then App
src/App.jsx                 login gate and routes
src/context/                AppContext, FormContext, UserContext
src/pages/                  one folder per screen, plus a Styles file
src/components/             header, footer, inputs, cards, sliders, modals
src/util/                   API, finance, insurance, fuel, risk, tables
src/styles/                 vendored Bootstrap and shared typography
src/assets/                 Absa logos, car drawings, result images
```

`main.jsx` wraps the tree as `UserProvider` → `AppContextProvider` → `FormProvider` → `App`. Strict mode is not used. Commit `280a9bf` records it being removed.

### Routes

Defined in `App.jsx`. Paths other than `/` are written without a leading slash (`home`, `select_a_car`, …). `exact` is passed and has no effect in React Router 6.

Logged out, every path renders `Login`.

Logged in:

| Path | Screen |
|---|---|
| `/`, `/home` | Home |
| `/select_a_car` | Step 1 |
| `/select_model` | Model list |
| `/average_usage` | Km bands |
| `/cost_of_ownership_1` | Tier cards |
| `/calculator` | Finance calculator |
| `/chances_of_approval` | Salary and expenses |
| `/result_options` | Risk result |
| `/select_model_recommended` | Cheaper models |
| `/cost_of_ownership_recommended` | Recommended tier cards |
| `/recommended_option` | Recommended risk result |
| `/congratulations` | Apply complete |
| `/fix_my_credit` | Credit-assistance message |
| `/about` | Stub |
| `/error` | Data-load error |
| any other | Not found |

Header and footer render only after login. The error screen from a failed login post is requested by `navigate`, then replaced by `Login` because the flag is still false.

### State

`AppContext` holds the fields that are posted: `app_id`, `session` (initial `"0"`), car type, model, km, car ids, repayment, salary, expenses, debt, living costs, savings, max instalment, deposit, credit statement, term, balloon, estimated repayment, interest rate. Setters are named `update` plus the field.

`FormContext` holds the UI copies: card arrays, selected cards, instalment text, calculator inputs, `primeInterestRate` default `12.75`, and `loged_in`.

The two stores overlap. Pages read both. Nothing is persisted. A reload remounts the providers, so `app_id` is a new UUID and the visitor is logged out.

`UserContext` is unused by pages.

## API client

`callApi(endpoint, payload)` always uses HTTP POST and returns `res.data`. It does not catch errors. Callers use try/catch and navigate to `/error`.

```text
production (Vite sets NODE_ENV=production for `vite build`):
  https://admin.findndrive.co.za/CarApi/<endpoint>

any other NODE_ENV (vite dev):
  /CarApi/<endpoint>
  proxied to https://admin.findndrive.co.za with changeOrigin, secure: false
```

No timeout, no retry, no `Authorization` header, no `withCredentials`. The comment in `ApiCall.js` says the proxy must not be used in production.

`SelectACar` and `SelectModelRecommended` also `axios` GET `https://geolocation-db.com/json/` and copy `country_name`, `country_code`, `city`, `state`, `postal`, and `IPv4` into the step payload. There is no fallback if that host fails.

### Step methods this client calls

| Endpoint | Caller | Response used |
|---|---|---|
| `step0_login` | `Login` | Truthy check only. The body becomes `loged_in` |
| `step1_range_car` | `SelectACar`, `SelectModelRecommended` | `data.session`, `data.data` type rows |
| `step2_model` | `SelectModel` | `data.data` models with tier prices |
| `step2_model_km` | `AverageUsage` | Ignored |
| `step2_result` | `CostOfOwnership1` | `data.data` models |
| `step2_calculator` | `Calculator` | Ignored. Local price comes from the selected card |
| `step3_affort` | `ChancesOfApproval` | Ignored |
| `step4_maintenance_and_services` | `ResultOptions`, `RecommendedOption` | `maintenance` and `services` objects |
| `step4_summary` | `CostOfOwnershipRecommended`, `RecommendedOption` | `data.data` on the cost screen. The result screen ignores it |
| `step4_recommend_confirm` | `SelectModelRecommended` | `data.data` models |
| `step4_congratulations` | `Congratulations` | Ignored |
| `step4_fix_my_credit` | `FixMyCredit` | Ignored |

The server that implements these names is `findndrive_webportal`. This client does not vendor that code.

Payloads are a growing set of context fields. Later screens repeat earlier ids (`app_id`, `session`, car, km, deposit, term, salary). `step4_fix_my_credit` sends `fix_my_credit` as an empty string. The webportal documentation records a server key with a trailing space. See `business-rules.md`.

## Local calculations

| Module | Role |
|---|---|
| `FinanceCalculate.js` | Instalment, fees, balloon, interest total |
| `CalculateInsurance.js` | Percent of instalment by `car_type` |
| `calculateFuelSpent.js` | Litres from consumption and km, times 22.46 |
| `GetCarTiers.js` | One tier card when price is usable and the 72-month instalment fits |
| `RiskLevelCalculation.js` | Bands, overall level, `canApply`, recommended ceiling |
| `MaintenanceTableData.js`, `ServiceTableData.js` | Row labels, doubled brake costs, months until the next interval |
| `FormatNumber.js` | `R` currency, comma stripping, upper km bound |

Numeric rules are listed in `business-rules.md`. They are not configured outside the source.

`formatValue` is passed into the fuel function, so consumption is first turned into a two-decimal string and then coerced back to a number.

## Auth

Login is a single POST. A truthy JSON body flips `loged_in`. There is no token, cookie, refresh, or expiry handling in this repo. The flag is a React state value, so it ends when the tab's JS context ends.

The logged-in routes are not otherwise protected. Anyone who has the flag can open `/calculator` directly; the screen will still POST with whatever context still holds, including the initial `"0"` ids if the earlier steps were skipped.

## Data

This repository does not own a database.

Session identity is `app_id` (client UUID) plus `session` (string returned by `step1_range_car`). Catalogue fields the UI reads from responses include `car_type`, `car_model`, `id`, `tiera`/`tierb`/`tierc`/`tierd` and the matching `_image_path` fields, `petrol_consumption`, and the maintenance and services maps.

Sample year, mileage, and the word "Pre-owned" are constants in the cost-of-ownership pages, not response fields.

## Infrastructure and deployment

`bitbucket-pipelines.yml` uses image `node:16.15.0`.

| Trigger | Steps | Output |
|---|---|---|
| Branch `master` | `npm install` (tests commented out), then `npm install` and `npm run build`, then `atlassian/aws-s3-deploy:0.2.4`, then `atlassian/aws-cloudfront-invalidate:0.1.1` | `dist/` uploaded to `$PRODUCTION_BUCKET_NAME`. Cache invalidation uses `$PRODUCTION_DISTRIBUTION_ID`. ACL line is commented out |
| Branch `dev` | `npm install` only. Test line commented out | No deploy. No `dev` branch exists on the remote |
| Custom pipeline `staging` | Deletes `package-lock.json` and `node_modules`, `npm install`, `npm run test`, then `npm install --production` and `REACT_APP_ENVIRONMENT=staging npm run build`, then S3 of `build/` to `$STAGING_BUCKET_NAME`, then CloudFront `$STAGING_DISTRIBUTION_ID` | This path does not match the current Vite app. `npm run test` is not a script. Vite writes `dist/`, not `build/`. `REACT_APP_ENVIRONMENT` is not read by `ApiCall.js` |

AWS key variable names: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`. Values are not in the repo. No `.env` file is tracked. `.gitignore` ignores `*.local`, `node_modules`, and `dist`.

A production `vite build` sets `NODE_ENV` to `production`, so the built site calls the admin host directly. A staging build that used `npm run build` would do the same. There is no staging API host in source.

Bucket names, distribution ids, and the public URL of the site are **Unknown**.

## Monitoring and quality

`sonar-project.properties` contains only:

```text
sonar.projectKey=overdrive_findndrive_pwa_AYlFc0cWp5diRRkAyQha
```

The pipeline has no Sonar step. No application logging, error reporting service, or analytics call was found. Failures are `console`-silent: the catch navigates to the error page.

## Security notes visible in the tree

- Login passwords are posted in a JSON body to `CarApi`. They are not stored in this repo.
- The dev proxy sets `secure: false` toward the HTTPS admin host.
- Geolocation, including IPv4, is sent to `CarApi` on the two price-range screens.
- Footer and social links use `href=""` or `href="#"`. They do not leave the page.
- No rate limiting, captcha, or CSRF token exists in the client.
- Dependency versions are the July 2023 ranges in `package-lock.json`. Whether they are still acceptable is **Requires Confirmation**. This document does not upgrade them.

## Development and testing

Local development is `npm install` then `npm run dev`. The Vite dev server proxies `/CarApi`. No port override is set, so Vite's default applies. No README states the port or the login identities.

Lint is available and is not called by the pipeline.

No PHPUnit, Jest, or Playwright tests exist. Commit `50c41ec` (2023-07-18, "remove the npm test") removed the test script. The custom staging pipeline still calls it.

`UserContext` and the `uuid` dependency are unused. Several components remain in the tree after their call sites were commented out (`HeaderButton`, `HeaderIcon`, `FndInputValue`). They are not routed.

## Related system

`silicon_ark/findndrive_webportal` documents the PHP admin and `CarApi`. As of this evidence date, that set still says the simulator client is not in the webportal repository. This repository is that client: the step names and the host `admin.findndrive.co.za` match. Updating the webportal pages to point here was left out of this change. **Requires Confirmation** before those pages are edited.
