# Business rules — paymyfines-backend

Evidence date: 2026-10-08.

Source: `master` at `5b98fa7640e9cd4d1a71da5f18b107899179c4be`.

There is no business-rules document in the source repository. Rules below are taken from code. Each rule is labelled.

**Inferred** means the code behaves this way and no separate requirement states it.

## Identity and access

**BR-001 — HTTP calls are not authenticated.** **Implemented.** `serverless.yml` sets CORS and no authorizer. A caller-supplied `userId`, `Id`, or `strCognitoUuid` is the only identity on most routes.

**BR-002 — `Id` may be the word GUEST.** **Implemented.** `Model/APIModel.cs` allows digits or `GUEST`. Guest is not a `user` row. It is written into logs as the caller id on notice search.

**BR-003 — Role does not gate routes.** **Implemented.** `strRole` is returned with user details. Graphs, settings, logs, transactions, and the cron HTTP route do not read it. The only role check is the cron failure mail: `strRole` must equal the literal `["ADMIN"]`, including the bracket and quote characters.

**BR-004 — Cognito is not called to prove the session.** **Implemented.** Registration stores a Cognito id from the body. The Custom Message Lambda is invoked by the user pool. No handler validates a JWT.

## People and ID numbers

**BR-005 — One email and one Cognito id.** **Implemented.** `POST /register` refuses an email that already has a row (200, "User Email Already exists") and a Cognito id that already has a row (200, "User CognitoUuid Already exists"). It does not update the existing row.

**BR-006 — New users are active.** **Implemented.** Insert sets `blnActive` to 1. No route in this repository sets it back to 0.

**BR-007 — RSA ID shape is required only for some type codes.** **Implemented.**

| Type value | Where | Rule |
|---|---|---|
| `"2"` | Register, profile update, transactions | `strIdNumber` must match the 13-digit RSA pattern in `ValidData.StrIdNumber` |
| `"2"` or `"02"` | `GET /getallfines` | Same RSA pattern |
| Any other numeric type, max length 2 | Those routes | Number is accepted without the RSA pattern |
| Missing type when an ID number is sent | `getallfines`, transactions | 400 |

The pattern is YYMMDD, then one gender digit, three series digits, citizenship, a uniform digit, and a control digit. It does not check the Luhn control digit or that the date is a real calendar date. **Implemented.**

**BR-008 — A failed type-2 check on register or update does not stop the write.** **Implemented.** The handler stores a 400 response and continues. A valid email or a valid `userId` can still insert or update.

**BR-009 — A user may hold extra ID numbers.** **Implemented.** `userIdNumber` rows are inserted or updated on `POST /update` and deleted when they are absent from `additionalIds`. `GET /getallfines` by `userId` queries iForce and eNatis for the profile ID and every extra ID.

**BR-010 — Names cannot contain digits.** **Implemented.** `strName` and `strSurname` must match `^([^0-9]*)$` and be at least 2 characters when those fields are validated. Email format is checked with `MailAddress` on register only.

**BR-011 — Mobile numbers are length-checked only.** **Implemented.** Minimum length 8. A stricter phone regex is commented out.

## Notice lookup

**BR-012 — Slash versus digits chooses the source.** **Inferred** from `GetQuickFines`, and **Implemented** there. A notice containing `/` is sent to iForce. A digits-only notice is sent to eNatis. Anything else returns HTTP 200 with a format message and does not call upstream.

**BR-013 — Notice numbers are short and limited in charset.** **Implemented.** Minimum length 3. Allowed characters are letters, digits, and `/`.

**BR-014 — iForce phase codes.** **Implemented** in outstanding-fines and all-fines.

| Code | Label |
|---|---|
| 1 | Notice |
| 2 | Summons |
| 3 | Court |
| 4 | Warrant |
| other | Unknown |

**BR-015 — A notice already marked paid locally cannot be paid again here.** **Implemented.** `PaymentChecker` reads `infringement.intPaid`. Value `YES` makes `customerPaymentAllowed` false. No row, or any other value, makes it true. A database exception makes it false.

**BR-016 — Upstream `paymentAllowed` false blocks local payment only on the logged-in all-fines path.** **Implemented.** `GetAllFines` when searching by `userId` sets `customerPaymentAllowed` false if `fine.paymentAllowed` is false. The same method's ID-number branch, and `GetOutstandingFines`, do not apply that override.

**BR-017 — Issuing-authority filter is an exact string match.** **Implemented.** Optional `strIssuingAuthority` on `getallfines` keeps iForce fines whose `districtName` equals the filter and eNatis fines whose `location.issAuthority.description` equals the filter.

**BR-018 — iForce ID search uses attribute 2. Notice search uses attribute 0.** **Implemented.**

## Payments

**BR-019 — The local payment provider id is 1.** **Implemented.** Basket insert and both settlement payloads set provider 1. `LoadInfringements` comments "iveri only for now".

**BR-020 — A basket is stored before the card result.** **Implemented.** `POST /postiveritransaction` inserts the transaction and infringements. Default `intPaid` is `NO` when the body omits it. `dtPaid` is the literal string `NO` on that insert, not a timestamp.

**BR-021 — iForce charges stored from the basket have zero demerit points.** **Implemented.** Comment: demerit points are not available for iForce. eNatis charges store the upstream `demeritPoints`.

**BR-022 — Empty iVeri result description is treated as success.** **Implemented**, and **Requires Confirmation** against iVeri. The handler comment says to check. Success sets `intPaid = YES` on the transaction and its infringements, copies query `status` into `strStatus`, and emails a receipt.

**BR-023 — Customer cancel is unpaid and has no receipt.** **Implemented.** Description `Transaction cancelled by customer` sets `intPaid = NO` and does not write receipt, bank, or merchant columns.

**BR-024 — Any other iVeri description is unpaid.** **Implemented.** `intPaid = NO`. The handler still reads receipt fields that were not filled in this branch.

**BR-025 — Convenience fee is a setting, times the number of infringements.** **Implemented.** Key `convenience` must be numeric. The receipt multiplies it by the infringement count and labels it "Convenience Fee(R{amount} per Infringement)".

**BR-026 — Settlement runs only for status `success` plus `intPaid = YES`, and again for source-failed infringements.** **Implemented.** See PRD FR-011. If the iVeri query `status` is not the literal `success`, pass 2 of the cron will not pick the row up.

**BR-027 — A successful source post is settled.** **Implemented.** Transaction `intPaid` becomes `SETTLED` and both transaction and infringement `strStatus` become `Source Approved Payment`.

**BR-028 — The first source failure is marked failed with one try.** **Implemented.** `intSettlementTries` is set to 1 and statuses become `Source Failed Payment`.

**BR-029 — Later failures count tries, and more than three tries raises an email.** **Implemented.** iForce retry increments the counter and keeps the failed status. eNatis retry increments the counter and writes `strSourceResponse` without setting `strStatus` in that statement. Either way, tries greater than 3 add the row to the problem mail.

**BR-030 — Failure mail has a fixed primary recipient.** **Implemented.** To `lloyd@overdrive.co.za`. CC is the email column (ordinal 5) of users whose role matches BR-003. From `noreply@paymyfines.co.za`, display name `PMF`. Subject `PMF Error Payments`.

**BR-031 — iForce settlement sends `terminalId` 0 and `freshFine` true.** **Implemented.** The terminal id is a code comment, not a confirmed business value.

**BR-032 — The cron HTTP route and the schedule do the same work, and both report success.** **Implemented.** `GET /runcronjob` is open. The method returns 200 `"Cronjob completed"` even when the body of the method caught an exception.

## Mail and settings

**BR-033 — Cognito mail subjects are fixed. Bodies come from settings.** **Implemented.**

| Trigger | Subject |
|---|---|
| `CustomMessage_SignUp` | Pay My Fines: Sign Up |
| `CustomMessage_ResendCode` | Pay My Fines: Resent code |
| `CustomMessage_ForgotPassword` | Pay My Fines: Forgot Passowrd |

The body row must contain `%7Bcode%7D` before it will be saved. At send time `{code}` is replaced with Cognito's code parameter.

**BR-034 — Only four setting keys are writable by the API.** **Implemented.** The three Custom Message keys and `convenience`. Other keys can be read when `setting.Active = 1`.

**BR-035 — Receipt from-address is the constant, not a setting.** **Implemented.** `noreply@paymyfines.co.za`.

## Graphs

**BR-036 — Active graph queries are executed as SQL.** **Implemented.** `blnActive = 1`. Placeholders `{startDate}` and `{endDate}` are replaced with the request dates. A title containing `NedBank` uses a built-in transaction listing and ignores the dates.

**BR-037 — Saving a graph runs it once against a fixed demo range.** **Implemented.** `{startDate}` and `{endDate}` become `2012-02-02` before insert or update. The comment calls this a demo date.

**BR-038 — Delete returns immediately.** **Implemented.** The first graph record that carries `Delete` is removed and the request ends, even if later records were in the body.

## Logging and data handling

**BR-039 — Queries and upstream bodies are stored in `log`.** **Implemented.** Columns `strCognitoUuid`, `txtQuery`, `txtResponse`, `strType`. Types seen in code include `DB`, `TMT`, `iForce`, `eNatis`, `eNATiS`. `WriteLog` swallows its own exceptions.

**BR-040 — Single quotes are doubled. SQL is still concatenated.** **Implemented.** `CleanInput` is not used on every interpolated value.

**BR-041 — Upstream TLS errors are ignored.** **Implemented.** The certificate callback returns true.

## Unknown / requires confirmation

- The catalogue of `strIdType` values and whether `"02"` must match `"2"` on every route.
- Whether empty iVeri result text means a captured payment.
- Whether `strStatus = success` is the value iVeri is told to send.
- Whether more than three settlement tries, the fixed mailbox, and the `["ADMIN"]` role string are the approved operations rule.
- Whether a second payment must be blocked when the local row says `YES` but the source has not reached `SETTLED`.
- Whether guests are allowed to search and pay. The code allows the `GUEST` token on validated `Id` fields. Payment still needs a numeric `userId` on the basket.
