# Technical documentation — pmf-frontend-ah (PayMyFines)

Evidence date: 2026-10-06. Source: `git@bitbucket.org:overdrive/pmf-frontend-ah.git`, `master` at `3e8d2a5` (2022-05-06, `After handover commit`). Parent commit: `ae541e8` (`Initial commit`), same day. Remote: `origin/master` only.

The scan-host directory from the onboarding ticket was not on this machine.

## What this repository is

A Create React App single-page application. `package.json` name is `role-based-access-control`, version `0.1.0`, private. The product name used in the UI is PayMyFines / Paymyfines.

It is a browser client. User records, fine queries, settings, and payment records are HTTP calls to an API Gateway stage named in config as `unity`. Card capture is a form POST to iVeri Lite.

## Stack

| Item | Evidence |
|---|---|
| React 17.0.2, React DOM 17.0.2 | `package.json` |
| `react-scripts` 4.0.3 | `package.json`. Start script adds `--max-http-header-size=77777` |
| React Router DOM 5.2 | `package.json`, `src/routes/` |
| React Bootstrap 1.6 and Bootstrap 5.0.1 | `package.json`. `public/index.html` also loads Bootstrap 4.3.1 from the MaxCDN stylesheet, plus W3.CSS and the Raleway font |
| aws-amplify 3.3.21 | Auth, API, Storage configuration in `src/index.js` |
| Google reCAPTCHA | `react-google-recaptcha` 2.1, site key in `src/config/google.js` |
| Charts | `recharts`, `react-chartist`, `chartist`. Admin graphs use `common/Graphs.js` |
| Tables | `react-data-table-component` via `common/ExportableTable.js` |
| Dates | `moment` on the transactions screen. Admin dates use `react-datepicker` |
| HTTP | Amplify `API.get` / `API.post`. `RequestsAPI.js` is a `fetch` wrapper with `mode: 'cors'` and is not referenced by the screens that were traced |
| Language level | No TypeScript. `jsconfig.json` sets `baseUrl` to `./src`, so imports such as `components` and `utils` resolve there |
| Formatting | Prettier 1.18, `.prettierrc.json`. npm script `pretty` writes `src/**/*.js` with tabs, single quotes, and always-parens arrows |
| Tests | `src/App.test.js` only (`react-scripts test`) |
| Storybook 5.2 | Dev dependency. One story file |

`yarn.lock` and `package-lock.json` are both committed. Workflows run Yarn.

## Application structure

```text
public/                 index.html, manifest, favicons, iveri.html
documentation/          iveri.php redirect
src/index.js            Amplify.configure, render <App>, unregister service worker
src/App.js              renders <Routes>
src/aws-config.js       region, S3 bucket, API URL, Cognito ids
src/config/             Roles, RoutingConfig, reCAPTCHA site key
src/routes/             Routing shell, MapAllowedRoutes
src/utils/index.js      auth flag, route filter, validators, UUID, header labels
src/components/         screens listed in features.md
src/components/common/  nav, footer, table, graphs, modal, loader
src/contexts/           popup modal
src/hooks/              form fields, popup, window size
build/                  committed webpack output
.github/workflows/      master, developer, qa
```

`MapAllowedRoutes` registers only routes that survive `getAllowedRoutes`. Unknown paths render `common/NotFound.js`. The home route is the only `exact` route. Path `/` uses `exact`; other paths do not, so a longer path can still match a shorter prefix if both are registered. **Inferred** from `exact={path === "/"}`.

Each route is wrapped in `PopupModalProvider`.

Navigation links are the route entries with `canNavigate: true`. Footer links are those with `showInFooter: true`. Child routes (profile, admin) become dropdown items when the child also has `canNavigate: true`.

## Runtime configuration

All of the following are hardcoded. There is no `.env` in the repository and the React build does not switch on `REACT_APP_*` in the files that were read. Developer and QA workflows run the same `yarn build` without extra env vars, so they embed this production API config.

From `src/aws-config.js`:

| Key | Value |
|---|---|
| AWS region | `eu-west-1` |
| S3 bucket | `tmt-pmf-prod-invoices` |
| API Gateway URL | `https://puru95wxc8.execute-api.eu-west-1.amazonaws.com/production` |
| Cognito user pool | `eu-west-1_gzZa8Iitq` |
| App client id | `24kutsb2l5dlrm3k6c9tjrlp7d` |
| Identity pool id field | `eu-west-1:475415325198:userpool/eu-west-1_gzZa8Iitq` |

The identity pool value uses a user-pool ARN shape (`account:userpool/poolId`), not an identity-pool UUID. Amplify's `identityPoolId` is given that string in `src/index.js`. Whether Cognito accepts it is **Unknown / Requires Confirmation**.

`Amplify.configure` sets `Auth.mandatorySignIn: true` and one API endpoint named `unity`.

reCAPTCHA: active key is the v2 key assigned in `src/config/google.js`. Two other keys are comments (one marked v3).

iVeri, from `Cart.js` (not from `aws-config.js`):

| Field | Value |
|---|---|
| Authorise URL | `https://portal.nedsecure.co.za/Lite/Authorise.aspx` |
| Merchant application id | `{4C03BE1B-05AF-48BF-972B-00DAE5F11AD9}` |
| Terminal | `77777001` |
| Lite version | `4.0` |
| Currency | `ZAR` |

`MAX_ATTACHMENT_SIZE` (5000000) is exported on the config object. No reader under `src/` was found.

## Auth

Cognito is the login store. The username is the email address.

| Action | API |
|---|---|
| Sign up | `Auth.signUp` |
| Confirm | `Auth.confirmSignUp` |
| Resend code | `Auth.resendSignUp` |
| Sign in | `Auth.signIn` |
| Reset request | `Auth.forgotPassword` |
| Reset confirm | `Auth.forgotPasswordSubmit` |
| Change password | `Auth.changePassword` |
| Sign out | `Auth.signOut` |

After sign-in the UI copies Unity `strRole` into `localStorage.roles` and the profile into `userDetails`. Route filtering uses only that localStorage value. Logout removes `roles`, `userDetails`, `userProfileDetails`, `cartItems`, `IDNumbers`, and `backFilters`.

`unityRequests` does not set an `Authorization` header itself. Amplify 3 attaches credentials for a configured API when a session exists. Guest calls (`Id=GUEST`) run with no session. **Unknown** how API Gateway authorises those.

The forgot-password screen stores the password in `localStorage` key `asdf` across the redirect to `/register`. Register reads and deletes it. That is client behaviour, not a Cognito feature.

## Unity API paths called by this UI

Base URL: the API Gateway URL above. Wrapper: `src/components/unityRequests.js` (`get` or `post`; default post). Errors become `{status: error.response.status}` when `error.response` exists. A network error that lacks `error.response` throws when the catch reads `.status`.

| Method | Path | Caller |
|---|---|---|
| GET | `/getfinesearch?Id=&noticeNumber=` | `LandingPage.js`, `QuickFineSearch.js` |
| GET | `/queryinfringements?Id=&noticeNumber=` | `AARTOFineDetails.js` |
| GET | `/getoutstandingfines?Id=&noticeNumber=` | `CPAFineDetails.js` |
| GET | `/getcharges?Id=&noticeNumber=` | `CPAFineDetails.js` |
| GET | `/evidence?Id=&evidenceToken=&includeImage=true` | `CPAFineDetails.js` |
| GET | `/getallfines?...` | `OutstandingFines.js` (`userId` or `strIdNumber`+`strIdType`, optional `strIssuingAuthority`) |
| GET | `/user/details?strEmail=` | `Login.js` |
| GET | `/user/details?strCognitoUuid=` | `Login.js`, `Register.js` |
| GET | `/user/details?userId=` | `Profile.js` |
| GET | `/user/details?strEmail=` | `Register.js` (parallel with the uuid call) |
| POST | `/register` | `Register.js`, `Login.js` |
| POST | `/updateregistration` | `Register.js`, `Login.js` body `{strCognitoUuid, strEmail}` |
| POST | `/recaptcha` | `Register.js` body `{recaptcha}` |
| POST | `/update` | `Profile.js` |
| GET | `/transactions?userId=` | `Transactions.js` optional `strIdNumber`, `strIdType` |
| GET | `/getsettings?userId=` | `Cart.js`, `AdminSettings.js` |
| POST | `/postsettings` | `AdminSettings.js` |
| GET | `/getgraphsettings?userId=` | `AdminSettings.js` |
| POST | `/postgraphsettings` | `AdminSettings.js` create, update, and delete |
| GET | `/getgraphs?userId=&startDate=&endDate=` | `AdminDashboard.js` |
| GET | `/reports?issuingAuthority=` | `AdminDashboard.js` |
| POST | `/postiveritransaction` | `Cart.js` |
| GET | `{API}/iveripayment?transactionID=&veriLocation=&status=` | Not called with `API.get`. It is the iVeri return URL |

Successful bodies are used as `{result, payload}`. Screens disagree on failure strings (`Third party did not respond` versus `No response from iForce`). Both are handled only by the screen that mentions them.

## Data kept in the browser

| Key | Contents |
|---|---|
| `roles` | `strRole` from Unity, consumed with `JSON.parse` |
| `userDetails` | `{data, additionalIds}` via `@rehooks/local-storage` |
| `userProfileDetails` | JSON profile used as the `/update` body |
| `IDNumbers` | JSON array of `id:type` strings |
| `cartItems` | Map of infringement number → original fine object |
| `cartCheckout` | Declared in `Cart.js`. The checkout function that was read does not write it |
| `backFilters` | Outstanding-fines filters, and separately the admin finance filter (same key, different default shape) |
| `outstandingFines` | Normalised rows |
| `issuingAuthList`, `financeIssuingAuthList` | Authority strings collected from responses |
| `asdf` | Password, only between forgot-password sign-up and register link |

No IndexedDB and no cookie written by application code were found. Matomo and Google Analytics set their own cookies. The service worker is unregistered, so CRA's offline cache is not active.

## Payments

1. Cart reads the convenience fee.
2. `POST /postiveritransaction` records status `Sent to iVeri`.
3. A dynamically built HTML form POSTs to Nedsecure Lite.
4. iVeri is told to return to the API Gateway `/iveripayment` URL, with `veriLocation` set to the current origin plus `/paymentNotification`.
5. `PaymentNotification.js` only reads the query string. It assumes something else has redirected the browser onto the SPA with `status` and optional `liteResultDescription`.

`public/iveri.html` is a different return path: script redirects to `http://pay-my-fines.s3-website-eu-west-1.amazonaws.com/PaymentNotification` plus the query string. `documentation/iveri.php` sends `Location` to `{veriLocation}/PaymentNotification` plus the original query string. The PHP file cannot run on the S3/CloudFront static deploy as PHP.

AARTO amounts on the infringement payload use `payment.amount`. Display and iVeri lines sum `feesList.debt`. See business rule 41.

## Deployment

GitHub Actions, Node `12.x`, `actions/checkout@v1`, `actions/setup-node@v1`.

| Workflow | Branch | Build | Publish |
|---|---|---|---|
| `.github/workflows/master.yml` | `master` | `yarn` then `yarn build`, `CI` set to empty | `jakejarvis/s3-sync-action@master` using `AWS_PRODUCTION_BUCKET_NAME`, `AWS_PROD_ACCESS_KEY_ID`, `AWS_PROD_SECRET_ACCESS_KEY`, `AWS_REGION`. Then `chetan/invalidate-cloudfront-action@v2` for paths `/` and `/index.html`, distribution `AWS_DISTRIBUTION_ID` |
| `.github/workflows/developer.yml` | `dev` | same | S3 `AWS_DEVELOPER_BUCKET_NAME` with `--acl public-read --follow-symlinks --delete`. No CloudFront step |
| `.github/workflows/qa.yml` | `qa` | same | S3 `AWS_QA_BUCKET_NAME` with the same public-read delete args. Secrets `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (different names from prod and dev). No CloudFront step |

`dev` and `qa` do not exist on `origin`. A push to `master` is the only workflow that can run against the remote as it was cloned.

Bucket names and the CloudFront distribution id are not in git.

Other hosts named in the tree, with no evidence of which one is current:

| Host | Where |
|---|---|
| `https://all.paymyfines.co.za` | Terms |
| `*.master.d39rr3ejryd4rk.amplifyapp.com` | Matomo `setDomains` in `public/index.html` |
| `http://pay-my-fines.s3-website-eu-west-1.amazonaws.com` | `public/iveri.html` |

`README.md` local start: `yarn install`, `yarn start` (webpack dev server via react-scripts).

## Monitoring

- Matomo: tracker `//analytics.siliconsoftware.net/matomo.php`, site id `2`, link tracking on, title `document.domain + "/" + document.title`.
- Google Analytics gtag `G-6M1WRZYG8D`.
- No application log shipper, health check, or error reporter (no Sentry or similar) in `package.json`.
- `unityRequests` logs `error.response.status` to the console.

## Security notes visible in the tree

These are observations, not a penetration test.

- Production API URL, Cognito pool id, app client id, reCAPTCHA site key, and iVeri merchant application id and terminal are in source. App client ids and reCAPTCHA site keys are normally public. The merchant terminal is still a production identifier committed in the client.
- GitHub Actions secrets hold the deploy IAM keys. They are not in the git objects that were read.
- Route authorisation is entirely in the browser.
- Register is the only reCAPTCHA. Login, search, and checkout are not.
- Password is written to `localStorage` (`asdf`) on one forgot-password branch.
- Admin graph settings send `GraphQuery` text to `/postgraphsettings`. The UI does not constrain the SQL.
- `build/`, `.scannerwork/`, and `.vs/` (including a Visual Studio `.suo` and `slnx.sqlite`) are tracked. `.gitignore` ignores `node_modules/` and `dist/` and does not ignore `build/`, `.scannerwork/`, or `.vs/`.
- `public/index.html` loads Bootstrap, W3.CSS, Google Fonts, the Google API client (`apis.google.com/js/client:platform.js`), Matomo, and gtag from third parties.
- Terms and privacy are static and incomplete relative to their own cross-references (clauses 9 and 10 are absent).

## Development and test process

| Command | Effect |
|---|---|
| `yarn start` | Dev server. Header size flag 77777 |
| `yarn build` | Production bundle in `build/` |
| `yarn test` | CRA test runner. One render test |
| `yarn pretty` | Prettier on `src/**/*.js` |
| `yarn storybook` | Storybook on port 6006 |

No Docker file, no CI test step, and no lint step in the workflows. `eslintConfig` extends `react-app`.

The onboarding snapshot listed Sonar scanner bat files and `sonar-project.properties`. They are not in this commit. `.scannerwork/report-task.txt` is committed, which shows a scanner work directory was checked in.

## Background work

None. No queue, cron, or worker process exists in this repository. Payment completion after the browser leaves for iVeri depends on API Gateway `/iveripayment` and on iVeri. That handler is not in this repo.

## Databases

None in this repository. User, fine, transaction, settings, and graph rows are Unity API payloads. Field names (`strRole`, `strIdType`, `dtLastEdit`, `intPaid`) look like a relational schema consumed by the API. The schema is **Unknown** here.

ID type codes used by the UI: `02` South African ID, `03` foreign ID, `04` business registration.
