# Technical documentation — pmf-backend-ah

Evidence date: 2026-10-06.

Source: `git@bitbucket.org:overdrive/pmf-backend-ah.git`, `master` at `064c4fa` (2022-05-06).

Secret material is named, not copied. That includes the reCAPTCHA literal in `classes/Constants.cs`, the RDS secret, and the SES SMTP secret.

## What this repository is

A single C# project deployed as one Lambda zip by the Serverless Framework. Every HTTP function and the Cognito trigger use the same artifact and the class `PMF.Handler`.

| Fact | Value | Evidence |
|---|---|---|
| Service | `tmt-pmf-prod` | `serverless.yml` |
| Runtime | `dotnetcore3.1` | `serverless.yml`, `AwsDotnetCsharp.csproj` |
| Stage | `production` | `serverless.yml` |
| Region | `eu-west-1` | `serverless.yml`, `aws-lambda-tools.default.json`, `Constants.REGION` |
| AWS profile | `pmf-prod` | `serverless.yml`, `aws-lambda-tools.default.json` |
| Handler prefix | `AwsDotnetCsharp::PMF.Handler::` | `serverless.yml` |
| Zip | `bin/release/netcoreapp3.1/pay-my-fines-lambda.zip` | `serverless.yml`, `build.cmd` |
| Documented base URL | `https://puru95wxc8.execute-api.eu-west-1.amazonaws.com/production` | Comment block in `serverless.yml` |
| Last commit | `064c4fa`, 2022-05-06 | git |

The README still describes a starter service `myService`, region `us-east-1`, stage `dev`, and functions `hello` and `getquerystring`. The "Silicon sequence" at the bottom of the README deploys with profile `pmf` and curls `https://4row8qyxh1.execute-api.us-east-1.amazonaws.com/dev/hello`. Those names are not in `serverless.yml`.

## Repository layout

| Path | Role |
|---|---|
| `Handler.cs` | All Lambda entry points (~3,700 lines) |
| `serverless.yml` | Functions, VPC, schedule, CORS |
| `AwsDotnetCsharp.csproj`, `AwsDotnetCsharp.sln` | Project `AwsDotnetCsharp`, namespace of handlers is `PMF` |
| `classes/` | `Constants`, `Helper`, `Logger` |
| `Services/` | HTTP envelope, secrets, payments, receipts, mail, graphs |
| `Model/` | DTOs. `Model/InfringementReceipt.cs.cs` has a doubled extension |
| `TestRequests/` | Lambda test-tool bodies. They contain personal identifiers |
| `Assets/` | `mystyle.css`, `pmf_logo.svg` |
| `build.cmd`, `build.sh` | `dotnet restore` and `dotnet lambda package` |
| `setup.sh` | Installs `Amazon.Lambda.TestTool-3.1` version `0.9.5` |
| `Properties/launchSettings.json` | Mock Lambda Test Tool on port 5050 |
| `aws-lambda-tools.default.json` | Profile, region, Release, `netcoreapp3.1` |
| `sonar-scanner.bat` | SonarScanner for MSBuild invocation script |
| `.sonarqube/`, `.scannerwork/` | Generated scanner output committed in `064c4fa` |
| `renovate.json` | Extends `config:base` |
| `yarn.lock` | Four lines. No `package.json` |
| `resources/images/lambda_test_tool.png` | README screenshot |

Not in `origin/master`, although the onboarding task listed them at the scan-host root: `sonar-project.properties`, `sonarqube_scan.bat`. A generated copy of scanner properties is `.sonarqube/out/sonar-project.properties` (project key `TMTBackend`, base dir `C:\wamp64\www\PMF\pmf-backend`).

`.gitignore` ignores `bin/`, `obj/`, `.vs`, `.serverless`, and `.lambda-test-tool`. It does not ignore `.sonarqube` or `.scannerwork`.

There is no test project, no `bitbucket-pipelines.yml`, no Dockerfile, and no SQL migration directory.

## NuGet dependencies

From `AwsDotnetCsharp.csproj`:

| Package | Version |
|---|---|
| Amazon.Lambda.APIGatewayEvents | 2.4.0 |
| Amazon.Lambda.Core | 1.2.0 |
| Amazon.Lambda.Serialization.Json | 1.8.0 |
| Amazon.Lambda.AspNetCoreServer | 5.3.0 |
| AWSSDK.S3 | 3.5.1 |
| MySql.Data | 8.0.23 |
| Newtonsoft.Json | 12.0.3 |
| Amazon.Extensions.CognitoAuthentication | 2.0.3 |
| AWSSDK.CognitoIdentityProvider | 3.5.1.17 |
| AWSSDK.SecretsManager | 3.5.0 |

`Select.HtmlToPdf.NetCore` is commented out. PDF generation is the HTTP service, not this package. `Amazon.Lambda.AspNetCoreServer` is referenced. No ASP.NET controller is in the tree; entry points are Lambda methods. The serializer is `Amazon.Lambda.Serialization.Json.JsonSerializer` (assembly attribute in `Handler.cs`).

## Deployment

`build.cmd` (and the equivalent `build.sh` `dotnet lambda package` line):

```text
dotnet restore
dotnet lambda package --configuration release --framework netcoreapp3.1 --output-package bin/release/netcoreapp3.1/pay-my-fines-lambda.zip
```

`serverless deploy` publishes the CloudFormation stack. `sls remove` is documented in the README as the way to destroy it.

`package.individually: true` is set together with a single `package.artifact`. Every function in the template points at that same zip.

VPC on the provider, so every function uses it:

- Security group `sg-0ef1507ae1fcd0913`
- Subnets `subnet-0c1aaf57b8d9fdb93`, `subnet-0827171db5e6ea7bb`

Memory is not set. Timeout is the platform default except where the template sets it.

| Function | Timeout |
|---|---|
| `getEvidence`, `getQuickFines`, `getStreamEvidence`, `getoutstandingfines`, `getcharges`, `queryinfringements`, `queryinfringer`, `payinfringement`, `postIveriLiteTransaction` | 30 seconds |
| `getAllFines`, `iVeriPayment` | 45 seconds |
| All other functions | Not set in the template |

`provisionedConcurrency: 5` is commented on the functions.

Local debug: `Properties/launchSettings.json` starts `dotnet-lambda-test-tool-3.1.exe` on port 5050. The README says the starter returns hello world. That function is not in this template.

## HTTP API

CORS is `true` on every HTTP event. `HttpService` also sets `Access-Control-Allow-Origin: *`.

No API key, usage plan, Cognito authorizer, or IAM authorizer is active.

| Serverless function | Method and path | Handler |
|---|---|---|
| `isRecaptchaValid` | `POST /recaptcha` | `IsRecaptchaValid` |
| `getLoggedInUserDetails` | `GET /details` | `GetLoggedInUserDetails` |
| `postUserDetails` | `POST /update` | `PostUserDetails` |
| `getUserDetails` | `GET /user/details` | `GetUserDetails` |
| `getAPILogs` | `GET /logs` | `GetAPILogs` |
| `getTransactions` | `GET /transactions` | `GetTransactions` |
| `registerUser` | `POST /register` | `RegisterUser` |
| `registerpayment` | `POST /registerpayment` | `PostPayment` |
| `getEvidence` | `GET /evidence` | `GetEvidence` |
| `getAllFines` | `GET /getallfines` | `GetAllFines` |
| `getQuickFines` | `GET /getfinesearch` | `GetQuickFines` |
| `getStreamEvidence` | `GET /streamevidence` | `GetStreamEvidence` |
| `getoutstandingfines` | `GET /getoutstandingfines` | `GetOutstandingFines` |
| `getcharges` | `GET /getcharges` | `GetCharges` |
| `queryinfringements` | `GET /queryinfringements` | `QueryExternalInfringement` |
| `queryinfringer` | `GET /queryinfringer` | `QueryExternalInfringer` |
| `payinfringement` | `POST /payinfringement` | `PayExternalInfringement` |
| `updateRegistration` | `POST /updateregistration` | `UpdateRegistration` |
| `postsettings` | `POST /postsettings` | `PostUpdateSettings` |
| `getFinanceReportData` | `GET /reports` | `GetFinanceReportData` |
| `getgraphs` | `GET /getgraphs` | `GetGraphs` |
| `getsettings` | `GET /getsettings` | `GetSettings` |
| `getgraphsettings` | `GET /getgraphsettings` | `GetGraphSettings` |
| `postgraphsettings` | `POST /postgraphsettings` | `PostGraphSettings` |
| `postIveriLiteTransaction` | `POST /postiveritransaction` | `PostIveriLiteTransaction` |
| `iVeriPayment` | `POST /iveripayment` | `IVeriPayment` |
| `RunCronJob` | `GET /runcronjob` and schedule | `RunCronJob` |
| `generateCustomMessage` | Cognito `CustomMessage`, pool `tmt-pmf-prod-user-pool`, `existing: true` | `GenerateCustomEmail` |

`GET /reports` is not in the comment list at the top of `serverless.yml`. `deletepayment` is commented and has no method.

The usual body is JSON `{ "result": true|false, "payload": ... }` from `Services/ResponseService.cs`. `IVeriPayment` returns 302 and header `location` instead. Passing `exception:` to `CreateResponse` replaces the body with the exception message.

## Schedule

```yaml
name: RunCronJobDev
rate: rate(2 hours)
```

The description string says "run the cronjob every 8 hours". A comment says to use `rate(2 hours)` for testing and shows a cron expression for 18:00 daily as an alternative. The HTTP route is the same handler. `authorizer: aws_iam` is commented.

The handler returns 200 `"Cronjob completed"` even when the try/catch logged an exception.

## Data

MySQL via `MySql.Data`. Connection string from `SecretsManager.GetDBSecret()`, with `convert zero datetime=True`. The Lambda constructor always calls Secrets Manager and opens `MySqlConnection`.

Secret id: `arn:aws:secretsmanager:eu-west-1:475415325198:secret:pmf-prod-rds-6mUVsR`.

`DbUser` fields used: `host`, `username`, `password`, `dbInstanceIdentifier` (used as the database name).

Tables named in SQL: `user`, `userIdNumber`, `transaction`, `infringement`, `charge`, `paymentProvider`, `setting`, `graph`, `log`.

There is no `CREATE TABLE` in the repository. Column order is part of the contract for several readers (`GetValue(n)` / `GetName(n)`). `GetAllAssociateIds` reads the user's id number from ordinal 14 and id type from ordinal 4, then additional ids from `userIdNumber` ordinals 5 and 4.

Writes use string-built SQL. `Helper.CleanInput` strips a slash-escape list, HTML tags, and doubles single quotes. It is not used on every interpolated value (email templates are stored raw; graph SQL is executed; iVeri body fragments are interpolated). `GetFinanceReportData` is the handler that uses MySql parameters (`@issuingAuthority`, `@startSettlementDate`, `@endSettlementDate`).

`Logger.WriteLog` inserts into `log` with the Cognito uuid, query text, response text, and a type string. The SQL is concatenated.

## Integrations

### Internal fine API

Base URL `http://10.106.3.22:80/api/` (`Constants.IFORCE_ENATIS_API_BASE_URL`).

| Call | Path |
|---|---|
| iForce fines | `GET IForce/queryfines?Attribute={n}&FineQueryAttributeValue={value}` |
| iForce charges | `GET IForce/querycharge?NoticeNumber={n}` |
| iForce evidence | `GET IForce/queryevidence?EvidenceToken={t}&IncludeImage={b}` |
| iForce stream | `GET IForce/queryevidencestream?SessionToken={s}&EvidenceToken={t}` |
| iForce payment | `POST IForce/registerpayment` |
| eNaTIS infringer | `GET infringement/queryinfringer?IdDocumentType={t}&IdDocumentNumber={n}` |
| eNaTIS infringement | `GET infringement/queryinfringement?InfringementNoticeNumber={n}` |
| eNaTIS payment | `POST infringement/payinfringement` |

Attribute `0` is a notice number. Attribute `2` is an identity number (comments in `GetQuickFines` and `GetAllFines`).

`Helper.ServerCertificateCustomValidation` returns true for these clients.

Timeout constant: 10 seconds. Not applied on every eNaTIS client.

`GetAllFines` fans out one HTTP call per stored identity (`Task.WhenAll` in `GetIForceIdsData` and `GeteNatisIdsData`).

### iVeri Lite

`IVeriPayment` parses `application/x-www-form-urlencoded` style fields out of the body after `__EVENTVALIDATION`: `LITE_RESULT_DESCRIPTION`, `ECOM_CONSUMERORDERID`, `MERCHANTREFERENCE`, `LITE_BANKREFERENCE`. Query: `veriLocation`, `status`, `transactionID`.

The payment page itself is not implemented here. `PostIveriLiteTransaction` only stores the reference the client already has (`striVeriNumber`).

### reCAPTCHA

`POST https://www.google.com/recaptcha/api/siteverify` with `secret` and `response`. The secret literal lives in `Constants.reCAPTCHA_KEY`. A second literal is commented above it.

### PDF, S3, mail

`PaymentReceiptService.GenerateReceipt`:

1. Builds an HTML receipt (logo URL on `master.d39rr3ejryd4rk.amplifyapp.com`, enquiries in the HTML).
2. `POST http://10.106.3.158:8080/htmltopdf` with JSON `{ "html": ... }`.
3. Waits 1.5 seconds, then `GetObject` on bucket `tmt-pmf-prod-invoices`, region `eu-west-1`, key `PdfMicro.filename`.
4. Sends the PDF with SES SMTP. On PDF or S3 failure, an HTML-only mail is attempted when a path was returned.

SMTP secret: `arn:aws:secretsmanager:eu-west-1:475415325198:secret:pmf-prod-ses-smtp-0oR1H2`.

`SesUser` supplies `SMTP_Username` and `SMTP_Password`. Host `email-smtp.eu-west-1.amazonaws.com`, port 587, SSL. From `noreply@paymyfines.co.za`, display name `PMF`.

`Mailer.SendEmail` uses the same SMTP settings for cron failure mail. To address is hardcoded. The method rethrows after a failed send, which the cron catch logs.

### Cognito

The template attaches `GenerateCustomEmail` to an existing pool. The handler reads `triggerSource`, `request.codeParameter`, and `request.userAttributes.email`, and writes `response.emailSubject` and `response.emailMessage`. Template HTML is a row in `setting`. Placeholder stored in the database must include the characters `%7Bcode%7D`; the sender replaces `{code}` after the row is read.

`AWSSDK.CognitoIdentityProvider` and `Amazon.Extensions.CognitoAuthentication` are package references. No handler in this tree calls the Cognito admin API.

## Auth

There is no JWT validation, no API key, and no check that `userId` matches `requestContext.identity`.

`Constants` defines `INVALID_SESSION_TOKEN`, `AUTHENTICATION_FAILED`, and `INVALID_STRUCTURE`. Those three strings are not referenced outside `Constants.cs`.

## Background work

The only job is `RunCronJob`. It is not a queue consumer. There is no SQS, SNS, or Step Functions resource in the template.

Settlement HTTP calls are sequential per infringement inside `PaymentService`. A failed HTTP call `continue`s without updating that row.

## Configuration and environments

No `.env` and no stage-specific serverless file. Production values are literals: profile, VPC ids, secret ARNs, internal IPs, reCAPTCHA secret, S3 bucket, mail addresses, convenience key name.

`Constants.TimeDefaultValue` is 10 (seconds). `NO_RESPONSE` (`"Third party did not respond"`) is the shared upstream-failure string. `INVALID_SESSION_TOKEN`, `AUTHENTICATION_FAILED`, and `INVALID_STRUCTURE` are declared in `Constants` and are not referenced by a handler.

## Logging and monitoring

Application logs go to the MySQL `log` table and to `ILambdaContext.Logger` (`Logger.LogMessage`). There is no CloudWatch metric, alarm, or X-Ray configuration in the template. The README points operators at CloudWatch Logs for HTTP 500s.

SonarScanner artifacts are committed. They are the output of a scan of `C:\wamp64\www\PMF\pmf-backend`, project key `TMTBackend`, not a runnable quality gate for this Bitbucket repository.

## Security notes visible in the tree

These are observations about the implementation, not a pentest.

- HTTP routes accept a caller-supplied user id.
- SQL is concatenated. Graph SQL stored via `POST /postgraphsettings` is executed.
- `IVeriPayment` redirects to the supplied `veriLocation`.
- TLS validation is disabled for the internal fine API.
- A reCAPTCHA secret is committed in source.
- `.sonarqube` output, including analysis metadata, is committed.
- `TestRequests/` contains real-looking personal identifiers.
- S3 and Secrets Manager calls use the Lambda execution role (default SDK client, no keys in code). The role is not defined in `serverless.yml`; Serverless creates a default role unless the account already overrides it. Which permissions that role has in AWS is **Unknown / Requires Confirmation**.

## Development and testing

Documented tools: .NET Core 3.1 SDK, Node.js, Serverless CLI, `Amazon.Lambda.Tools`, `Amazon.Lambda.TestTool-3.1`, VS Code C# extension or Visual Studio.

`TestRequests/` holds sample API Gateway events for get-all-fines, finance, graphs, user details, profile update, and iVeri transaction. There is no automated test project and no CI definition.

`renovate.json` extends the shared Renovate base config. Whether Renovate is enabled on the Bitbucket repository is **Unknown**.

## Related names that this repo does not link

The Amplify host in the receipt, the pool name `tmt-pmf-prod-user-pool`, and the local path `PMF\pmf-backend` suggest other Pay My Fines projects. This repository does not name their git remotes. Whether Silicon Ark directories for a frontend or the internal fine API belong to the same product is **Unknown / Requires Confirmation**.
