# PRD — paymyfines-frontend-new

Evidence date: 2026-10-07.

Source: `git@bitbucket.org:overdrive/paymyfines-frontend-new.git`, `main` at `a335e20` (2025-02-27). Full hash `a335e20139975fffafe50478d1b884fe49b30668`.

There is no prior PRD in the source repository or in Silicon Ark for this directory. `README.md` is the only product statement in the source tree. Provenance is labelled on each requirement.

Related Silicon Ark directories (`pmf-frontend-ah`, `pmf-backend-ah`, `paymyfines-worker`, `paymyfines-pdf-application`) are not referenced by this repository. A shared product is **Unknown / Requires Confirmation**.

## Product purpose

**Documented.** Individuals use the application to search, pay, and manage fine information (`README.md`).

**Documented.** The operator in the terms page is TMT Services & Supplies (Pty) Ltd (Reg No: 2000/022850/07), called PayMyFines. The terms say the site lets a person check outstanding traffic fines and pay them online when no warrant of arrest has been issued (`src/components/TermsAndConditions.js`, clause 5.1.3).

**Documented.** The about page says the platform is for viewing fine history and paying outstanding fines, and points payment security information at `https://iveri.com/payment-gateway/` (`src/components/About.js`).

**Implemented.** The home page (`/`) does not offer that search. It shows: "Paymyfines is temporarily undergoing maintenance." with telephone `+27 21 126 0600` and email `payments@tmt.africa` (`src/components/LandingPage.js`).

## Users and roles

Roles are string constants in `src/config/Roles.js`: `SUPER_ADMIN`, `ADMIN`, `MANAGER`, `CUSTOMER`, `GUEST`. The file comment says they are placeholders that can be replaced.

| Actor | How the UI treats them | Provenance |
|---|---|---|
| Guest | Default when `localStorage.roles` is null. Can open home, about, login, register, forgot password, contact, terms, privacy, quick ID fine search, quick fine search, payment notification, and fine-detail routes whose `permission` array is empty | **Implemented** |
| Customer | Signed-in routes: outstanding fines, profile, transactions, cart. Same list as `MANAGER` | **Implemented** route table |
| Manager | Same customer routes. Not included on `/adminDash` or `/adminSettings` | **Implemented** route table. No screen treats `MANAGER` differently from `CUSTOMER` |
| Admin and super admin | Customer routes plus `/adminDash`, `/adminSettings`, and `/adminAnalytics` | **Implemented**. The UI does not distinguish `ADMIN` from `SUPER_ADMIN` |
| API | `POST` login stores `payload.data.strRole` from `GET /user/details` into `localStorage.roles` | **Implemented**. Which role values the API returns is **Unknown / Requires Confirmation** |

`getAllowedRoutes` (`src/utils/index.js`) `JSON.parse`s `localStorage.roles` and keeps a route when the route's `permission` array intersects the parsed value. A missing `permission` or an empty array allows the route for every visitor. Fine detail paths `/aartofinedetails` and `/cpafinedetails` use `permission: []`, so they stay available to guests.

If `strRole` is a plain word such as `CUSTOMER` rather than a JSON array, `JSON.parse` throws. The expected payload shape is **Unknown / Requires Confirmation**.

## Functional requirements

### FR-001 — Home

**Requirement.** Opening `/` shows a maintenance message and the telephone and email above. It does not render a search box.

**Trigger.** Navigation to `/`.

**Implementation.** `LandingPage` return markup. `searchFines` remains in the file and is not referenced by that markup.

**Evidence.** `src/components/LandingPage.js`, route `/` in `src/config/RoutingConfig.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-002 — Notice and ID search (code present, home UI does not call it)

**Requirement.** A non-empty text value is classified as follows:

- If it contains `/` or its length is greater than 13, hyphens are removed when present and the UI calls `GET /getfinesearch?Id={userId or GUEST}&noticeNumber={value}`.
- Otherwise the value must pass South African ID validation (field type `02`). The UI then calls `GET /getpaginatedfines` with `pageSize=100`, `pageStart=1`, `pageEnd=1`, `strIdNumber`, and `strIdType=02`, and navigates to `/quickidfinesearch?idno={value}`.

On a notice search, a payload key `infringementNoticeNumber` opens `/aartofinedetails`. A key `noticeNumber` opens `/cpafinedetails`. Strings "Third party did not respond" and "No fines found" are shown in a modal. Both sources reporting no fines shows "Fine not found".

**Actor.** Any visitor, if the function runs. Logged-out calls use `Id=GUEST`.

**Status.** Implemented in `searchFines`. Not reachable from the rendered home page.

**Evidence.** `src/components/LandingPage.js`.

**Confidence.** High for the function. The product decision to hide it is **Unknown / Requires Confirmation**.

### FR-003 — Quick fine search route

**Requirement.** `/quickfinesearch` looks up a notice via `GET /getfinesearch` and opens AARTO or CPA detail. It is not in the navigation (`canNavigate: false`). Guest is allowed.

Response strings differ from the home function: "No response from iForce" and "No response from eNatis".

**Evidence.** `src/components/QuickFineSearch.js`, `src/config/RoutingConfig.js`.

**Status.** Implemented, hidden from navigation.

**Confidence.** High.

### FR-004 — Outstanding fines

**Requirement.** A signed-in user (not guest) opens `/outstandingfines`. A guest may open `/quickidfinesearch`, which renders the same component.

**Inputs.**

- Default: `userId` from stored user details.
- ID filter not `ALL`: `strIdNumber` and `strIdType` split from `id:type` stored in `localStorage.IDNumbers`.
- Query `idno`: treated as ID type `02`.
- Issuing authority filter, default `ALL`.
- Paging: `pageSize` (default 100), `pageStart`, `pageEnd`.

**Call.** `GET /getpaginatedfines`.

**Behaviour.**

- eNatis rows are tagged `api: "eNatis"`. iForce rows are tagged `api: "iForce"`.
- If either source is missing or equals "Third party did not respond", a modal says not all infringements may be available.
- If `result` is false, a modal asks the user to retry and to use the customer service email if it persists. The component does not name that email address.
- If iForce `totalCases` is greater than the page size, a modal says only the first page can be paid now, and that Paymyfines depends on external systems which can take up to 4 hours to update.
- Rows can be filtered in the table, opened for detail, or added to the cart when `customerPaymentAllowed` is true.
- "Add all" adds every visible payable fine and navigates to `/Cart`.

**AARTO amount rule (implemented).** If `paymentNotAllowedCode` is not `3` and `feesList.debt` has entries, status is `debt[0].debtType` and the amount is the sum of `debtAmt`. Otherwise `customerPaymentAllowed` is set to false and status and amount are `N/A`. Demerit points are the sum of `charge[].demeritPoints`.

**iForce display (implemented).** Status is `phase`. Amount is `totalAmountDue`. Demerit points display as `N/A`. Empty ID numbers display as `N/A`.

**Evidence.** `src/components/OutstandingFines.js`.

**Status.** Implemented.

**Confidence.** High for the UI rules. The meaning of `paymentNotAllowedCode` `3`, and whether it is the warrant named in the terms, is **Unknown / Requires Confirmation**.

### FR-005 — AARTO fine detail

**Requirement.** `/aartofinedetails?id={notice}` loads `GET /queryinfringements?Id={userId or GUEST}&noticeNumber={id}`.

HTTP 504 or 400 sets an error state and the screen shows "eNatis is down."

When `feesList.debt` is empty, the UI sets `customerPaymentAllowed` to false and shows status and amount `N/A`.

The user can add the fine to the cart unless it is already there or `customerPaymentAllowed` is false. A second action adds it and navigates to `/Cart`.

**Evidence.** `src/components/AARTOFineDetails.js`.

**Status.** Implemented. Route permission is an empty array, so guests are not filtered out.

**Confidence.** High.

### FR-006 — CPA fine detail

**Requirement.** `/cpafinedetails?id={notice}` loads:

1. `GET /getoutstandingfines?Id={userId or GUEST}&noticeNumber={id}`
2. `GET /getcharges?Id=...&noticeNumber=...`
3. When the first fine has an evidence token, `GET /evidence?...&includeImage=true`

Failure sets an error state and the screen shows "iForce is down."

Evidence images can be opened in a modal or a new browser tab (base64 data URL).

Cart behaviour matches AARTO: blocked when `customerPaymentAllowed` is false or the notice is already in the cart.

**Evidence.** `src/components/CPAFineDetails.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-007 — Registration

**Requirement.** A guest submits `/register`.

**Inputs.** Email, name, surname, cellphone, ID number and confirmation, password and confirmation, reCAPTCHA token. A business checkbox switches ID type from `02` (South African ID) to `04` (business registration).

**Validation.** See business rules for field types. ID confirmation must match. Passwords must match and must not be blank. No minimum password length is checked in this form. Cognito may still reject the password. That policy is **Unknown / Requires Confirmation**.

**Behaviour.**

1. `POST /recaptcha` with `{ recaptcha }`. Continue only when `result === true` and `payload === 'true'`.
2. `Auth.signUp` with username and email attribute equal to the email.
3. `POST /register` with Cognito uuid, name, surname, email, ID type, ID number, mobile number.
4. Email confirmation uses `Auth.confirmSignUp`. Resend uses `Auth.resendSignUp`.
5. After confirmation the UI signs in and links records: `GET /user/details` by Cognito uuid and by email, then `POST /updateregistration` or `POST /register` depending on the branch in `Register.js`.

**Evidence.** `src/components/Register.js`.

**Status.** Implemented.

**Confidence.** High for the steps above. The exact success payload of `/register` beyond `result === true` and the string `"User Email Already exists"` (handled on the login completion modal, not only on register) needs the API. **Unknown / Requires Confirmation** for server-side duplicates.

### FR-008 — Login

**Requirement.** Guest submits email and password on `/login`.

**Behaviour.**

1. Email must match the email pattern. Password must be non-blank.
2. `Auth.signIn`.
3. If `email_verified` is true, `GET /user/details?strCognitoUuid={username}&email={email}`.
4. `result === true`: store `strRole`, user details, and ID numbers (`primary` plus `additionalIds`), then go to `/outstandingfines`.
5. Payload `"Profile update required"`: modal collects name, surname, mobile, and ID, then `POST /register`. If the payload is `"User Email Already exists"`, the UI also calls `POST /updateregistration` and retries sign-in.
6. Payload `"Cognito ID update required"`: `POST /updateregistration`, then reload user details.
7. If Cognito throws and `GET /user/details?strEmail=` returns `dtLastEdit === '1/1/0001 12:00:00 AM'`, the modal says this is the first time on the new site and the user should reset the password.
8. `NotAuthorizedException`: "Incorrect email or password". `UserNotConfirmedException`: offer to continue to `/register?verify={email}`.

**Evidence.** `src/components/Login.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-009 — Forgot password

**Requirement.** Guest opens `/forgot-password`.

**Behaviour.** The screen looks up `GET /user/details?strEmail=`. It can call `Auth.forgotPassword`, `Auth.forgotPasswordSubmit`, and, on one path, `Auth.signUp` while storing the new password in `localStorage` under the key `asdf`. `Register.js` reads `asdf` when present. A path calls `Auth.confirmSignUp(username, '000000', ...)`.

**Evidence.** `src/components/ForgotPassword.js`, `src/components/Register.js`.

**Status.** Implemented.

**Confidence.** High for the calls. Why code `000000` is sent, and whether that path is still used, is **Unknown / Requires Confirmation**.

### FR-010 — Profile

**Requirement.** A signed-in user opens `/profile`, loads `GET /user/details?userId=`, and can update name, surname, email, mobile, primary ID, and additional IDs via `POST /update`.

On save the UI sets `txtBillAddress` to the literal `"Home"`.

If a new password is entered it must differ from the old password, and the old password is required. The change is `Auth.changePassword`.

Deleting an additional ID removes matching cart lines (including lines whose ID is null or `N/A`) when the user saves. The modal says the delete is kept only after update.

**Evidence.** `src/components/Profile.js`.

**Status.** Implemented.

**Confidence.** High. Why the billing address is always `"Home"` is **Unknown / Requires Confirmation**.

### FR-011 — Transactions

**Requirement.** A signed-in user opens `/transactions`. `GET /transactions?userId=` with optional `strIdNumber` and `strIdType` when an ID filter is not `ALL`.

Amounts display with an `R` prefix. `dtPaid` displays as `YYYY/MM/DD`. The table can search, sort, paginate, and export. A row opens a detail modal.

**Evidence.** `src/components/Transactions.js`.

**Status.** Implemented. The route is also a child of Profile in the nav.

**Confidence.** High.

### FR-012 — Cart and checkout

**Requirement.** Signed-in users open `/Cart`. The cart is `localStorage.cartItems`, keyed by infringement or notice number.

**Totals.**

- AARTO line (object has `infringementNoticeNumber`): sum of `feesList.debt[].debtAmt`.
- Other line: `totalAmountDue`.
- Convenience fee: settings value `convenience` multiplied by the number of lines.
- Cart total: fine total plus convenience total.

**Checkout.**

1. `GET /getsettings?userId=` supplies the fee (`strKey` trimmed equals `convenience`).
2. Build an iVeri Lite 4.0 payload in ZAR. Line amounts sent as `Lite_Order_LineItems_Amount` are the rand amount times 100. A final line is "Convenience Fee".
3. `POST /postiveritransaction` with `striVeriNumber` (generated id), `userId`, `strStatus` `"Sent to iVeri"`, `strAmount` (cart total), `intPaid` `"NO"`, and an `infringements` array.
4. Only when `result === true`, submit a POST form to `https://portal.nedsecure.co.za/Lite/Authorise.aspx`.
5. Otherwise show "There was an issue during checkout...".

Return URLs point at `{apiGateway}/iveripayment?transactionID={uuid}&veriLocation={encoded origin/paymentNotification}&status=success|fail|error|trylater`.

iForce lines are stored with source `IForce`. Other lines are stored with source `Aarto`.

**Evidence.** `src/components/Cart.js`, `src/aws-config.js`.

**Status.** Implemented.

**Confidence.** High for the browser steps. Whether `/iveripayment` records the payment is outside this repository. **Unknown / Requires Confirmation.**

### FR-013 — Payment notification

**Requirement.** `/paymentNotification` reads `status` from the query string.

| `status` | Screen | Then |
|---|---|---|
| `success` | "Payment Successful"; cart cleared | After 9 seconds, `/` |
| `error` | "Payment Unsuccessful" plus `liteResultDescription` | After 9 seconds, `/Cart` |
| `fail` | "Payment Failed" plus description | After 9 seconds, `/Cart` |
| `trylater` | "Try Again Later" plus description | After 9 seconds, `/Cart` |

The component does not call the Unity API. Guests are allowed on the route.

**Evidence.** `src/components/PaymentNotification.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-014 — Admin dashboard

**Requirement.** `SUPER_ADMIN` and `ADMIN` open `/adminDash`.

Default date range is the previous calendar month through today (the code walks back until the month changes).

**Calls.**

- `GET /getgraphs?userId=&startDate=&endDate=` (`YYYY-MM-DD`).
- `GET /reports?issuingAuthority=&startSettlementDate=&endSettlementDate=`.

Graph types routed to tables, counter tabs, or charts: `Table`, `CounterTab`, `LineMarkGraph`, `VerticalBarGraph`, `RadialBarGraph`, `StackGraph`, `PieGraph`, `AreaGraph`, `ScatterGraph`, `RadarGraph`.

`result !== true` on graphs shows "There was an error fetching the graphs!". Finance payload `"No settings found"` leaves the report empty.

**Evidence.** `src/components/AdminDashboard.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-015 — Admin settings

**Requirement.** `SUPER_ADMIN` and `ADMIN` open `/adminSettings`.

**Calls.** `GET /getsettings`, `GET /getgraphsettings`, `POST /postsettings`, `POST /postgraphsettings`.

Email template saves are rejected in the UI unless the value contains the substring `{code}`. Values are `encodeURI`'d before post and `decodeURI`'d on load.

Convenience fee posts `{ userId, convenience: parseInt(convenienceFee) }`.

Graph types offered in the form: `Table`, `CounterTab`, `AreaGraph`, `LineMarkGraph`, `PieGraph`, `RadarGraph`, `StackGraph`, `VerticalBarGraph`. The dashboard also accepts `RadialBarGraph` and `ScatterGraph`, which are not in this dropdown.

**Evidence.** `src/components/AdminSettings.js`.

**Status.** Implemented.

**Confidence.** High. The list of setting keys is whatever `/getsettings` returns, except the UI special-cases the key `convenience`. **Unknown / Requires Confirmation** for the full key list.

### FR-016 — Contact us

**Requirement.** Any role, including guest, can open `/contactus`.

**Inputs.** Name, surname, email, ID number, ticket number, category, message, reCAPTCHA.

**Categories.** Reduction Request, Redirection Request, Payment Query, Login Query, Fine Dispute, Copy of Fine.

**Behaviour.** `POST /recaptcha` must succeed as in registration. Then `POST /contactUs` with `strCaterogy` (spelling in the payload). Success sets a sent state. The form says submit agrees to the terms and privacy policy.

**Evidence.** `src/components/ContactUs.js`.

**Status.** Implemented.

**Confidence.** High. Where the message is delivered is the API. **Unknown / Requires Confirmation.**

### FR-017 — Static information pages

**Requirement.** `/about`, `/TermsAndConditions`, and `/PrivacyPolicy` render fixed copy. Terms name `https://all.paymyfines.co.za` and stop at clause 5.1.6 while referring to contact details in clauses 9 and 10, which are not in the component. Footer on pages other than `/` shows postal address PO Box 234, Century City, Cape Town, 7446, affiliate images for eNatis, RTIA, and AARTO, and links to routes with `showInFooter: true`.

**Evidence.** `About.js`, `TermsAndConditions.js`, `PrivacyPolicy.js`, `src/components/common/Footer.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-018 — Session end

**Requirement.** Logout calls `Auth.signOut` and deletes local storage keys `roles`, `userDetails`, `userProfileDetails`, `cartItems`, `IDNumbers`, and `backFilters`, clears `issuingAuthList`, and navigates to `/`.

**Evidence.** `src/components/common/TopNav.js`.

**Status.** Implemented.

**Confidence.** High.

### FR-019 — Unknown route

**Requirement.** A path that is not in the allowed route list renders `NotFound`. The link "Back to home" goes to `/home` when `localStorage.roles` is set, and to `/` otherwise. There is no `/home` route in `RoutingConfig.js`.

**Evidence.** `src/routes/MapAllowedRoutes.js`, `src/components/common/NotFound.js`.

**Status.** Implemented.

**Confidence.** High.

## Major workflows

### Look up a fine while logged out

Home does not show the form (FR-001). If a caller hits the unused search function or `/quickfinesearch` / `/quickidfinesearch`, the API is called with `GUEST` or with an ID number, and detail routes are not role-filtered.

### Register, confirm, sign in

FR-007 then FR-008. First-time users of "the new site" with `dtLastEdit` of `1/1/0001` are told to use forgot password.

### Pay selected fines

Outstanding fines or a detail page adds payable lines to `cartItems`. Cart loads the convenience fee, posts the transaction, and redirects to iVeri. The gateway return is expected to land on `/paymentNotification`, which clears the cart only for `status=success`.

### Admin reviews a period

Dashboard date range reloads graphs and the finance report. Settings edits templates, the fee, and graph definitions.

## Integrations

| System | Direction | Trigger | Data | Failure the UI shows |
|---|---|---|---|---|
| Amazon Cognito | Browser → Cognito | Register, login, confirm, forgot password, change password, logout | Email, password, confirmation code | Cognito error message, or the first-time-site modal |
| Unity API (API Gateway) | Browser → `unity` | Screens above | User, fines, settings, transactions, contact, reCAPTCHA, iVeri transaction | Status on the helper result, or a generic modal |
| eNatis / iForce | Not called by the browser | Unity responses include those payloads | Fine, charge, evidence | "did not respond", "eNatis is down.", "iForce is down." |
| iVeri Nedsecure Lite | Browser form POST | Checkout after a successful `/postiveritransaction` | Card checkout fields, ZAR amounts in cents | Checkout modal if the post fails. Return status if iVeri sends the user back |
| reCAPTCHA | Widget, then `POST /recaptcha` | Register and contact | Token | "The recaptcha failed, please try again" |
| Matomo | Browser → `analytics.siliconsoftware.net` | Every page load | Page view, site id 2, domain `*.master.d39rr3ejryd4rk.amplifyapp.com` | Not handled |
| Google Analytics | Browser | Every page load | Measurement id `G-6M1WRZYG8D` | Not handled |
| S3 via Amplify Storage | Configured in `src/index.js` | No screen in `src` calls `Storage` | Invoice bucket name only | Not handled in UI |

`documentation/iveri.php` redirects `veriLocation` plus the query string to `{veriLocation}/PaymentNotification`. `public/iveri.html` redirects to `http://pay-my-fines.s3-website-eu-west-1.amazonaws.com/PaymentNotification` plus the query string. The cart builds return URLs against the API Gateway, not against these two files. Which of the three paths production uses is **Unknown / Requires Confirmation**.

## Constraints

- Currency in the iVeri payload is `ZAR`. Terminal value sent is `77777001`. Lite version sent is `4.0`.
- Production AWS settings apply only when `window.location.origin` is exactly `https://www.paymyfines.co.za`.
- Cart and roles live in `localStorage`. Logout clears them. Another browser does not see the cart.
- Terms limit the described payment method to credit card and say the operator may limit transactions per card per day. The UI does not implement a per-card daily cap.
- Service worker is unregistered (`src/index.js`).
- `README.md` says to start with `yarn install` and `yarn start`. `package.json` name is `role-based-access-control`, version `0.1.0`, private.

## Requirements that are assumptions

- That maintenance on `/` is temporary. The copy says "temporarily". No date or flag turns the search form back on.
- That `paymentNotAllowedCode === 3` means a warrant of arrest. The terms mention a warrant. The code checks the number `3` and an empty debt list. The link is not written in the code.
- That `MANAGER` is a real account type. It exists in the route table only.
