# PRD — paymyfines-backend-new

Evidence date: 2026-10-08.

Source: `git@bitbucket.org:overdrive/paymyfines-backend-new.git`, `main` at `c01bc83` (2025-02-27). Full hash `c01bc83810ee8dd035870baba60e871e96066d86`.

There is no prior PRD in the source repository or in Silicon Ark for this directory. `README.md` is a Serverless .NET starter and is not a product specification. Provenance is labelled on each requirement.

## Product purpose

**Implemented.** HTTP Lambdas record a Pay My Fines user, look up infringements, take an iVeri Lite payment result, store the payment, email a receipt, and later submit the payment to iForce or eNatis.

**Documented in user-facing strings.** Mail subjects use the name "Pay My Fines". The constant from-address is `help@paymyfines.co.za`. The contact-us HTML footer gives the postal line `Paymyfines, PO BOX 234, Century City, Cape Town, 7446, South Africa`.

**Not documented.** No statement in this repository names the legal operator, the traffic authorities, or the problem statement. The service name in config is `tmt-pmf` / `tmt-pmf-prod`. Log type strings use `TMT`. **Inferred** from those names and from `sshabala@tmt.africa` as the failed-settlement mailbox: the operator side is TMT. Confirmation is still required.

## Users and interactions

This API does not authenticate the HTTP caller. `serverless.yml` sets `cors: true` on every HTTP event and sets no `authorizer`. A commented `authorizer: aws_iam` sits on `RunCronJob` only.

| Actor | What the code shows | Provenance |
|---|---|---|
| HTTP caller | Anyone who can invoke API Gateway. Identity is whatever `userId`, `Id`, or `strCognitoUuid` the body or query sends | **Implemented** |
| Registered user | A row in `user`, created by `POST /register` or linked by `POST /updateregistration` | **Implemented** |
| Guest | Query parameter `Id` may be the word `GUEST` (`Model/APIModel.cs`). Several fine lookups pass that value through and use it as a log id | **Implemented** |
| Cognito | Existing user pool invokes `GenerateCustomEmail` on Custom Message | **Implemented** trigger wiring. Pool behaviour beyond the three trigger sources below is **Unknown** |
| Settlement schedule | EventBridge invokes `RunCronJob` every 2 hours. The same function is also `GET /runcronjob` | **Implemented** |
| Operator | `strRole` is read from `user` and returned by user-detail handlers. No handler branches on it. An admin-email lookup is commented out in the cron job | **Implemented** absence of a check. Who may call admin routes is **Unknown / Requires Confirmation** |

`HttpService.CreateResponse` wraps most bodies as `{ "result": true|false, "payload": ... }`. Status 200–299 set `result` true. Other statuses set `result` false unless `forcedResult` is passed. `Access-Control-Allow-Origin` is `*`. `IVeriPayment` does not use this wrapper. It returns HTTP 302 with a `location` header.

## Functional requirements

### FR-001 — Register a user

**Requirement.** `POST /register` creates a `user` row when the email is not already stored with a non-null Cognito id.

**Who.** HTTP caller. The body supplies the Cognito id. This function does not call Cognito.

**Trigger.** `POST /register`.

**Inputs.** JSON body. Required by `Helper.ValidateRequestParameters`: `strCognitoUuid`, `strEmail`, `strName`, `strSurname`, `strIdType`, `strMobileNumber`. Name and surname must match `^([^0-9]*)$` and be at least 2 characters. `strIdType` max length 2 and digits only. Mobile minimum length 8. Email must pass `MailAddress` parsing.

**Behaviour.**

- If `strIdType` is `"2"`, `strIdNumber` must match the RSA ID regular expression in `Model/APIModel.cs`. On failure the code assigns a 400 response and then continues. It does not return. **Implemented.** Whether a bad RSA ID still inserts a row depends on the email check that follows.
- If a `user` row already has that email and a non-null `strCognitoUuid`, respond 200 with payload `User Email Already exists`.
- Otherwise insert `blnActive = 1`, `strFirstUser` = name, `dtFirstEdit` = `NOW()`, and return 201 with name, surname, and email taken from the new row by column ordinal.

**Outputs.** 201 dictionary, 200 already exists, 400 validation or invalid email, 500 create failure or exception text.

**Evidence.** `Functions/Internal/RegisterUser.cs`, `serverless.yml` function `registerUser`.

**Status.** Implemented.

**Confidence.** High for the happy path. The missing `return` after a failed RSA ID check is **Requires Confirmation** as a defect.

### FR-002 — Attach a Cognito id to an existing email

**Requirement.** `POST /updateregistration` sets `user.strCognitoUuid` for a matching email, then returns UserID, Cognito id, name, surname, and email.

**Inputs.** `strEmail`, `strCognitoUuid`. Email must pass `IsAValidEmail`.

**Behaviour.** The `UPDATE` runs even when no row matches. The following `SELECT` by the new Cognito id returns 500 with `An error was encountered while updating user: Check if the email is registered` when no row is found.

**Evidence.** `Functions/Internal/UpdateRegistration.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-003 — Read and update the profile

**Requirement.**

- `GET /details` (`GetLoggedInUserDetails`) and `GET /user/details` (`GetUserDetails`) load a user by `userId` (numeric) or `strCognitoUuid`. `GetUserDetails` also accepts `strEmail`.
- `GetUserDetails` also loads rows from `userIdNumber` for that user.
- `POST /update` updates the profile and inserts, updates, or deletes `userIdNumber` rows from `additionalIds`.

**Inputs for update.** `userId`, `strName`, `strSurname`, `strIdType`, `strMobileNumber`, `txtBillAddress`, `additionalIds`. If `strIdType` is `"2"`, `strIdNumber` is checked with the RSA ID pattern. On failure the code assigns a 400 response and then continues into the update. It does not return. **Implemented.** Same shape as FR-001.

**Outputs.** User field dictionaries keyed by column name via `GetName(ordinal)`. `strRole` is the comment on ordinal 6 in `GetUserDetails`. The ordinal-to-column map is not enforced by a schema file. **Unknown** if ordinal 6 is still `strRole` in the live database.

**Evidence.** `Functions/Internal/GetLoggedInUserDetails.cs`, `GetUserDetails.cs`, `PostUserDetails.cs`.

**Status.** Implemented.

**Confidence.** Medium on field names, high on the routes.

### FR-004 — Cognito custom email

**Requirement.** On Cognito trigger sources `CustomMessage_SignUp`, `CustomMessage_ResendCode`, and `CustomMessage_ForgotPassword`, replace the email subject and body.

**Behaviour.** The body is `setting.strValue` where `strKey` equals the trigger source. `{code}` is replaced with Cognito's `codeParameter`. `{email}` is replaced when an email attribute is present. Forgot-password passes an empty email, so `{email}` is left unchanged. Other trigger sources are returned unchanged.

**Evidence.** `Functions/External/Cognito/GenerateCustomeMail.cs`. Pool names: `tmt-pmf-user-pool` (development), `tmt-pmf-prod-user-pool` (production) in `environmentConfig.yml`. The function sets environment variable `USERPOOL` and does not read it.

**Status.** Implemented.

**Confidence.** High.

### FR-005 — reCAPTCHA check

**Requirement.** `POST /recaptcha` posts the body field `recaptcha` and a secret to `https://www.google.com/recaptcha/api/siteverify`.

**Behaviour.** HTTP 200 payload `true` or `false` according to Google's `success` field. Both are `result: true` because the status is 200. Exceptions return 500.

**Evidence.** `Functions/External/IsRecaptchaValid.cs`, `classes/Constants.cs`.

**Status.** Implemented. The secret is a literal in source. See constraints.

**Confidence.** High.

### FR-006 — List fines for an identity

**Requirement.** `GET /getallfines` and `GET /getpaginatedfines` return iForce and eNatis infringements for a numeric `userId` or for a supplied `strIdNumber`.

**Who.** HTTP caller. `userId` must be digits (`APIModel.UserId`). It is not compared with a session.

**Inputs.**

- `userId`, or `strIdNumber` plus `strIdType`.
- Optional `strIssuingAuthority` filter.
- Paginated route also accepts `pageSize`, `pageStart`, `pageEnd`. Defaults in `GetPaginatedFines` are 10, 1, and 1. Further query names are in that method.

**Behaviour.**

- `userId` loads the user's own id (column ordinal 14 as the number, ordinal 4 as the type) and every `userIdNumber` row for that user (`GetAllAssociateIds`). If the user row is missing, HTTP 200 payload `userId does not exist`.
- Id type `2` or `02` must match the RSA ID expression. Other types are not format-checked.
- iForce open fines are requested with attribute `2` (comment in `GetAllFines`: ID). Bulk and paginated calls use `IForce/bulkqueryopenfines` and `IForce/queryopaginatedfines`. Fines with `totalAmountDue` of 0 are dropped. Phase codes 1–4 are labelled Notice, Summons, Court, Warrant on the non-paginated path. The paginated service copies `fine.phase` through unchanged (`IForceFineQueryService`).
- eNatis calls `infringement/queryinfringer?IdDocumentType=&IdDocumentNumber=`. Amount 0 is dropped on the AARTO service path.
- `customerPaymentAllowed` is false when the upstream fine says `paymentAllowed == false`, or when `PaymentChecker` finds `infringement.intPaid = 'YES'` for that notice number. Otherwise it is true. If the checker throws, it returns false.
- Upstream TLS errors are accepted. `Helper.ServerCertificateCustomValidation` returns true.

**Outputs.** Combined iForce and eNatis payloads inside the standard envelope. Empty upstream failures are logged to `log` and often become empty lists rather than HTTP errors. Exact empty-state strings differ by branch.

**Evidence.** `Functions/External/GetAllFines.cs`, `Services/IForceFineQueryService.cs`, `Services/AartoFineQueryService.cs`, `Services/PaymentChecker.cs`.

**Status.** Implemented.

**Confidence.** High on the route and the two sources. Medium on response field completeness, because the handler is large and several branches differ.

**Conflict inside the implementation.** `AartoFineQueryService.QueryAsync` fills a list that includes `customerPaymentAllowed`, then returns `flatArr`, which is the raw upstream list and does not include that flag. Callers of `QueryAsync` do not receive the flag this method computed.

### FR-007 — Search one notice

**Requirement.** `GET /getfinesearch` looks up one notice number.

**Inputs.** `noticeNumber` (min length 3, charset letters, digits, and `/`) and `Id` (digits or `GUEST`).

**Behaviour.**

- If the notice contains `/`, call iForce `IForce/queryopenfines?Attribute=0&FineQueryAttributeValue={notice}`. Attribute 0 is the comment "notice number" on the outstanding-fines handler.
- Else if the notice is digits only, call eNatis `infringement/queryinfringement?InfringementNoticeNumber={notice}`.
- Any other format returns HTTP 200 payload `Format: {value} is incorrect`.
- The JSON payload always has keys `iForce` and `eNatis`. The source that was not queried stays `Third party did not respond` (`Constants.NO_RESPONSE`).

**Evidence.** `Functions/External/IForce/GetQuickFines.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-008 — iForce evidence, charges, and outstanding notice

**Requirement.** These routes proxy the upstream base URL. They require `Id` (digits or `GUEST`) plus the fields below. They do not check that `Id` owns the notice.

| Route | Required query | Upstream |
|---|---|---|
| `GET /getoutstandingfines` | `noticeNumber`, `Id` | `IForce/queryopenfines?Attribute=0&FineQueryAttributeValue=` |
| `GET /getcharges` | `noticeNumber`, `Id` | `IForce/querycharge?NoticeNumber=` |
| `GET /evidence` | `evidenceToken`, `Id`, `includeImage` (`true`/`false`, length 4 or 5) | `IForce/queryevidence?EvidenceToken=&IncludeImage=` |
| `GET /streamevidence` | `sessionToken`, `evidenceToken`, `userId` (digits) | `IForce/queryevidencestream?SessionToken=&EvidenceToken=` |

`evidence` and `streamevidence` set Lambda timeout 30 seconds in `serverless.yml`.

**Evidence.** `Functions/External/IForce/GetOutstandingFines.cs`, `GetCharges.cs`, `GetEvidence.cs`, `GetStreamEvidence.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-009 — eNatis query and immediate pay

**Requirement.**

- `GET /queryinfringements` — `noticeNumber`, `Id`. Proxies `infringement/queryinfringement`. Drops rows whose `payment.amount` is 0. Adds `customerPaymentAllowed` from `PaymentChecker`.
- `GET /queryinfringer` — `strIdType`, `strIdNumber`, `userId`. Proxies `infringement/queryinfringer`.
- `POST /payinfringement` — requires query `userId`. Posts the request body to `infringement/payinfringement`.

These are separate from the cron settlement in FR-012. `PostPayment` (`POST /registerpayment`) is the iForce equivalent and its own comment says the call is not used except from the cron job. The HTTP function is still deployed.

**Evidence.** `Functions/External/Enatis/QueryInfringement.cs`, `QueryExternalInfringer.cs`, `PayExternalInfringement.cs`, `Functions/External/IForce/PostPayment.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-010 — Record an iVeri Lite transaction before payment

**Requirement.** `POST /postiveritransaction` inserts one `transaction` row and one `infringement` row per entry in `infringements`.

**Inputs.** `striVeriNumber`, `userId` (digits), `strStatus`. Optional `intPaid` (default `NO`), `dtPaid` (default the string `NULL`, and the INSERT still writes `dtPaid` as the literal `NO` in the SQL, ignoring `dtPaid` for the transaction row). `strAmount` is interpolated and is not in the required-parameter list. Each infringement object is expected to carry `strInfringementSource`, `strInfringementNumber`, `strConvenienceAmount`, `strAmount`, `intDemeritPoints`, and optional id, date, authority, and vehicle fields.

**Behaviour.** `refPaymentProviderID` is inserted as `1`. `intPaid` on the new transaction is the request value or `NO`. The handler returns 200 with a subset of transaction columns. Charge-lookup methods `GetIForceInfringementCharge` and `GetENatisInfringementCharge` exist on the class and are not called by `PostIveriLiteTransaction`.

**Evidence.** `Functions/External/Payments/PostIveriLiteTransaction.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-011 — Apply the iVeri return and email a receipt

**Requirement.** `POST /iveripayment` reads iVeri Lite form fields from the body and query, updates `transaction` and `infringement`, and on success asks the PDF service for a receipt and emails it.

**Inputs.** Query `veriLocation`, `status`, `transactionID`. Body must contain `__EVENTVALIDATION`. The code then splits on `&` and reads `LITE_RESULT_DESCRIPTION`, and on the success branch `ECOM_CONSUMERORDERID`, `MERCHANTREFERENCE`, and `LITE_BANKREFERENCE`.

**Behaviour.**

- Empty `LITE_RESULT_DESCRIPTION` is treated as success. `intPaid` becomes `YES`, `strStatus` becomes the query `status`, receipt number is the consumer order id, bank reference and merchant reference are stored, `dtIVeriSettlement` is `NOW()`. Matching infringements get `intPaid = YES` and the same status.
- Convenience fee is `setting.strValue` where `strKey = 'convenience'`, parsed as a double. The receipt multiplies that fee by the number of infringements (`PaymentReceiptService`).
- Receipt HTML is posted to the PDF base URL from secret `pmf-pdf-server`. The mail is sent with SES credentials from `pmf-ses-smtp`. From-address for this mailer path comes from secret `pmf-mail` (`GetTMTEmail`). S3 save is commented out; a private `ReadFromS3` remains.
- The function responds HTTP 302 to `{veriLocation}?PaymentNotification?transactionID={id}&status={status}`.
- Non-empty result description sets `intPaid = NO`. Cancelled-by-customer (`Transaction cancelled by customer`) omits receipt, bank, and merchant fields. Other failures try to store those fields from variables that were not assigned on this branch (they stay `Not returned by iVeri` until substring logic runs). **Implemented.** This branch can throw. **Requires Confirmation** whether iVeri always sends the success field names on failure.
- `POST /generatereceipt` is a second handler on the same class (`GenerateReceipt` in `IVeriPayment.cs`). It rebuilds a receipt from `strIVeriNumber`. Timeout 45 seconds, same as `iveripayment`.

**Evidence.** `Functions/External/Payments/IVeriPayment.cs`, `Services/PaymentReceiptService.cs`.

**Status.** Implemented.

**Confidence.** High on success. Medium on the failure branch.

### FR-012 — Settle payments with the source

**Requirement.** `RunCronJob` submits locally paid infringements to iForce or eNatis.

**Trigger.** EventBridge `rate(2 hours)`, schedule name `RunCronJobDev` (hardcoded, including when the stage is production). Also `GET /runcronjob` with no authorizer. The YAML comment says "every 8 hours" and "2 hours for testing". The rate that is deployed is 2 hours.

**Behaviour.**

1. Load infringements with `strStatus = 'Source Failed Payment'` and `intSettlementTries < 4`. Split on `strInfringementSource`: `IFORCE` (case-insensitive) goes to iForce. Every other source goes to eNatis.
2. iForce `POST IForce/registerpayment` with `freshFine = false`, `paymentProvider = 13`, `terminalId = 0`. HTTP 200 sets infringement `strStatus` to `Source Approved Payment` and stores transaction id and result description. Other HTTP statuses set `Source Failed Payment`, increment tries, and after the incremented count passes 3 the row is added to a problem list.
3. eNatis `POST infringement/payinfringement` with notice number and amount. HTTP 200 sets `Source Approved Payment` and stores pay result, invoice number, and receipt number. The retry overload increments tries and does not set `Source Failed Payment` again (the status is already that value). The first-attempt overload sets `Source Failed Payment` and `intSettlementTries = 1`.
4. If the problem list is non-empty, email subject `PMF Error Payments` to `sshabala@tmt.africa`. The admin lookup by `strRole` is commented out, so the CC list is empty.
5. Then load `infringement.intPaid = 'YES'` and `infringement.strStatus = 'success'` and run the first-attempt settlement.
6. The HTTP response is always 200 `Cronjob completed`, including after a caught exception. Exceptions are written to `log`.

**Evidence.** `Functions/Jobs/RunCronJob.cs`, `Services/PaymentService.cs`, `Services/MailService.cs`, `serverless.yml` function `RunCronJob`.

**Status.** Implemented.

**Confidence.** High.

**Requires Confirmation.** `POST /postiveritransaction` stores `strStatus` from the caller, and `IVeriPayment` stores the iVeri query `status`. The cron only settles rows whose infringement status is exactly `success`. If iVeri never sends `status=success`, new payments are never settled. That contract is not written down in this repo.

### FR-013 — Transactions list

**Requirement.** `GET /transactions` returns payment rows for a numeric `userId` or for `strIdNumber` plus `strIdType`.

**Behaviour.** Joins `user`, `transaction`, and `infringement`. Groups by `strIVeriNumber`. Id type `2` validates the RSA ID pattern. The response maps columns by ordinal. An id-number search reads ordinal 16 as `TransactionId` while the SELECT list's last selected expressions are settlement time and bank reference. **Implemented.** The ordinal map may not match the selected columns. **Requires Confirmation** against a real row.

**Evidence.** `Functions/Internal/GetTransactions.cs`.

**Status.** Implemented.

**Confidence.** Medium.

### FR-014 — Finance report

**Requirement.** `GET /reports` returns settled payments in a date range.

**Inputs.** Query `issuingAuthority`, `startSettlementDate`, `endSettlementDate`. `Helper` is asked to validate those names, but `APIModel.ValidData` has no attributes that run for those names, and the switch in `Helper` does not assign them. The check therefore only requires the keys to be present.

**Behaviour.** Parameterised SQL (one of the few queries that uses parameters) selects infringements joined to transactions where `intPaid = 'YES'` and `dtIVeriSettlement` is between the two dates. `issuingAuthority=ALL` skips the authority filter. Columns returned: settlement time, issuing authority, channel (`strInfringementSource`), bank ref, notice number, amount, receipt number. No rows returns HTTP 200 payload `No settings found`.

**Evidence.** `Functions/Internal/GetFinanceReportData.cs`. Comment in the SQL: "adjusted as per statuses update - lloyd".

**Status.** Implemented.

**Confidence.** High.

### FR-015 — Settings and Cognito templates

**Requirement.**

- `GET /getsettings` returns `strKey` and `strValue` from `setting` where `Active = 1`. Requires numeric `userId`. Does not check a role.
- `POST /postsettings` updates keys `CustomMessage_SignUp`, `CustomMessage_ResendCode`, `CustomMessage_ForgotPassword`, `convenience`, and the body key `CustomMessage_ContactUs` which is stored as `ContactUs_CustomMessage`.
- Custom-message values must contain the placeholder `%7Bcode%7D` (`Constants.CognitoCodePlaceHolder`) before they are URL-decoded and saved. `convenience` must parse as a double. HTML is kept for the sign-up template (comment: not cleaned, to allow HTML from the frontend).

**Evidence.** `Functions/Internal/GetSettings.cs`, `PostUpdateSettings.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-016 — Graphs

**Requirement.**

- `GET /getgraphs` runs active graph definitions and returns chart data. `GraphService` comment: the shape is tied to the frontend and should not change unless the frontend changes.
- `GET /getgraphsettings` returns every `graph` row.
- `POST /postgraphsettings` inserts, updates, or deletes `graph` rows. Requires `userId` and does not check a role. Queries to execute are stored in `strGraphData`.

**Evidence.** `Functions/Internal/GetGraphs.cs`, `GetGraphSettings.cs`, `PostGraphSettings.cs`, `Services/GraphService.cs`.

**Status.** Implemented. The SQL each graph runs is data, not source. **Unknown** which charts exist in a given database.

**Confidence.** High for the mechanism.

### FR-017 — API log read

**Requirement.** `GET /logs?userId=` returns `log` rows whose `strCognitoUuid` equals that `userId` value. The column name is Cognito-shaped. The filter value is the numeric user id parameter, not a verified Cognito subject.

**Evidence.** `Functions/Internal/GetApiLogs.cs`, `classes/Logger.cs`.

**Status.** Implemented.

**Confidence.** High.

### FR-018 — Contact us

**Requirement.** `POST /contactUs` emails the contact form. It does not insert a database row.

**Inputs.** `strName`, `strSurname`, `strEmail`, `idNumber`, `strMessage`. Optional JSON fields on `ContactUsModel`: `strTicketNumber`, `strCaterogy` (spelled that way in the model). `idNumber` is `ContactUsIdNumber` and has no format attributes.

**Behaviour.** Valid email only. `Mailer.SendEmail` builds an HTML body in `GetEmailTemplate` (name, surname, ID, email, ticket number, message). It does not read `setting`. The footer text is `Paymyfines, PO BOX 234, Century City, Cape Town, 7446, South Africa`. SES sends that HTML to `help@paymyfines.co.za` (`Constants.TMT_MAIL`). Subject is `contactUs.Caterogy`. `ReplaceEmailPlaceHolders` can load a `setting` row but `SendEmail` does not call it.

**Evidence.** `Functions/Internal/ContactUs.cs`, `Services/MailService.cs`, `Model/ContactUsModel.cs`.

**Status.** Implemented.

**Confidence.** High.

## Major workflows

### Look up fines and decide if they can be paid again

1. Caller sends `GET /getallfines` or `GET /getpaginatedfines` with `userId` or an id number and type.
2. The Lambda loads associated id numbers from `user` and `userIdNumber` when `userId` is used.
3. It calls iForce and eNatis on the shared base URL.
4. For each fine it checks `infringement.intPaid` locally.
5. The payload tells the caller `customerPaymentAllowed`.

`GET /getfinesearch` is the single-notice variant. A slash means iForce. Digits only means eNatis.

### Pay

1. Caller `POST /postiveritransaction` stores the basket as `transaction` plus `infringement` rows, unpaid.
2. The caller completes iVeri outside this repository.
3. iVeri or the caller `POST /iveripayment`.
4. On an empty Lite result description, rows become paid and a receipt email is sent.
5. The browser is redirected to `{veriLocation}?PaymentNotification?...`.
6. On the schedule, `RunCronJob` posts `success` rows to iForce or eNatis and retries `Source Failed Payment` up to the try limit.

### Account

1. Cognito (outside this API) creates the login and fires Custom Message.
2. `POST /register` or `POST /updateregistration` writes the MySQL user.
3. `POST /update` maintains extra id numbers used by the fine search.

## Integrations

| System | Direction | Trigger | Data | Auth visible in this repo | Failure |
|---|---|---|---|---|---|
| MySQL via secret `pmf-rds` | Both | Almost every function | Users, fines, payments, settings, graphs, logs | Username and password inside the secret JSON (`host`, `username`, `password`, `port`, `dbInstanceIdentifier`) | Most handlers return 500 with `ex.Message`. Logger swallows its own insert errors |
| Secrets Manager `eu-west-1` | Read | Process start | Secret ids below | Lambda IAM role, not in this repo | Exception thrown to the caller |
| iForce and eNatis | Outbound HTTP | Fine, charge, evidence, pay, cron | Paths under `IForce/` and `infringement/` | None in the HttpClient. TLS validation disabled | Log row, empty payload, or `Third party did not respond`. Cron continues to the next row |
| iVeri Lite | Inbound HTTP | `POST /iveripayment` | Form body and query | None | 302 with `liteResultDescription`, or 500 |
| Google reCAPTCHA | Outbound | `POST /recaptcha` | Token plus committed secret | Secret in source | 500 or `false` |
| Amazon SES SMTP | Outbound | Receipt, contact, failed settlement | HTML or plain body | Secret `pmf-ses-smtp`, port 587 | Contact and receipt log or throw. Cron mail throw is caught by the cron `catch` |
| PDF service | Outbound | Successful iVeri return and `POST /generatereceipt` | HTML | Secret `pmf-pdf-server` field `IP` used as the POST URL | Logged, receipt flag false, payment still marked paid |
| S3 | Configured | Upload method commented out | Invoice bucket name from `pmf-invoices-bucket` | SDK default chain | Not on the live receipt path |
| Cognito | Inbound trigger | Custom Message | Code and email attributes | Existing pool named in `environmentConfig.yml` | Returns the input event. Exceptions are logged and the input is still returned |
| EventBridge | Inbound | `rate(2 hours)` | No payload used | Schedule name `RunCronJobDev` | Function still returns 200 |

Secret ids: `pmf-rds`, `pmf-ses-smtp`, `pmf-enatis` (JSON field `IFORCE_ENATIS_API_BASE_URL`), `pmf-invoices-bucket`, `pmf-pdf-server`, `pmf-mail`.

## Constraints

- Region is `eu-west-1` in `serverless.yml` and `Constants.REGION`.
- Runtime is `dotnet6`. README still says .NET Core 3.1.
- One deployment zip: `bin/release/net6.0/pay-my-fines-lambda.zip`.
- Development API host written in the `serverless.yml` header comments: `https://h8vzvoqq9c.execute-api.eu-west-1.amazonaws.com/development/...`. Whether that stack still exists is **Unknown**.
- No API key and no Cognito authorizer on HTTP routes.
- reCAPTCHA secret is committed in `classes/Constants.cs`. The value is not copied into this PRD.
- Most SQL is string concatenation. `Helper.CleanInput` strips HTML and doubles single quotes. It is not parameterised queries. Finance report SQL is parameterised.
- Upstream HTTPS certificates are not validated.
- There is no automated test project. `TestRequests/` holds sample event JSON only.

## Requirements that are assumptions

- That `status=success` is the iVeri value which makes a payment eligible for settlement. **Unknown / Requires Confirmation.**
- That payment provider id `1` on insert and provider id `13` on iForce settlement are the same iVeri provider. The settlement comment says "iveri only for now". **Inferred.**
- That iForce phase 1–4 labels match the product language (Notice, Summons, Court, Warrant). The labels are **Implemented** strings. The legal meaning is **Inferred**.
- That `paymyfines-frontend-new` is the client of this API. Path overlap supports that. **Requires Confirmation.**
- That column ordinals used when reading `SELECT *` still match production. **Requires Confirmation.**
