# Technical documentation — paymyfines-backend-new

Evidence date: 2026-10-08.

Source: `main` at `c01bc83810ee8dd035870baba60e871e96066d86` (2025-02-27).

## Stack

| Item | Evidence |
|---|---|
| Language | C# |
| Framework | `net6.0` (`AwsDotnetCsharp.csproj`, `serverless.yml` `provider.runtime: dotnet6`) |
| Hosting | AWS Lambda + API Gateway, Serverless Framework |
| Region | `eu-west-1` |
| Database client | `MySqlConnector` 2.2.7 |
| JSON | Newtonsoft.Json 12.0.3, plus `Amazon.Lambda.Serialization.Json` as the Lambda serializer (`PmfBase.cs` assembly attribute) |
| AWS SDK | Secrets Manager, S3, Cognito Identity Provider. Cognito authentication extensions package is referenced. No sign-in call was found in the handlers |
| Serialiser assembly | `Amazon.Lambda.Serialization.Json.JsonSerializer` |

`README.md` and `setup.sh` still refer to the .NET Core 3.1 Lambda test tool (`Amazon.Lambda.TestTool-3.1`). The project that is packaged targets `net6.0`.

Solution file: `AwsDotnetCsharp.sln`. Project file: `AwsDotnetCsharp.csproj`. Namespace on handlers is `PMF`. The assembly name used in `serverless.yml` handlers is `AwsDotnetCsharp`.

## Application structure

```text
AwsDotnetCsharp.csproj
serverless.yml
environmentConfig.yml
developmentVpcConfig.yml
productionVpcConfig.yml
classes/            Pmfbase, Helper, Logger, Constants
Functions/
  External/         fines, payments, recaptcha, Cognito mail
  Internal/         users, settings, graphs, reports, contact, logs
  Jobs/             RunCronJob
Model/              request and response types
Services/           secrets, payments, mail, graphs, fine query
TestRequests/       sample API Gateway JSON
Properties/launchSettings.json
resources/          referenced by the README image path
```

`Pmfbase` constructs a MySQL connection and a `Logger`, and sets `baseUrl` from Secrets Manager, on every handler instance. There is no connection pool wrapper and no DI container.

Handlers are static entry points named in `serverless.yml` (`Assembly::Namespace.Class::Method`). They are not an ASP.NET pipeline, even though `Amazon.Lambda.AspNetCoreServer` is a package reference.

## Runtime configuration

`environmentConfig.yml`:

| Stage | Service | Cognito pool | Stage value |
|---|---|---|---|
| development | `tmt-pmf` | `tmt-pmf-user-pool` | `development` |
| production | `tmt-pmf-prod` | `tmt-pmf-prod-user-pool` | `production` |

AWS profiles `pmf-dev` and `pmf-prod` are commented out in that file. `serverless.yml` also comments out `provider.profile`. `aws-lambda-tools.default.json` sets `"profile": "pmf-prod"` and `"region": "eu-west-1"` for the Visual Studio / `dotnet lambda` wizard.

Stage selection: `${opt:stage, self:provider.stage, 'development'}`. A deploy with no `--stage` uses the development block.

VPC attachment is `${file(./${stage}VpcConfig.yml)}`.

| File | Contents |
|---|---|
| `developmentVpcConfig.yml` | Security group `sg-0c5f904ebb1698f8f`. Subnets `subnet-09207d5c8869eed73`, `subnet-075eadc8e1b658e76` |
| `productionVpcConfig.yml` | Security group `sg-0ef1507ae1fcd0913`. Subnets `subnet-0c1aaf57b8d9fdb93`, `subnet-0827171db5e6ea7bb` |

API Gateway `minimumCompressionSize` is 1024. Functions are packaged individually, and `package.artifact` points every function at the same zip `bin/release/net6.0/pay-my-fines-lambda.zip`.

Default Lambda timeout is the platform default except where `serverless.yml` sets 30, 45, or 900 seconds. See `features.md`.

`Constants.REGION` is `eu-west-1`. `Constants.TimeDefaultValue` is 60 and is used as an HTTP client timeout in seconds.

## Secrets

`SecretsManagerService` calls `GetSecretValue` for `AWSCURRENT` in `eu-west-1` with the default credential chain.

| Secret id | Model | Fields the code reads |
|---|---|---|
| `pmf-rds` | `DbUserModel` | `host`, `username`, `password`, `port` (declared, not used in the connection string), `dbInstanceIdentifier` (used as the database name) |
| `pmf-ses-smtp` | `SesUserModel` | `SMTP_Username`, `SMTP_Password` |
| `pmf-enatis` | `EndPointModel` | `IFORCE_ENATIS_API_BASE_URL` |
| `pmf-invoices-bucket` | `BucketSecretModel` | `Invoice_Bucket` |
| `pmf-pdf-server` | `PdfSecretModel` | `IP` (used as an HTTP POST URL, not only an IP) |
| `pmf-mail` | `MailAddressModel` | `TMT_MAIL` |

Connection string shape: `server={host};userid={username};password={password};database={dbInstanceIdentifier};convert zero datetime=True`.

The reCAPTCHA secret is not in Secrets Manager. It is `Constants.reCAPTCHA_KEY`.

## Authentication

API Gateway HTTP events do not set an authorizer. CORS is enabled.

Cognito appears only as:

- A Custom Message trigger on an existing pool (`existing: true`).
- Columns `strCognitoUuid` on `user`.
- The `Amazon.Extensions.CognitoAuthentication` package reference, with no login call found in this tree.

`GenerateCustomEmail` documents the AWS trigger contract in a comment and handles three `triggerSource` values. The environment variable `USERPOOL` is set and not read.

## HTTP API

Standard JSON body from `HttpService.CreateResponse`:

```json
{ "result": true, "payload": {} }
```

`result` is false when the status is outside 200–299. Header `Content-Type: application/json`. Header `Access-Control-Allow-Origin: *`.

`POST /iveripayment` returns status 302 and a `location` header instead.

Routes are the `functions:` block in `serverless.yml`. The comment block at the top of that file is a partial list. It includes `deletepayment`, which is commented out of `functions`, and it omits `getpaginatedfines`, `generatereceipt`, and `contactUs`.

Development base URL written in that comment, not verified against AWS from this investigation:

`https://h8vzvoqq9c.execute-api.eu-west-1.amazonaws.com/development/`

## Upstream HTTP

Base URL: secret `pmf-enatis`, field `IFORCE_ENATIS_API_BASE_URL`.

`HttpClientHandler.ServerCertificateCustomValidationCallback` is `Helper.ServerCertificateCustomValidation`, which returns true for every certificate.

| Method | Path | Caller |
|---|---|---|
| GET | `IForce/queryopenfines?Attribute=&FineQueryAttributeValue=` | Quick search, outstanding, all-fines per id |
| POST | `IForce/bulkqueryopenfines` | `GetAllFines` |
| POST | `IForce/queryopaginatedfines` | Paginated iForce query. Body: `Ids`, `PageSize`, `PageStart`, `PageEnd`, `IssuingAuthorityCode` |
| GET | `IForce/querycharge?NoticeNumber=` | Charges, and an unused method on the iVeri insert class |
| GET | `IForce/queryevidence?EvidenceToken=&IncludeImage=` | Evidence |
| GET | `IForce/queryevidencestream?SessionToken=&EvidenceToken=` | Evidence stream |
| POST | `IForce/registerpayment` | Cron and `POST /registerpayment` |
| GET | `infringement/queryinfringement?InfringementNoticeNumber=` | Quick search, eNatis notice route |
| GET | `infringement/queryinfringer?IdDocumentType=&IdDocumentNumber=` | All-fines and eNatis person route |
| POST | `infringement/payinfringement` | Cron and `POST /payinfringement`. Body: `infringementNoticeNumber`, `amount` |

iForce register-payment JSON (`PayIForceInfringement`): `freshFine`, `issuingAuthorityCode`, `noticeNumber`, `paymentAmount`, `paymentDate` (round-trip `o` format), `paymentProvider` (literal 13), `terminalId` (literal 0), `receiptNumber`.

Client timeout is 60 seconds where `Constants.TimeDefaultValue` is applied. Some `HttpClient` instances do not set a timeout.

## Data

No DDL, migration, or ERD is in the repository. Names below appear in SQL strings.

| Table | Use |
|---|---|
| `user` | Profile. Columns named in SQL include `UserID`, `strCognitoUuid`, `strName`, `strSurname`, `strEmail`, `strIdType`, `strIdNumber`, `strMobileNumber`, `blnActive`, `strFirstUser`, `dtFirstEdit`. `strRole` is a comment on a column ordinal, not a name in an INSERT |
| `userIdNumber` | Extra identities. `refUserID`, `strName`, `strSurname`, `strIdType`, `strIDNumber`, `strFirstUser`, `strLastUser`, `dtLastEdit`, `dtFirstEdit` |
| `transaction` | iVeri payment. `TransactionID`, `striVeriNumber` / `strIVeriNumber` (both spellings appear), `refClientID`, `intPaid`, `dtPaid`, `dtPayment`, `refPaymentProviderID`, `strStatus`, `strAmount`, `strVeriResponse`, `dtFirstEdit`, `strRecieptNumber`, `strBankRef`, `dtIVeriSettlement`, `strMerchantReference`, `dtLastEdit` |
| `infringement` | One notice on a transaction. `InfringementID`, `refTrasactionID` (spelled that way), `strInfringementSource`, `strIdNumber`, `dtInfringementDate`, `refClientID`, `strInfringementNumber`, `intPaid`, `dtPaid`, `strIssuingAuthority`, `strVechicleRegistration`, `strStatus`, `strConvenienceAmount`, `strAmount`, `intDemeritPoints`, `intSettlementTries`, `strSourceResponse` |
| `paymentProvider` | Joined for `strProviderName` and `PaymentProviderID`. The settlement code does not use the name. It sends provider 13 |
| `setting` | `strKey`, `strValue`, `Active`. Keys written by the API: `CustomMessage_SignUp`, `CustomMessage_ResendCode`, `CustomMessage_ForgotPassword`, `ContactUs_CustomMessage`, `convenience` |
| `graph` | `GraphID`, `strGraphType`, `strGraphTitle`, `strGraphData`, `strDescription`, `strXColumn`, `strYColumn`, `blnActive` |
| `log` | `strCognitoUuid`, `txtQuery`, `txtResponse`, `strType` |

`intPaid` is the string `YES` or `NO`, not a boolean column, everywhere this code writes it.

Several readers use `SELECT *` and then `GetValue(ordinal)` or `GetName(ordinal)`. A column added in the database changes those responses. There is no schema snapshot to confirm ordinals.

## Payments

1. `POST /postiveritransaction` inserts `transaction` (`refPaymentProviderID = 1`) and child `infringement` rows inside nested loops. The `MySqlTransaction` begin/commit is commented out, so each statement auto-commits.
2. `POST /iveripayment` parses the iVeri Lite body after `__EVENTVALIDATION`.
3. Success updates both tables to `intPaid = YES` and calls `PaymentReceiptService.GenerateReceipt`.
4. Receipt HTML includes a convenience line of `setting.convenience` times the infringement count. HTML is posted to the PDF URL. Mail uses SMTP `email-smtp.eu-west-1.amazonaws.com` port 587 with SES credentials. From display name on the contact mailer is `PMF`.
5. `RunCronJob` selects unpaid-to-source rows and posts them upstream. See `business-rules.md`.

`PaymentChecker` is a read of `infringement.intPaid` by notice number. It returns false (do not allow another payment) when any row is `YES` or when the query throws.

## Mail

| Path | From | To | Transport |
|---|---|---|---|
| Contact us | `Constants.TMT_MAIL` (`help@paymyfines.co.za`), display name `PMF` | the same constant | SES SMTP |
| Failed settlement | same from constant | `sshabala@tmt.africa` | SES SMTP |
| Cognito custom message | Returned to Cognito. Cognito sends it | the user | Not SMTP from this function |
| Receipt | secret `pmf-mail` | the `user.strEmail` for `transaction.refClientID` | SES SMTP, after the PDF call |

Contact HTML is built in `Mailer.GetEmailTemplate` (`Services/MailService.cs`). It does not load `setting`. `ReplaceEmailPlaceHolders` in the same file can load a setting by key and is not called by `SendEmail`.

## Background work

`RunCronJob` is the only schedule.

- Event: `rate(2 hours)`.
- CloudFormation name: `RunCronJobDev`.
- Description string: "run the cronjob every 8 hours".
- Timeout: 900 seconds.
- Also bound to `GET /runcronjob`.
- IAM authorizer is commented out.

There is no SQS, SNS, or Step Functions resource in `serverless.yml`.

## Deployment

Build (`build.cmd` and `build.sh`):

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

Deploy, as the README example, which is **Documented** and not proven by a pipeline:

```text
serverless deploy --stage development --aws-profile pmf-dev
```

`bitbucket-pipelines.yml` runs only on branch `main`. The step is `sonarsource/sonarqube-scan:3.0.2` with `SONAR_HOST_URL` and `SONAR_TOKEN` from repository variables. Clone depth is full. It does not run `dotnet` or Serverless.

`sonar-project.properties` sets `sonar.projectKey=overdrive_paymyfines-backend-new_149f2ffa-fe3c-4990-b3ae-c049206da9e2`.

`renovate.json` extends `config:base`.

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

## Development and test

`Properties/launchSettings.json` and the README describe the AWS Lambda Test Tool (port 5050 in the README). The README's `launch.json` snippet is not a committed `.vscode` directory in the file list used for this pass. **Unknown** whether `.vscode` is gitignored.

`TestRequests/*.json` are sample events. No `.csproj` test project and no automated test run is configured in the pipeline.

`sonarqube_scan.bat` is in the tree. It was not executed for this document.

## Security notes visible in the tree

- No authorizer on HTTP routes, including `GET /runcronjob` and the routes that update settings, graphs, and payments.
- reCAPTCHA secret committed in `classes/Constants.cs`.
- TLS certificate checks disabled for upstream calls.
- SQL built by concatenation. `CleanInput` removes HTML tags, trims, strips a set of backslash escapes, and doubles single quotes.
- `RegisterUser` logs and returns `ex.Message` to the HTTP client on failure.
- VPC security group and subnet IDs are committed.
- `yarn.lock` is committed without a `package.json`.

## Files that are generated or IDE state

`AwsDotnetCsharp.csproj.user` is in the working tree listing from the onboarding issue. It is a Visual Studio user file. Whether it is gitignored was not re-checked line by line. Do not treat it as product behaviour.

`bin/` and `obj/` are build outputs. The deploy zip is produced there and is not source.

## Related repositories

Silicon Ark already has `paymyfines-frontend-new`, `paymyfines-frontend`, `paymyfines-pdf-application`, `paymyfines-worker`, and `Paymyfines-CronJob`. This repository does not reference those git URLs. It does call a PDF HTTP service and it does contain its own cron, so those other directories may overlap. **Unknown / Requires Confirmation.**
