# Technical documentation — paymyfines-backend

Evidence date: 2026-10-08.

Source: `git@bitbucket.org:overdrive/paymyfines-backend.git`, `master` at `5b98fa7640e9cd4d1a71da5f18b107899179c4be` (2022-02-15).

`README.md` is the Silicon Overdrive serverless .NET starter. Where it disagrees with `serverless.yml` and the project file, the YAML and the code are the implementation. Disagreements are listed under Documentation drift.

## Stack

| Item | Value | Evidence |
|---|---|---|
| Language | C# | `Handler.cs` and project file |
| Framework | `netcoreapp3.1` | `AwsDotnetCsharp.csproj` |
| Lambda runtime | `dotnetcore3.1` | `serverless.yml` `provider.runtime` |
| Hosting | AWS Lambda, API Gateway, EventBridge, Cognito, Secrets Manager, S3, SES SMTP | `serverless.yml`, services |
| Region | `eu-west-1` | `serverless.yml`, `Constants.REGION`, `aws-lambda-tools.default.json` |
| Stage | `production` | `serverless.yml` |
| Service name | `tmt-pmf-prod` | `serverless.yml` |
| AWS profile in Serverless | `pmf-prod` | `serverless.yml` |
| AWS profile in Lambda tools | `pmf` | `aws-lambda-tools.default.json` |
| Data | MySQL through `MySql.Data` 8.0.23 | Project file and SQL strings |
| JSON | Newtonsoft.Json 12.0.3 | Project file |
| Deploy tool | Serverless Framework | `serverless.yml`, README |
| Package | `dotnet lambda package` | `build.cmd`, `build.sh` |

NuGet references: `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, `AWSSDK.SecretsManager` 3.5.0, `AWSSDK.CognitoIdentityProvider` 3.5.1.17, `Amazon.Extensions.CognitoAuthentication` 2.0.3. The HTML-to-PDF package is commented out. AspNetCoreServer is referenced. No ASP.NET pipeline is configured in this tree. The entry point is `PMF.Handler`.

Serializer: assembly attribute `Amazon.Lambda.Serialization.Json.JsonSerializer` on `Handler.cs`.

## Repository layout

```text
Handler.cs                         all Lambda methods (namespace PMF)
classes/Constants.cs               URLs, mail, reCAPTCHA, region
classes/Helper.cs                  validation dispatch, CleanInput, TLS callback
classes/Logger.cs                  MySQL log plus CloudWatch line
Services/SecretsManager.cs         RDS and SES secrets
Services/ResponseService.cs        HTTP envelope (class HttpService)
Services/PaymentChecker.cs
Services/PaymentService.cs
Services/PaymentReceiptService.cs
Services/MailService.cs            class Mailer
Services/GraphService.cs
Model/                             request and upstream DTOs
Model/ResponseModel/               Cognito, iForce, eNatis, PDF, HTTP error
Model/APIModel.cs                  namespace ResponseModel.APIModel
Assets/                            pmf_logo.svg, mystyle.css
resources/images/                  lambda test tool screenshot used by the README
serverless.yml
build.cmd / build.sh / setup.sh
aws-lambda-tools.default.json
Properties/launchSettings.json
bin/release/netcoreapp3.1/pay-my-fines-lambda.zip    committed package
.serverless/                       committed CloudFormation templates and state
```

`Model/InfringementReceipt.cs.cs` is the filename on disk (double extension).

There is no `Functions/` split, no test project, and no `bitbucket-pipelines.yml`.

## Deployment

`serverless.yml` packages one zip for every function (`package.individually: true`, `artifact: bin/release/netcoreapp3.1/pay-my-fines-lambda.zip`).

`build.cmd` restores and runs `dotnet lambda package` in Release to that zip path.

`build.sh` restores, installs `zip` on Debian if `/etc/debian_version` exists, and packages Debug to `bin/release/netcoreapp3.1/hello.zip`. That output name does not match `serverless.yml`.

`setup.sh` installs global tool `Amazon.Lambda.TestTool-3.1` at version `0.9.5`.

VPC attachment on the provider, so every function receives it:

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

Commented URL block names API `puru95wxc8` in `eu-west-1`, stage `production`. That is a comment. This session did not call the API.

`bin/release/.../pay-my-fines-lambda.zip` and `.serverless/serverless-state.json` plus the two CloudFormation templates are tracked. They are a deployment snapshot from whenever they were committed, not proof of what AWS is running today. This document does not copy values out of the state file.

`renovate.json` extends `config:base`. `yarn.lock` has a v1 header and no packages.

## Functions and timeouts

Handler class: `AwsDotnetCsharp::PMF.Handler::{Method}`.

Lambda timeout is set only on the functions below. Other functions omit `timeout`. Serverless Framework's own default when the field is omitted is 6 seconds. That default is framework behaviour, not a line in this repo. **Inferred** for any function without an entry in this table.

| Function | Timeout in YAML |
|---|---|
| `getEvidence`, `getQuickFines`, `getStreamEvidence`, `getoutstandingfines`, `getcharges`, `queryinfringements`, `queryinfringer`, `payinfringement`, `postIveriLiteTransaction` | 30 |
| `getAllFines`, `iVeriPayment` | 45 |

HTTP client timeout for upstream calls is 10 seconds (`Constants.TimeDefaultValue`). `QueryExternalInfringer`, `QueryExternalInfringement`, and `PayExternalInfringement` construct an `HttpClient` without that timeout.

Cognito function `generateCustomMessage` has no HTTP event. It uses:

```yaml
cognitoUserPool:
  pool: tmt-pmf-prod-user-pool
  trigger: CustomMessage
  existing: true
```

Schedule on `RunCronJob`:

- Name `RunCronJobDev`
- Description text: "run the cronjob every 8 hours"
- Rate: `rate(2 hours)`
- Comment: 2 hours is for testing; a cron example for 18:00 daily is commented

`RunCronJob` also has `GET /runcronjob`.

## Configuration and secrets

Hardcoded in `classes/Constants.cs`:

| Name | Value |
|---|---|
| `REGION` | `eu-west-1` |
| `TMT_MAIL` | `noreply@paymyfines.co.za` |
| `TMT_MAIL_HOST` | `email-smtp.eu-west-1.amazonaws.com` |
| `IFORCE_ENATIS_API_BASE_URL` | `http://192.168.0.41:8080/api/` |
| `PDF_MICRO_BASE_URL` | `http://10.106.3.158:8080/htmltopdf` |
| `reCAPTCHA_URL` | `https://www.google.com/recaptcha/api/siteverify` |
| `reCAPTCHA_KEY` | Present in source. Not copied here |
| `CognitoCodePlaceHolder` | `%7Bcode%7D` |
| `NO_RESPONSE` | `Third party did not respond` |
| `TimeDefaultValue` | 10 |

Secrets Manager, both `AWSCURRENT`, region `eu-west-1`, account `475415325198`:

| Method | Secret id |
|---|---|
| `GetDBSecret` | `arn:aws:secretsmanager:eu-west-1:475415325198:secret:pmf-prod-rds-6mUVsR` |
| `GetSesUser` | `arn:aws:secretsmanager:eu-west-1:475415325198:secret:pmf-prod-ses-smtp-0oR1H2` |

`DbUser` fields used: `host`, `username`, `password`, `dbInstanceIdentifier`. `port` exists on the class and is not used. Connection string: `server={host};userid={username};password={password};database={dbInstanceIdentifier};convert zero datetime=True`.

`SesUser` fields: `SMTP_Username`, `SMTP_Password`. SMTP port 587, SSL enabled.

S3 bucket name hardcoded in `PaymentReceiptService`: `tmt-pmf-prod-invoices`, region `EUWest1`. `ReadFromS3` uses the filename returned by the PDF service. `SaveToS3` writes a key of timestamp plus email and has no caller.

No `.env` file is in git. No appsettings file is in git.

## Data

No DDL or migration is in the repository. Tables and columns below appear in SQL strings. Types and nullability are **Unknown**.

| Table | Used for | Columns named in code |
|---|---|---|
| `user` | Profile | `UserID`, `strCognitoUuid`, `strName`, `strSurname`, `strEmail`, `strIdType`, `strIdNumber`, `strMobileNumber`, `blnActive`, `strFirstUser`, `strLastUser`, `dtFirstEdit`, `dtLastEdit`, `txtBillToAddress`, `strRole` |
| `userIdNumber` | Extra IDs | `refUserID`, `strName`, `strSurname`, `strIdType`, `strIDNumber`, `strFirstUser`, `strLastUser`, `dtLastEdit`, `dtFirstEdit` |
| `setting` | Templates and convenience | `strKey`, `strValue`, `Active` |
| `log` | Call log | `strCognitoUuid`, `txtQuery`, `txtResponse`, `strType` |
| `graph` | Dashboard definitions | `GraphID`, `strGraphType`, `strGraphTitle`, `strGraphData`, `strDescription`, `strXColumn`, `strYColumn`, `blnActive` |
| `transaction` | Card payment | `TransactionID`, `striVeriNumber` / `strIVeriNumber` / `strIVeriNUmber`, `refClientID`, `intPaid`, `dtPaid`, `dtPayment`, `refPaymentProviderID`, `strStatus`, `strAmount`, `strVeriResponse`, `dtFirstEdit`, `dtLastEdit`, `strRecieptNumber`, `strBankRef`, `dtIVeriSettlement`, `strMerchantReference` |
| `infringement` | Notice on a transaction | `InfringementID`, `refTrasactionID`, `strInfringementSource`, `strIdNumber`, `dtInfringementDate`, `refClientID`, `strInfringementNumber`, `intPaid`, `dtPaid`, `strIssuingAuthority`, `strVechicleRegistration`, `strStatus`, `strConvenienceAmount`, `strAmount`, `intDemeritPoints`, `strSourceResponse`, `intSettlementTries` |
| `charge` | Offence lines | `refInfringementID`, `intDemeritPoints`, `strChargeDescription` |
| `paymentProvider` | Join only | `PaymentProviderID`, `strProviderName` |

Ordinal reads assume a fixed column order (`GetValue(n)`). A schema change that reorders columns changes the API without a compile error. **Inferred** from the ordinal reads.

`intPaid` values written by this code: `NO`, `YES`, `SETTLED`. It is selected and compared as a string.

## Upstream HTTP paths

Base URL: `Constants.IFORCE_ENATIS_API_BASE_URL`.

| Path | Verb | Caller |
|---|---|---|
| `IForce/queryfines?Attribute=&FineQueryAttributeValue=` | GET | Quick search, outstanding, all fines |
| `IForce/querycharge?NoticeNumber=` | GET | Charges route and basket insert |
| `IForce/queryevidence?EvidenceToken=&IncludeImage=` | GET | Evidence |
| `IForce/queryevidencestream?SessionToken=&EvidenceToken=` | GET | Stream |
| `IForce/registerpayment` | POST | `POST /registerpayment` and cron |
| `infringement/queryinfringement?InfringementNoticeNumber=` | GET | Quick search, eNatis query, eNatis charges |
| `infringement/queryinfringer?IdDocumentType=&IdDocumentNumber=` | GET | Person query and all fines |
| `infringement/payinfringement` | POST | `POST /payinfringement` and cron |

`Helper.ServerCertificateCustomValidation` returns true, so certificate errors do not fail these calls.

## Auth

No JWT, API key, or IAM authorizer is attached to HTTP events. Cognito authentication extensions are a package reference. The only Cognito integration that is wired is the Custom Message trigger.

reCAPTCHA is a separate check on `POST /recaptcha`. Other routes do not call it.

## Background work

One scheduled Lambda, `RunCronJob`, described in the PRD. It is not a queue worker. There is no SQS, SNS, or separate process in this repo.

The cron is also exposed as HTTP GET.

## Receipt path

1. Build HTML in `GetReceiptHTMLString`. The logo image is `https://master.d39rr3ejryd4rk.amplifyapp.com/static/media/pmf_logo_pos_1.447e4375.svg`. Fonts are requested from Google Fonts. `Assets/pmf_logo.svg` is not referenced.
2. POST `{ "html": ... }` to `PDF_MICRO_BASE_URL`.
3. Deserialize `PdfMicro` (`filename`, `path`).
4. Wait 1.5 seconds.
5. `GetObject` on `tmt-pmf-prod-invoices` with that filename.
6. SES message to the payee, attachment name `PMF_Payment_Receipt.pdf`.
7. On S3 failure, if `path` is set, send the HTML body instead.

## Logging and monitoring

- `Logger.LogMessage` writes to the Lambda context logger: request id, function name, message.
- `Logger.WriteLog` inserts into MySQL `log` and ignores failures.
- No metrics, alarms, X-Ray, or health endpoint are defined in the repo.
- Several 500 responses return `ex.Message` to the caller.

## Security notes visible in the tree

These are observations, not a pentest.

- HTTP API has no authorizer.
- SQL is string-built. `CleanInput` strips tags and doubles quotes, and it is not applied everywhere.
- reCAPTCHA secret is in source.
- Secret ARNs include the AWS account id.
- TLS validation for upstream HTTP is disabled.
- CORS allow-origin is `*`, except the iVeri 302 which omits CORS on purpose.
- `GET /logs` and `GET /runcronjob` are deployed without a role check.
- Graph SQL stored in MySQL is executed by the API.
- A zip and Serverless state are committed. Treat the state file as sensitive until someone confirms it holds no credentials.

## Local development and tests

README and `Properties/launchSettings.json` describe the Amazon Lambda Mock Test Tool on port 5050 (`dotnet-lambda-test-tool-3.1`). Launch profile name: `Mock Lambda Test Tool`. Executable path: `%USERPROFILE%\.dotnet\tools\dotnet-lambda-test-tool-3.1.exe`.

No unit or integration test project is in the tree. No CI definition is in git.

`.gitignore` ignores `bin/`, `obj/`, `.serverless/`, and `.vscode/`. `bin/release/.../pay-my-fines-lambda.zip` and `.serverless/*` are tracked anyway, so ignore rules do not remove them.

The onboarding issue's scan-host listing included `sonar-project.properties` and `sonarqube_scan.bat`. They are not in `git ls-files` at this revision.

## Documentation drift

| README or comment | Code |
|---|---|
| Example region `us-east-1`, stage `dev`, service `myService` | `eu-west-1`, `production`, `tmt-pmf-prod` |
| Silicon sequence profile `pmf` and API `4row8qyxh1` in `us-east-1` | Profile `pmf-prod` in YAML; tools file profile `pmf` |
| Hello-world and `getquerystring` | Not in `serverless.yml` |
| Test tool 0.10.0 | `setup.sh` pins 0.9.5 |
| Schedule comment "every 8 hours" | `rate(2 hours)`, rule name `RunCronJobDev` |
| `build.sh` output `hello.zip` | YAML artifact `pay-my-fines-lambda.zip` (`build.cmd` matches the YAML) |

## Related repositories

Not referenced by a URL in this code, except the PDF and upstream IPs. Silicon Ark directories that share the Pay My Fines name are listed in `project-plan.md`. `paymyfines-backend-new` is a later repository with a similar route list and a different project layout. Whether it replaced this service is **Unknown / Requires Confirmation**.
