# Business rules — paymyfines-frontend

Evidence date: 2026-10-07.

Source: `master` at `8279c82`. Rules are copied from code or from on-screen text. Where the code implies a rule that the text does not state, the row is marked **Inferred**.

The Unity API may enforce more rules than this file. Those rules are not in this repository.

## Operator and service

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | The site operator named in the terms is TMT Services & Supplies (Pty) Ltd, registration number 2000/022850/07, called PayMyFines | **Documented** in `src/components/TermsAndConditions.js` |
| BR-002 | The terms say the website they govern is `https://all.paymyfines.co.za` | **Documented** in that file. Whether this build is that site is **Unknown / Requires Confirmation** |
| BR-003 | The service described in clause 5.1.3 is checking outstanding traffic fines and paying them on the website, provided no warrant of arrest has been issued for failure to pay | **Documented**. The UI displays `warrantOfArrestNumber` on CPA detail and does not test it before Add to Cart |
| BR-004 | Payments described in clause 5.1.4 are online by credit card, with review before the final order. The operator reserves the right to limit how many transactions one credit card may make on the site per day | **Documented**. The UI does not count transactions per card. Checkout is one form POST to iVeri |
| BR-005 | The home screen asks for a notice number and offers "FIND YOUR FINES" | **Implemented** copy in `LandingPage.js` |
| BR-006 | Footer payment email is `payments@paymyfines.co.za`. Other enquiries go to `help@paymyfines.co.za`. Postal address is PO Box 234, Century City, Cape Town, South Africa, 7446 | **Implemented** in `src/components/common/Footer.js`. The footer is not rendered when the path is `/` |
| BR-007 | About text says the payment platform is safe and links security information to `https://iveri.com/payment-gateway/` | **Documented** in `About.js` |
| BR-008 | Footer copyright line is "© 2021 Paymyfines" | **Implemented** in `Footer.js` |

The terms component ends at clause 5.1.6 and points at contact clauses 9 and 10, which are not in the file. A customer telephone number and registered street address are **Unknown** from this component.

## Identity types

Field checks are `validateFieldType` in `src/utils/index.js`.

| Code | Meaning in the UI | Check | Provenance |
|---|---|---|---|
| `02` | South African ID. Default on register and on the login completion modal | 13-digit pattern `^\d{2}[0-1][0-9][0-3]\d\d{4}[0-1]\d{2}$` and a mod-10 check digit on the first 12 digits | **Implemented** |
| `03` | Foreign ID | The function returns `true` with no pattern | **Implemented** |
| `04` | Business registration. Register checkbox sets this and clearing it sets `02` | The function returns `true` with no pattern | **Implemented** |

| ID | Rule | Provenance |
|---|---|---|
| BR-009 | Registration requires email, name, surname, cellphone, ID, matching ID confirmation, password, and matching password confirmation | **Implemented** in `Register.js` |
| BR-010 | Name and surname must match Unicode letters plus space, `.`, apostrophes, and hyphen | **Implemented** field type `Name` |
| BR-011 | Phone must match `^\+?\d{10,13}$` | **Implemented** field type `Phone` |
| BR-012 | Email must match the expression in `validateFieldType` for `Email` | **Implemented** |
| BR-013 | Notice search text uses field type `Text`: letters, digits, spaces, and a listed set of punctuation. Blank is rejected | **Implemented** in `LandingPage.js` and `QuickFineSearch.js` |
| BR-014 | Hyphens are stripped from a notice number before `GET /getfinesearch` | **Implemented** |
| BR-015 | A guest search sends `Id=GUEST`. A signed-in search sends `userDetails.data.UserID` | **Implemented** |
| BR-016 | Registration does not call Cognito until `POST /recaptcha` returns `result === true` and `payload === 'true'` | **Implemented** |
| BR-017 | Profile save sets billing address to the literal string `Home` | **Implemented** in `Profile.js`. No address form was found |
| BR-018 | A new profile password must differ from the old password, and the old password is required when a new one is typed | **Implemented**. No length rule is in the React validation |
| BR-019 | Additional ID delete is local until the user clicks update | **Implemented**. The modal says "Click update to save changes." |

## Fines and payment

| ID | Rule | Provenance |
|---|---|---|
| BR-020 | Two payload sources are shown: eNatis (treated as AARTO, key `infringementNoticeNumber`) and iForce (treated as CPA, key `noticeNumber`) | **Implemented** |
| BR-021 | On the outstanding list, an eNatis row is payable when `paymentNotAllowedCode !== 3` and `feesList.debt` is non-empty. Otherwise `customerPaymentAllowed` is set false and amount is "N/A" | **Implemented**. What code `3` means is **Unknown** |
| BR-022 | Outstanding-list amount for a payable eNatis row is the sum of `debtAmt`. AARTO detail shows only `feesList.debt[0].debtAmt`. The cart sums every `debtAmt` again | **Implemented**. The three screens do not use one formula |
| BR-023 | iForce list amount is `totalAmountDue`. Demerit points on an iForce row are the string "N/A" | **Implemented** |
| BR-024 | Add to cart is offered on detail pages only when `isLoggedIn()` is true | **Implemented** |
| BR-025 | A fine already stored under the same notice key is not added again. The button is disabled when `customerPaymentAllowed === false` | **Implemented** |
| BR-026 | "Add all" on the outstanding list copies only rows with `customerPaymentAllowed === true` | **Implemented**. An iForce row is included only when that flag is already true. The list mapper sets the flag false for some eNatis rows and does not set it true for iForce rows |
| BR-027 | Guest detail pages show `*` instead of the ID number | **Implemented** |
| BR-028 | Convenience fee comes from the settings row whose key trims to `convenience`. Cart convenience is that amount times the number of cart keys | **Implemented** |
| BR-029 | Currency sent to iVeri is `ZAR`. Lite order amounts are the rand totals times 100. A separate line is named "Convenience Fee" | **Implemented** in `Cart.js` |
| BR-030 | Checkout records the attempt with `POST /postiveritransaction` using status "Sent to iVeri" and `intPaid` "NO", then submits the card form regardless of that response | **Implemented** |
| BR-031 | iForce cart lines are labelled source `IForce`. Other lines are labelled source `Aarto` | **Implemented** |
| BR-032 | Payment return `status=success` clears the cart and sends the user home after 9 seconds. `fail`, `error`, and `trylater` leave the cart and return to `/Cart` after 9 seconds | **Implemented** in `PaymentNotification.js` |
| BR-033 | The payment page does not call the API to verify the gateway result | **Implemented** |
| BR-034 | If iForce or eNatis on the outstanding list is missing or "Third party did not respond", the user is told that not all infringements may be available | **Implemented** |
| BR-035 | Home search treats "Third party did not respond" and "No fines found". Quick search treats "No response from iForce" and "No response from eNatis" | **Implemented**. The two screens do not share one message list |

## Access

| ID | Rule | Provenance |
|---|---|---|
| BR-036 | A visitor with no `localStorage.roles` is treated as `GUEST` | **Implemented** |
| BR-037 | A route with a missing or empty `permission` array is allowed for every visitor | **Implemented** in `getAllowedRoutes` |
| BR-038 | Login, register, and forgot password are limited to `GUEST` in the route table. Outstanding fines, profile, transactions, and cart are limited to the four signed-in role names | **Implemented** |
| BR-039 | Admin dashboard, settings, and analytics are limited to `SUPER_ADMIN` and `ADMIN` | **Implemented**. The UI does not grant `SUPER_ADMIN` any extra control |
| BR-040 | `MANAGER` uses the same routes as `CUSTOMER` | **Implemented**. **Inferred** that the product does not give managers a separate job in this UI |
| BR-041 | After a successful details load, login stores `strRole` and navigates to `/outstandingfines` | **Implemented** |
| BR-042 | Sign-out calls Cognito `signOut` and deletes `roles`. It does not delete `userDetails`, `cartItems`, or `IDNumbers` | **Implemented** |
| BR-043 | Email templates saved in admin settings must contain the substring `{code}` | **Implemented** in `AdminSettings.js` |
| BR-044 | Graph delete is a `POST /postgraphsettings` body with `Delete: true`, not a separate HTTP method | **Implemented** |

## Copy that the UI does not enforce

| ID | Rule | Provenance |
|---|---|---|
| BR-045 | Terms clause 1.7 says the user is responsible for their username and password | **Documented**. Password storage under `localStorage` key `asdf` during the forgot-password sign-up path is still in `ForgotPassword.js` and is read by `Register.js` |
| BR-046 | Privacy says card numbers may be collected so payment can be processed, and that confidential financial information is used only for that purpose | **Documented**. This UI posts the payer to iVeri and does not show a card-number field of its own |
| BR-047 | Privacy says registration requires cookies, and rejecting cookies blocks registration | **Documented**. The app also stores roles, cart, and profile in `localStorage` |
| BR-048 | Privacy clause 1.10 says personal data is transferred to Amazon Web Services | **Documented** |

## Unknown

- The API's own role names, payment-not-allowed codes, and warrant rule.
- Whether the convenience fee is inclusive of tax.
- Whether a failed `/postiveritransaction` should stop the iVeri POST. The UI does not stop.
- Whether `strRole` is stored as a JSON array. The reader `JSON.parse`s it.
