# Business rules — paymyfines-frontend-new

Evidence date: 2026-10-07.

Source: `main` at `a335e20`. 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** |
| 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 |
| BR-005 | The home screen tells visitors the service is temporarily under maintenance and to phone `+27 21 126 0600` or email `payments@tmt.africa` | **Implemented** copy in `LandingPage.js` |
| BR-006 | Footer 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` |

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 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 (unless business is checked) 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. Comment in `validateFieldType` | The function returns `true` with no pattern | **Implemented** |
| `04` | Business registration. Register checkbox sets this and clears it back to `02` | The function returns `true` with no pattern | **Implemented** |

| ID | Rule | Provenance |
|---|---|---|
| BR-008 | Registration requires email, name, surname, cellphone, ID, matching ID confirmation, password, and matching password confirmation | **Implemented** in `Register.js` |
| BR-009 | Name and surname must match Unicode letters plus space, `.`, apostrophes, and hyphen | **Implemented** field type `Name` |
| BR-010 | Phone must match `^\+?\d{10,13}$` | **Implemented** field type `Phone` |
| BR-011 | Email must match the expression in `validateFieldType` for `Email` | **Implemented** |
| BR-012 | Notice and message text uses field type `Text`: letters, digits, spaces, and a listed set of punctuation | **Implemented**. The home search uses this check. That search is not on the rendered home page |
| BR-013 | Home ID search (unused by the JSX) accepts only type `02` and sends `strIdType=02` | **Implemented** in `LandingPage.js` |
| BR-014 | Quick ID list (`?idno=`) also sends type `02` | **Implemented** in `OutstandingFines.js` |
| BR-015 | A user may store additional ID numbers. Outstanding fines and transactions can filter by `id:type` from `localStorage.IDNumbers` | **Implemented** in `Profile.js`, `OutstandingFines.js`, `Transactions.js` |
| BR-016 | Removing an additional ID and saving the profile removes cart lines whose ID equals that number, or whose ID is null or `N/A` | **Implemented** in `Profile.js` `removeFromCart` |

Registration does not check a minimum password length. Cognito's pool policy is **Unknown / Requires Confirmation**.

## Access

| ID | Rule | Provenance |
|---|---|---|
| BR-017 | With no `roles` value in local storage the visitor is a guest | **Implemented** in `getAllowedRoutes` |
| BR-018 | After a successful user-details load, `strRole` from the API is what the UI treats as the role | **Implemented** in `Login.js`. Legal values the API issues are **Unknown / Requires Confirmation** |
| BR-019 | Guests may open login, register, and forgot password. Signed-in roles are not listed on those routes | **Implemented** route table |
| BR-020 | Outstanding fines, profile, transactions, and cart exclude `GUEST` | **Implemented** route table |
| BR-021 | Admin dashboard and settings allow `SUPER_ADMIN` and `ADMIN` only | **Implemented** |
| BR-022 | `MANAGER` is allowed on customer routes and not on admin routes. No other manager-only behaviour exists | **Implemented** |
| BR-023 | Empty `permission` does not block the route. AARTO and CPA detail use that form | **Implemented** in `getAllowedRoutes` |
| BR-024 | Logged-out fine calls send `Id=GUEST` | **Implemented** in landing search, quick search, and both detail screens |
| BR-025 | Logout clears roles, user details, profile cache, cart, ID list, and back-filters | **Implemented** in `TopNav.js` |

**Inferred.** The UI assumes that if `roles` is in local storage, the user is authenticated (`isLoggedIn`). It does not re-check the Cognito session before that test.

## Fines

| ID | Rule | Provenance |
|---|---|---|
| BR-026 | Two sources are shown when the API returns them: eNatis (AARTO fields such as `infringementNoticeNumber`) and iForce (CPA fields such as `noticeNumber`) | **Implemented** |
| BR-027 | If a source payload is the string "Third party did not respond", or is missing, the list warns that not all infringements may be available | **Implemented** in `OutstandingFines.js` |
| BR-028 | If iForce reports more cases than the page size (default 100), the UI says payment can be made on the displayed set, and that external systems can take up to 4 hours to update | **Implemented** copy |
| BR-029 | An AARTO row is payable in the list only when `paymentNotAllowedCode` is not `3` and `feesList.debt` has at least one entry. Otherwise the UI forces `customerPaymentAllowed` false and shows amount `N/A` | **Implemented** |
| BR-030 | AARTO amount is the sum of `feesList.debt[].debtAmt`, not a single field, except the detail screen also displays `payment.amount` | **Implemented** in `OutstandingFines.js` and `Cart.js`. Detail display uses `payment.amount` in `AARTOFineDetails.js` |
| BR-031 | AARTO demerit points on the list and detail are the sum of `charge[].demeritPoints` | **Implemented** |
| BR-032 | iForce amount is `totalAmountDue`. Status shown on the list is `phase`. Demerit points display as `N/A` | **Implemented** |
| BR-033 | Add to cart is refused when `customerPaymentAllowed` is false or the notice is already a cart key | **Implemented** on the list and both detail screens |
| BR-034 | "Add all" adds only rows with `customerPaymentAllowed === true`, then opens the cart if the cart is non-empty | **Implemented** |
| BR-035 | Notice search treats a value containing `/` or longer than 13 characters as a notice number. Hyphens are stripped. Shorter values without `/` are treated as an ID | **Implemented** in `LandingPage.searchFines`. **Not shown** on the home page |
| BR-036 | A notice result with `infringementNoticeNumber` is AARTO. A result with `noticeNumber` is CPA | **Implemented** in `LandingPage.js` and `QuickFineSearch.js` |

**Unknown / Requires Confirmation.** Clause 5.1.3 refuses payment when a warrant of arrest has been issued. The code uses `paymentNotAllowedCode !== 3` and `customerPaymentAllowed`. No comment equates `3` with a warrant.

## Cart and payment

| ID | Rule | Provenance |
|---|---|---|
| BR-037 | Convenience fee comes from the setting whose `strKey` trims to `convenience` | **Implemented** in `Cart.js` and `AdminSettings.js` |
| BR-038 | Convenience total is that fee times the number of cart lines, added on top of the fine total | **Implemented** in `Cart.total` |
| BR-039 | Each infringement sent to `/postiveritransaction` carries its own `strConvenienceAmount` equal to the per-line fee, while the iVeri convenience line uses the summed convenience total | **Implemented** |
| BR-040 | iForce checkout source string is `IForce`. The other branch is `Aarto` | **Implemented** |
| BR-041 | Missing ID on a checkout line is sent as `N/A` | **Implemented** |
| BR-042 | Currency is `ZAR`. iVeri line amounts are rand times 100. Order amount is the cart total times 100 | **Implemented** |
| BR-043 | The browser posts to iVeri only after `/postiveritransaction` returns `result === true`. Status stored on that call is `Sent to iVeri` and `intPaid` is `NO` | **Implemented** |
| BR-044 | iVeri authorisation URL is `https://portal.nedsecure.co.za/Lite/Authorise.aspx`. Lite version sent is `4.0`. Terminal sent is `77777001`. `Lite_Authorisation` is `false`. `Ecom_TransactionComplete` is `false` | **Implemented** |
| BR-045 | Success return (`status=success`) clears the cart and sends the user home after 9 seconds | **Implemented** in `PaymentNotification.js` |
| BR-046 | `fail`, `error`, and `trylater` keep the cart, show `liteResultDescription` when present, and return to `/Cart` after 9 seconds | **Implemented** |
| BR-047 | Duplicate cart keys are the notice number. Adding the same notice again does not create a second line | **Implemented** |
| BR-048 | Profile save writes billing address `txtBillAddress` as `Home` | **Implemented**. **Inferred** that this is a placeholder, because the profile form does not collect an address. Confirm before treating it as a business rule |
| BR-049 | Checkout puts `strName` in both iVeri first name and last name (`Ecom_BillTo_Postal_Name_Last`) | **Implemented** in `Cart.js`. Surname is not sent as the last name |

The fee's unit (rand, cents, including VAT) is **Unknown / Requires Confirmation**. The UI multiplies it as a number and also multiplies by 100 when building iVeri cent fields.

## Account linking

| ID | Rule | Provenance |
|---|---|---|
| BR-050 | Cognito username stored on the user record is `response.username` from sign-in, written as `strCognitoUuid` | **Implemented** |
| BR-051 | Payload `Profile update required` collects name, surname, mobile, and ID before `POST /register` | **Implemented** |
| BR-052 | Payload `Cognito ID update required` calls `POST /updateregistration` with Cognito uuid and email | **Implemented** |
| BR-053 | If sign-in fails and user details show `dtLastEdit` equal to `1/1/0001 12:00:00 AM`, the user is told to reset the password because it is their first time on the new site | **Implemented** copy in `Login.js` |
| BR-054 | Register and contact require a reCAPTCHA token that `POST /recaptcha` answers with `result === true` and `payload === 'true'` | **Implemented** |
| BR-055 | Contact categories are Reduction Request, Redirection Request, Payment Query, Login Query, Fine Dispute, and Copy of Fine. The placeholder "Select Category Type" is invalid | **Implemented** in `ContactUs.js` |
| BR-056 | Contact submit states that the person agrees to the Paymyfines.co.za terms and privacy policy | **Implemented** copy |
| BR-057 | A new password on the profile must be non-empty to be changed, must differ from the old password, and the old password is required | **Implemented** |
| BR-058 | Email templates saved in admin settings must contain the substring `{code}` | **Implemented** in `AdminSettings.js` |
| BR-059 | Admin convenience fee is sent with `parseInt` | **Implemented**. A non-integer fee is truncated |

## Admin reporting

| ID | Rule | Provenance |
|---|---|---|
| BR-060 | Dashboard date defaults: end is today; start is the last day of the previous month, computed by moving backward while the month stays the same | **Implemented** in `AdminDashboard.js` |
| BR-061 | Finance report filters by issuing authority (or `ALL`) and settlement start/end dates | **Implemented** |
| BR-062 | Graph definitions are data from `/getgraphsettings` and `/getgraphs`, not hard-coded charts, except the type names the UI knows how to draw | **Implemented** |

## Conflicts

| Topic | Terms or copy | Code |
|---|---|---|
| Where the product is published | Terms: `https://all.paymyfines.co.za` | AWS config switches on `https://www.paymyfines.co.za`. Matomo domain is an Amplify hostname. `iveri.html` targets an S3 website |
| Warrant blocks payment | Clause 5.1.3 | `paymentNotAllowedCode` and `customerPaymentAllowed`. Mapping not stated |
| Credit card daily limit | Clause 5.1.4 reserves the right | No counter in the UI |
| Maintenance vs search | Home says the site is down for online settlement | Other routes (login, fines, cart, checkout) are still registered |

These are recorded, not resolved.
