# PRD — findndrive_pwa

Evidence date: 2026-10-15. Source: `git@bitbucket.org:overdrive/findndrive_pwa.git`, `master` at `d61d13b` (2023-08-08, "adding tier to app session and readd login").

There is no prior PRD in the repository or in Silicon Ark. Requirements below were recovered from pages, context, formulas, and the pipeline. Provenance is marked on each item. Secret values are omitted. Passwords are not stored in this repository.

The browser is the simulator. `findndrive_webportal` stores the posted session and returns catalogue rows. This PRD describes only what this repository does.

## Product purpose

Find&Drive is a guided vehicle-loan simulator branded with Absa assets. After a login probe, the visitor picks a monthly budget and a body style, narrows to a model and a monthly distance, reviews a sample car's repayment, insurance, and fuel, then enters deposit, credit behaviour, term, and balloon. They enter net salary, debt expenses, living expenses, and savings. The app shows a risk breakdown and offers apply, fix-my-credit, or a lower instalment range.

The catalogue cars are described on screen as calculation examples, not stock. The completion screens do not open a bank application. They post the session and show a thank-you, with an email address for feedback.

Provenance: **Implemented** (`index.html` title, `Home.jsx` copy, `FndDisclaimer.jsx`, `Congratulations.jsx`).

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Visitor | Completes the four-step simulator after login | Pages under `src/pages` | Implemented |
| Login identity | Posts `username` and `password` to `step0_login`. This repo does not decide which pairs succeed | `Login.jsx`, `ApiCall.js` | Implemented here. Acceptance rules are in `findndrive_webportal` |
| Staff / admin | Not present in this repository | No staff screens | Implemented absence |

`UserContext` holds `user` and `age` and is never read. No age capture exists on a screen. Provenance: **Implemented** unused context.

There is no role, permission, or menu system in this app.

## Main workflows

### W1 — Login gate

Trigger: any URL while `loged_in` is false. Initial value is `false`.

Inputs: username, password, and a client-generated `app_id` (UUID from `react-uuid`, created when `AppContext` mounts).

Behaviour:

1. `App.jsx` renders only `Login` on every path.
2. Submit posts `{ app_id, username, password }` to `step0_login`.
3. If the response is truthy, `loged_in` becomes that response and the app navigates to `/home`.
4. If the response is falsy, the visitor stays on the login form. No error text is set.
5. If the request throws, the code navigates to `/error`. The logged-out router has no error route, so the visitor still sees `Login`.

Output: in-memory flag. Refresh clears it. Provenance: **Implemented**.

Empty username or password is still posted. There is no client-side format check.

### W2 — Home

Trigger: `GET /` or `/home` after login.

Behaviour: heading "Navigate your Vehicle Loan journey with confidence", a start button to `/select_a_car`, and the stock disclaimer. A "How it works" button is commented out.

Provenance: **Implemented**.

### W3 — Step 1, price range and body style

Trigger: `/select_a_car`.

Behaviour:

1. GET `https://geolocation-db.com/json/`.
2. POST `step1_range_car` with `app_id`, current session (initially `"0"`), geolocation fields, and the journey fields held in `AppContext`.
3. Store `data.session` from the response.
4. Build four cards from `data.data` by fixed index: Hatchback `[0,1,2]`, Sedan `[3]`, Bakkie `[6,7]`, SUV `[4,5,8]`. Each card keeps the API `car_type` strings in `carTypes`.
5. The visitor types a rand amount and/or moves a slider. Slider stops are R0, R5,000, R7,500, R10,000, R12,500, R15,000. A typed amount snaps the slider to the nearest band but the typed value is what is stored.
6. Continue is enabled only when a body style and an instalment value are set. It saves `max_installment` and opens `/select_model`.

If geolocation or the API throws, navigate to `/error`. Provenance: **Implemented**.

The index mapping assumes at least nine rows in a stable order. A shorter list throws and becomes the error screen. Whether that order is guaranteed by the API is **Requires Confirmation**.

### W4 — Step 2, model

Trigger: `/select_model`.

Behaviour:

1. Display the range as `0 - <typed maximum>`.
2. POST `step2_model` with `car_type_list` equal to the selected card's `carTypes` joined by commas.
3. For each returned model, price tiers D, C, B, then A. A tier is kept when its price is not `0` or null and a 72-month, zero-deposit, zero-balloon instalment at prime `12.75` is less than or equal to the visitor's maximum.
4. Later tiers overwrite earlier ones. If several tiers fit, the card shows the last one that fit. A is checked last, so A wins when it fits.
5. Continue requires a body style and a model, then opens `/average_usage`.

Provenance: **Implemented**. The card does not show year, mileage, or the tier letter. Those appear on the cost-of-ownership screen.

### W5 — Monthly kilometres

Trigger: `/average_usage`.

On load, POST `step2_model_km`. The response body is not used. The distance choices are fixed in the page:

- 500 km – 999 km
- 1,000 km – 1,499 km
- 1,500 km – 1,999 km
- 2,000 km – 2,499 km
- 2,500 km – 2,999 km
- 3,000 km – 3,499 km

Confirm requires a selection and opens `/cost_of_ownership_1`. Later fuel and maintenance math uses the top of the chosen band (`getMaxDistance`). Provenance: **Implemented**.

### W6 — Cost of ownership

Trigger: `/cost_of_ownership_1`.

Behaviour:

1. POST `step2_result`.
2. For each model, estimate fuel from `petrol_consumption` and the top of the km band.
3. Build a card per tier A–D that passes the same instalment filter, using hardcoded sample year and mileage (see business rules). Condition is always "Pre-owned".
4. Select the first card. Previous/next move within that list.
5. Show monthly repayment, insurance, fuel, and their sum.
6. A question control opens the tier explanation modal.
7. Confirm opens `/calculator` and does not post again.

Provenance: **Implemented**.

### W7 — Finance calculator

Trigger: `/calculator`.

On load, POST `step2_calculator` including `tier` from the selected card, and set the vehicle price from that card.

The visitor enters:

- Deposit. Default state is the string `"0"`. The price field is read-only.
- One credit statement. That choice sets the interest rate. The rate field is read-only.
- Payment term: 24, 36, 48, 60, or 72 months.
- Balloon percent, in steps of 5, from 0 up to a calculated maximum.

Continue requires deposit, credit selection, interest rate, and payment term. Because the deposit default is `"0"`, a zero deposit satisfies the check. Balloon is not required for the check. The max balloon is applied by the slider.

Continue stores the values on `AppContext` and opens `/chances_of_approval`. Provenance: **Implemented**.

A disclaimer under the figures says the simulator is for convenience, gives no warranty, and accepts no liability. Provenance: **Documented** in the page copy and **Implemented**.

### W8 — Financial details

Trigger: `/chances_of_approval`.

On load, POST `step3_affort` with the calculator fields. The response is not read.

The visitor enters net salary (labelled "After deductions"), debt expenses, living expenses, and total savings. Continue is enabled only when all four values are truthy. Numeric `0` does not enable the button.

On continue, total expenses become debt plus living, those values are copied onto `AppContext`, and the app opens `/result_options`. Provenance: **Implemented**.

### W9 — Results

Trigger: `/result_options`.

Behaviour:

1. POST `step4_maintenance_and_services` with salary, expenses, debt, living costs, and savings.
2. Build maintenance and service tables from the response.
3. Run `riskLevelCalculation`.
4. Heading depends on the overall risk level: low uses the "might fit your budget" copy; any other level uses "might not fit".
5. An accordion shows risk profile, affordability, credit behaviour, debt level, and vehicle age/mileage. "Get more information" opens a modal of the same breakdown.
6. Buttons:
   - Explore other options when risk is not low and the recommended top instalment is not below 1700. Opens `/select_model_recommended`.
   - Apply now when risk is low or medium, or `canApply` is truthy, and `canApply` is not `false`. Opens `/congratulations`.
   - Fix my credit when risk is high or `canApply` is `false`, and `canApply` is not `true`. Opens `/fix_my_credit`.
   - Go back to `/chances_of_approval`.

`canApply === false` hides Apply even when risk is low. `canApply === true` can show Apply even when risk is high, and in that case it also suppresses Fix my credit. Provenance: **Implemented**. Whether both buttons should be able to show together is **Requires Confirmation**. The conditions allow overlap only when `canApply` is neither strictly `false` nor strictly `true` (it is `null` before the calculation returns).

### W10 — Recommended alternative

Trigger: `/select_model_recommended`.

Behaviour:

1. Show the recommended instalment range from the risk result. The low end is `0`. The high end is the smaller of the calculated maximum and the visitor's original maximum.
2. GET geolocation again, POST `step1_range_car`, then POST `step4_recommend_confirm` with `car_type_recommended_list`.
3. List models whose 72-month prime instalment falls inside that range. The same D-then-A overwrite rule as W4 applies.
4. If the list is empty, the page stays on the empty list. A redirect to `/fix_my_credit` is commented out.
5. Continue opens `/cost_of_ownership_recommended`.

That screen POSTs `step4_summary` and builds tier cards the same way as W6, except the instalment ceiling is `recommendedInstalmentEndRange`. Confirm opens `/recommended_option`.

`/recommended_option` POSTs `step4_maintenance_and_services` and `step4_summary` again, shows the risk accordion, and uses the same Apply / Fix my credit rules. Explore other options is hidden only when risk is low (the 1700 check is not repeated). Go back returns to the recommended cost screen. Provenance: **Implemented**.

### W11 — Apply and fix-my-credit

Trigger: `/congratulations` or `/fix_my_credit`.

Both post on load and then show a completion message and "Start again", which returns to `/select_a_car` without clearing context.

| Screen | Endpoint | Extra fields | On-screen outcome |
|---|---|---|---|
| Congratulations | `step4_congratulations` | `apply: 1` | "Congratulations!" and mailto `absa-simulator@findndrive.co.za` |
| Fix my credit | `step4_fix_my_credit` | `apply: 1`, `fix_my_credit: ""` | "Credit assistance tool coming soon!" plus the same mailbox, and a link whose href is `https://www.absa.zo.za` |

Provenance: **Implemented**. The href `absa.zo.za` is what the source contains. The visible words say absa.co.za. Whether the href is a typo is **Requires Confirmation**.

The explainer-video button is commented out on both screens.

### W12 — Errors and unknown URLs

After login:

- `/error` explains that data could not be retrieved. It has no retry button.
- Any other unknown path renders `NotFound`.
- `/about` renders a stub titled "Success Page" / "Next Page".
- `/services` and `/contact` are not routes. The header links that named them are commented out.

Provenance: **Implemented**.

## Functional requirements

### FR-001 — Login before the journey

Requirement: Every route shows the login form until `step0_login` returns a truthy body.

Who: Visitor. Trigger: first paint, or any navigation while logged out.

Inputs: username, password. Validation: none on the client.

Expected behaviour: truthy response reveals the journey at `/home`. Falsy response leaves the form in place with no message.

Outputs: `loged_in` in memory.

Error: a thrown request calls `navigate("/error")` but the logged-out tree still renders `Login`.

Evidence: `App.jsx`, `Login.jsx`. Implementation: Implemented. Confidence: High.

### FR-002 — Session identity

Requirement: The client creates `app_id` once per mount and sends it on every step. `session` starts as `"0"` and is replaced by `data.session` from `step1_range_car`.

Evidence: `AppContext.jsx`, `SelectACar.jsx`. Implementation: Implemented. Confidence: High.

A new mount (refresh) creates a new `app_id` and resets `session` to `"0"`.

### FR-003 — Persist each step on CarApi

Requirement: Each journey screen posts its step method before, or as, the visitor continues. The production URL is `https://admin.findndrive.co.za/CarApi/<endpoint>`. The dev server proxies `/CarApi` to that host.

Evidence: `ApiCall.js`, `vite.config.js`, each page's `pageLoadAction`. Implementation: Implemented. Confidence: High.

No `Authorization` header is set. Failure of a step navigates to `/error` except the login falsy case in FR-001.

### FR-004 — Monthly budget and body style

Requirement: The visitor must choose a body style and a monthly amount before leaving step 1. Body styles are Hatchback, Sedan, Bakkie, and SUV, filled from fixed response indexes.

Evidence: `SelectACar.jsx`, `FndSlider.jsx`. Implementation: Implemented. Confidence: High.

### FR-005 — Offer models inside the monthly amount

Requirement: A model is listed when at least one tier price produces a 72-month instalment at prime 12.75, with no deposit and no balloon, that does not exceed the visitor's maximum.

Evidence: `SelectModel.jsx`, `FinanceCalculate.js`, `FormContext.jsx` (`primeInterestRate` default `12.75`). Implementation: Implemented. Confidence: High.

The calculator later uses a different prime. See FR-007.

### FR-006 — Monthly distance

Requirement: The visitor must pick one of the six fixed km bands. Fuel and "months left" use the upper number of the band.

Evidence: `AverageUsage.jsx`, `FormatNumber.js` `getMaxDistance`. Implementation: Implemented. Confidence: High.

### FR-007 — Finance estimate

Requirement: The calculator shows an estimated monthly repayment and a breakdown of finance required, total interest and service fees, balloon, monthly service fee, and initiation fee, using the inputs in W7.

Evidence: `Calculator.jsx`, `FinanceCalculate.js`. Implementation: Implemented. Confidence: High.

Constants in code: initiation fee `1207.5`, monthly service fee `69`, calculator prime `11.75` plus a margin from the credit answer. Model filtering uses `12.75` with no margin. Two primes in one journey is **Requires Confirmation**.

### FR-008 — Affordability inputs

Requirement: Continue from financial details requires net salary, debt expenses, living expenses, and savings all to be truthy. Total expenses posted onward are the sum of debt and living expenses.

Evidence: `ChancesOfApproval.jsx`. Implementation: Implemented. Confidence: High.

### FR-009 — Risk breakdown and next action

Requirement: After maintenance and services are loaded, the visitor sees the five-part breakdown and the Apply, Fix my credit, and Explore buttons described in W9.

Evidence: `ResultOptions.jsx`, `RecommendedOption.jsx`, `RiskLevelCalculation.js`, `Accordion/FndAccordion.jsx`. Implementation: Implemented. Confidence: High.

The band thresholds are in `business-rules.md`. They are inferred from the function. No separate requirements document states them.

### FR-010 — Apply and credit-assistance completion

Requirement: Apply posts `step4_congratulations` with `apply: 1` and shows the congratulations screen. Fix my credit posts `step4_fix_my_credit` with `apply: 1` and `fix_my_credit` set to an empty string, and shows the coming-soon screen.

Evidence: `Congratulations.jsx`, `FixMyCredit.jsx`. Implementation: Implemented. Confidence: High.

`silicon_ark/findndrive_webportal` records that the server method reads the key `fix_my_credit ` with a trailing space. This client sends `fix_my_credit` without that space. Whether the flag is stored is **Requires Confirmation**. This PRD does not change the webportal documents.

### FR-011 — Example cars, not stock

Requirement: The home disclaimer states that the car options are limited, are not available in stock, and are used for calculations. Tier cards use fixed sample year and mileage per tier.

Evidence: `FndDisclaimer.jsx`, `CostOfOwnership1.jsx`. Implementation: Implemented. The disclaimer is **Documented** copy. The sample figures are **Implemented**.

### FR-012 — Deploy the production build

Requirement: A push to `master` installs dependencies, runs `npm run build`, uploads `dist/` to the production S3 bucket, and invalidates the production CloudFront distribution.

Evidence: `bitbucket-pipelines.yml`. Implementation: Implemented as pipeline definition. Whether the last run succeeded, and what the bucket name is, is **Unknown**.

## Integrations

| Name | Purpose | Trigger | Direction | Auth | Failure |
|---|---|---|---|---|---|
| `CarApi` on `admin.findndrive.co.za` | Save the step and return cars, maintenance, or services | Page load of each step, and login submit | Client posts JSON, reads JSON | None in this client | Navigate to `/error`, except falsy login |
| `geolocation-db.com` | Country, region, city, postal code, IPv4 | Load of select-a-car and recommended model | Client GET | None | Same error navigation. The step is not posted |
| AWS S3 / CloudFront | Host the built site | Pipeline on `master`, and manual `staging` | Pipeline upload | AWS keys are pipeline variables | Not described in the repo |
| mailto `absa-simulator@findndrive.co.za` | Feedback | Visitor clicks the address on a completion screen | Browser mail client | n/a | Browser dependent |

Endpoint list the client calls: `step0_login`, `step1_range_car`, `step2_model`, `step2_model_km`, `step2_result`, `step2_calculator`, `step3_affort`, `step4_maintenance_and_services`, `step4_summary`, `step4_recommend_confirm`, `step4_congratulations`, `step4_fix_my_credit`.

## Constraints

- Currency display is rand (`R` prefix, thousands separators).
- Journey state dies on refresh.
- Login is a boolean gate, not a stored token.
- The progress control marks only the current step as active. It does not mark earlier steps complete.
- Routes are declared without a leading slash except `/`. React Router 6 still matches them from the root. The `exact` prop is present and is ignored by this router version.
- No automated test covers a workflow.
- Node 16 is the pipeline image. `package.json` does not set an `engines` field.

## Open questions

- Which login identities are still valid? This repo does not contain them.
- Are 11.75 and 12.75 both intentional?
- Are initiation `1207.5`, service fee `69`, fuel `22.46` per litre, and the insurance percentages still approved?
- Should an empty recommendation list send the visitor to fix-my-credit? The redirect is commented out.
- Is `https://www.absa.zo.za` the intended link?
- Is the manual staging pipeline still used? It builds a `build/` folder and runs a test script this package does not define.
