# Business rules — paymyfines-backend-new

Evidence date: 2026-10-08.

Source: `main` at `c01bc83810ee8dd035870baba60e871e96066d86`.

No business-rules document was in the source tree. Rules below are taken from code. Each rule is marked **Inferred** when the code behaves that way and no comment or document states it as policy. Rules that a comment states outright are marked **Documented in code**.

This file does not promote inferred behaviour into approved policy.

## Operator and product name

| Rule | Provenance |
|---|---|
| Email subjects for sign-up, resent code, and forgot password start with "Pay My Fines:" | **Documented in code** (`GenerateCustomeMail.cs`) |
| Contact mail is sent from and to `help@paymyfines.co.za` unless a secret overrides the from-address on the receipt path | **Implemented.** Constant `TMT_MAIL`. Receipt from-address is secret `pmf-mail` |
| Failed source settlements are emailed to `sshabala@tmt.africa` with subject `PMF Error Payments` | **Implemented.** The commented query that would CC users with an ADMIN role does not run |
| Service names are `tmt-pmf` and `tmt-pmf-prod` | **Implemented** config. The legal entity is **Unknown / Requires Confirmation** |

## Identity

| Rule | Provenance |
|---|---|
| `strIdType` value `2` (and, on fine search, `02`) is validated as a South African ID number: YYMMDD, gender digit, series, citizenship, uniform, control | **Implemented** regular expression in `Model/APIModel.cs`. The comment "Invalid RSA ID Format" is the error string. The meaning of other type codes is **Unknown** |
| Id types other than `2` / `02` are stored and sent upstream with no format check | **Implemented** |
| Contact-us `idNumber` has no format check | **Implemented** |
| A name and surname must be at least 2 characters and must not contain a digit | **Implemented** validation attributes |
| Mobile numbers must be at least 8 characters. The phone regular expression in `APIModel` is commented out | **Implemented** |
| Email must parse as a `MailAddress` and the parsed address must equal the input | **Implemented** |
| An email that already has a non-null Cognito id cannot be registered again. Response text is `User Email Already exists` with HTTP 200 | **Implemented** |
| New users are inserted with `blnActive = 1` | **Implemented.** No handler in this tree sets `blnActive` back to 0 |
| Extra ID numbers live in `userIdNumber` and are included when fines are loaded by `userId` | **Implemented** |
| Query parameter `Id` may be digits or the word `GUEST` | **Implemented** validation. **Inferred:** guest search is allowed. The word is not looked up as a user |

## Access

| Rule | Provenance |
|---|---|
| HTTP routes have no API Gateway authorizer | **Implemented** in `serverless.yml` |
| `userId` is a caller-supplied number. It is not compared with a token | **Implemented** |
| `strRole` is returned with user details. No function grants or denies an action because of it | **Implemented.** **Inferred** from the absence of checks: role enforcement, if any, is not in this API |
| `GET /logs` returns rows for the supplied id. It does not check that the caller is that user | **Implemented** |
| Graph, settings, and finance routes require a numeric `userId` or, for finance, date and authority parameters. They do not require an admin role | **Implemented** |
| CORS allow-origin is `*` | **Implemented** |

## Fines

| Rule | Provenance |
|---|---|
| Two sources are queried: iForce and eNatis. They share one base URL from secret `pmf-enatis` | **Implemented** |
| A notice number that contains `/` is sent to iForce. A notice number that is only digits is sent to eNatis. Anything else is an incorrect format | **Implemented** in `GetQuickFines` |
| iForce attribute `0` means notice number. Attribute `2` means ID. Those meanings are comments next to the literals | **Documented in code** as comments. **Unknown** whether iForce defines further attributes |
| Fines with a zero amount due are omitted (`totalAmountDue` 0 or eNatis `payment.amount` 0) | **Implemented** |
| iForce phase 1 is "Notice", 2 "Summons", 3 "Court", 4 "Warrant", anything else "Unknown" | **Implemented** on `GetAllFines` and `GetQuickFines`. The paginated iForce service does not translate the phase |
| Optional `strIssuingAuthority` keeps iForce rows whose `districtName` matches, and eNatis groups whose `location.issAuthority.description` matches | **Implemented.** The two filters are different fields |
| `customerPaymentAllowed` is false when upstream `paymentAllowed` is false | **Implemented** |
| `customerPaymentAllowed` is false when a local `infringement` row for that notice has `intPaid` equal to `YES`. The checker comment says this flag exists so the frontend can avoid a double payment | **Documented in code** (`PaymentChecker` summary comment) |
| If that local lookup throws, the checker returns false, so payment is not offered | **Implemented** |
| If no local row exists, or no row is `YES`, the checker returns true | **Implemented** |
| Evidence `includeImage` must be the strings true or false (length 4 or 5) | **Implemented** validation. The attribute does not check the actual words beyond length |

## Payments and settlement

| Rule | Provenance |
|---|---|
| A new iVeri transaction is stored unpaid (`intPaid` defaults to `NO`) with `refPaymentProviderID = 1` | **Implemented** |
| The transaction insert writes `dtPaid` as the literal `NO`, not as a datetime | **Implemented** SQL in `PostIveriLiteTransaction` |
| An empty iVeri `LITE_RESULT_DESCRIPTION` means the payment went through | **Documented in code** (comment: "No result in here payment went through") |
| Success sets `transaction.intPaid` and `infringement.intPaid` to `YES`, copies the query `status` into `strStatus`, and stores consumer order id, bank reference, and merchant reference | **Implemented** |
| Result text `Transaction cancelled by customer` sets `intPaid` to `NO` and does not write receipt or bank fields | **Implemented** |
| Any other non-empty result description also sets `intPaid` to `NO` | **Implemented** |
| Convenience fee is the `setting` value for key `convenience`. The receipt multiplies it by the number of infringements | **Implemented.** **Inferred:** one fee per infringement |
| The fee must be numeric when an operator updates it | **Implemented** |
| Settlement treats `strInfringementSource` `IFORCE` as iForce. Every other source is sent to eNatis | **Implemented.** **Inferred:** eNatis/AARTO is the non-iForce source. The code never checks for the string `ENATIS` |
| iForce settlement always sends `paymentProvider = 13` and `terminalId = 0`, and `freshFine = false` | **Implemented.** Comment: "iveri only for now" and "what goes here??" on the terminal id |
| A new local payment is eligible for settlement only when `infringement.intPaid` is `YES` and `infringement.strStatus` is `success` | **Implemented** |
| A failed settlement is retried while `strStatus` is `Source Failed Payment` and `intSettlementTries` is less than 4 | **Implemented.** Comment: "Need to limit on an upper limit of tries max" |
| After a retry increments the counter past 3, the infringement is included in the error email | **Implemented** |
| HTTP 200 from the source sets `strStatus` to `Source Approved Payment` and stores the source receipt or transaction text on the infringement | **Implemented** |
| The cron does not update `transaction.strStatus` to a settled value. That SQL is commented out | **Implemented** |
| The job is scheduled every 2 hours. The YAML comment that says 8 hours is not the rate in force | **Implemented** configuration disagrees with its own comment. **Requires Confirmation** which interval was intended |
| The schedule resource is named `RunCronJobDev` for every stage | **Implemented** |
| `GET /runcronjob` runs the same job with no authorizer | **Implemented** |

## Mail templates

| Rule | Provenance |
|---|---|
| Cognito sign-up, resend, and forgot-password bodies come from `setting.strKey` equal to the trigger source | **Implemented** |
| `{code}` is replaced with the Cognito code. `{email}` is replaced when an email was passed | **Implemented** |
| An operator cannot save a custom-message template unless the stored check sees the placeholder `%7Bcode%7D` | **Implemented** |
| Contact-us body key `CustomMessage_ContactUs` is saved under `strKey` `ContactUs_CustomMessage` | **Implemented** writer. The contact email builder does not read `setting`. **Requires Confirmation** what consumes `ContactUs_CustomMessage` |
| Contact-us mail body is HTML assembled in code, including the postal line `Paymyfines, PO BOX 234, Century City, Cape Town, 7446, South Africa` | **Documented in code** (`MailService.GetEmailTemplate`) |

## Reporting and graphs

| Rule | Provenance |
|---|---|
| Finance rows are payments with `intPaid = YES` and `dtIVeriSettlement` inside the requested range | **Implemented** |
| `issuingAuthority=ALL` returns every authority. Any other value must equal `infringement.strIssuingAuthority` | **Implemented** |
| An empty finance result says `No settings found` | **Implemented** string. **Inferred** copy-paste. It does not mean the settings table is empty |
| Only `graph` rows with `blnActive = 1` are drawn by `GET /getgraphs` | **Implemented** |
| Graph data queries are whatever SQL is stored in `strGraphData` | **Implemented.** The statements are not in git. **Unknown** |
| `GET /getgraphsettings` returns inactive graphs as well | **Implemented** (`SELECT * FROM graph`) |

## Logging

| Rule | Provenance |
|---|---|
| `log` stores `strCognitoUuid`, `txtQuery`, `txtResponse`, and `strType` | **Implemented** insert |
| Log type values seen in calls include `DB`, `TMT`, and combined strings such as `TMT, eNatis` | **Implemented** literals. A controlled vocabulary is **Unknown** |
| A failed log insert is ignored | **Implemented** empty `catch` in `Logger.WriteLog` |

## Conflicts

| Topic | What the code does | What another text says |
|---|---|---|
| Runtime | `net6.0` in the project and in `serverless.yml` | README requires .NET Core 3.1 |
| Region | `eu-west-1` | README examples use `us-east-1` |
| Settlement interval | `rate(2 hours)` | Comment in the same YAML block says every 8 hours |
| iVeri provider id | Insert uses `1`. Settlement sends `13` | No document maps the ids |
| Paid status for settlement | Cron requires infringement `strStatus = success` | iVeri handler stores the query string `status` unchanged. Nothing in this repo sets the literal `success` |
| AARTO local-payment flag | `AartoFineQueryService` computes `customerPaymentAllowed` and then returns the list from before that loop | `PaymentChecker` comment says the flag is for the frontend |
| RSA ID failure on register | A 400 is assigned when type `2` fails validation, and execution continues | A caller can still be inserted if the email is valid |
| Delete payment | Commented route and a header URL | No handler |
| Role admin mail | Commented SELECT on `strRole` | Live mail has an empty CC list |
| Contact template | `POST /postsettings` can store `ContactUs_CustomMessage` | `POST /contactUs` sends HTML built in `GetEmailTemplate` and does not read that row |

Human confirmation is required before treating the code side of these rows as the business rule.
