# Business rules — Paymyfines-CronJob

Evidence date: 2026-10-07.

Source: `git@bitbucket.org:overdrive/paymyfines-cronjob.git`, `master` at `cbc8461` (2021-10-18).

No business-rules document exists in the source repository. Every rule below is taken from code. Where the code contradicts the names of its own variables, both facts are stated. Rules are not copied from `pmf-backend-ah`.

Provenance for all rows is **Implemented** unless the status column says otherwise.

## Settlement selection

| ID | Rule | Status |
|---|---|---|
| BR-001 | Pass 1 loads infringements with `strStatus = 'Source Failed Payment'`, joined to `transaction` through `infringement.refTrasactionID` and to `paymentProvider` through `transaction.refPaymentProviderID`. | Implemented |
| BR-002 | Pass 2 loads rows with `transaction.intPaid = 'YES'` and the same joins. It does not filter infringement status or `intSettlementTries`. | Implemented |
| BR-003 | Pass 1 runs before pass 2. Pass 2 is inside the same `try`. An exception in pass 1 or in the mail block skips pass 2. | Implemented |
| BR-004 | If pass 1 returns no rows, the job still runs pass 2. | Implemented |
| BR-005 | A row whose `strInfringementSource` upper-cases to `IFORCE` is placed on the loader's first list and stores `TransactionID`. Any other source, including blank, is placed on the second list and does not store `TransactionID`. | Implemented |
| BR-006 | The controller passes the eNaTIS list variable as the loader's first argument and the iForce list variable as the second. IFORCE rows are afterwards sent through `ProcessENatisPayments`. Other rows are sent through `ProcessIForcePayments`. | Implemented. Intent **Requires Confirmation** |

## What is posted

| ID | Rule | Status |
|---|---|---|
| BR-007 | The eNaTIS post sends only the notice number and the amount (`double` from `strAmount`) to `infringement/payinfringement`. | Implemented |
| BR-008 | The iForce post sends `freshFine = true`, the issuing authority, notice number, amount, the `dtPaid` string unchanged, the provider name, `terminalId = 0`, and `strRecieptNumber`, to `IForce/registerpayment`. | Implemented. Correct `terminalId` is **Unknown** |
| BR-009 | Both posts use base URL `http://192.168.0.41:8080/`. | Implemented |
| BR-010 | HTTP 200 is the only success status. Any other status is a failure. | Implemented |

## Status written back

| ID | Rule | Status |
|---|---|---|
| BR-011 | On HTTP 200, `transaction.intPaid` is set to `SETTLED`, `dtLastEdit` is set to now, and the infringement becomes `Source Approved Payment`. | Implemented. The transaction id used is `InfringementPayment.TransactionId`, which BR-005 leaves at 0 for non-IFORCE rows |
| BR-012 | On HTTP 200, eNaTIS stores `PayResult`, `InvoiceNumber`, and `ReceiptNumber` in `strSourceResponse`. iForce stores upstream `transactionId` and `resultDescription`. iForce `resultCode`, `amountPaid`, and `contemptAmountPaid` are not stored. | Implemented |
| BR-013 | The first failure of a pass 2 row sets `intSettlementTries` to 1 and infringement `strStatus` to `Source Failed Payment`, and stores `statusCode` and `message`. | Implemented |
| BR-014 | That first eNaTIS failure also sets `transaction.strStatus` to `Source Failed Payment` and `dtLastEdit` to now, before the infringement update. | Implemented |
| BR-015 | That first iForce failure sets `transaction.strStatus` to `Source Failed Payment` after the infringement update, and does not set `dtLastEdit`. | Implemented |
| BR-016 | A pass 1 eNaTIS failure adds one to `intSettlementTries` and rewrites `strSourceResponse`. It does not set infringement `strStatus` and does not update `transaction`. | Implemented |
| BR-017 | A pass 1 iForce failure adds one to `intSettlementTries`, sets both the infringement and the transaction `strStatus` to `Source Failed Payment`, and rewrites `strSourceResponse`. | Implemented |
| BR-018 | Pass 1 adds the row to the problem list only when the new try count is greater than 3. Pass 2 never adds to that list. | Implemented |
| BR-019 | This code never writes `transaction.intPaid = 'YES'` and never reads or writes `infringement.intPaid`. | Implemented |

`SETTLED` on success (BR-011) disagrees with the Silicon Ark record of `pmf-backend-ah`, where that update is described as commented out. Both statements can be true of their own repositories. Which value operations should use now is **Requires Confirmation**.

## Notification

| ID | Rule | Status |
|---|---|---|
| BR-020 | Mail is attempted only when the pass 1 problem list is non-empty. | Implemented |
| BR-021 | The message lists infringement id and notice number. Subject is `PMF Error Payments`. | Implemented |
| BR-022 | From and To are the Gmail address hardcoded in `MailService`. CC entries are `GetString(0)` from `user` rows whose `strRole` equals `["ADMIN"]`, including the bracket and quote characters the controller concatenates. | Implemented. Whether any user row matches that literal is **Unknown** |
| BR-023 | The admin `ExecuteReader` runs before `CommandText` is assigned. | Implemented. **Inferred** that this throws and that `SendEmail` is then not called. Not executed in this investigation |

## Access and failure

| ID | Rule | Status |
|---|---|---|
| BR-024 | `GET /CronJob` does not check a credential, a key, or a role. | Implemented |
| BR-025 | The caller receives HTTP 200 and the text `Cronjob completed` when the action throws. | Implemented |
| BR-026 | Failures are not written to the injected logger. Log lines in source are comments. | Implemented |
| BR-027 | SQL predicates and updates are string-built. Inputs are not parameterised. | Implemented |

## Rules that are not established

- How often the job must run. No schedule is in the repository.
- Who is allowed to call it. BR-024 is the absence of a check, not an approved access policy.
- Whether a non-IFORCE fine must be settled at iForce. The method names say no. BR-006 is what runs.
- A maximum number of notices per run. The loops have no cap.
- Idempotency beyond the status filters. A pass 2 row whose post succeeds leaves `intPaid = 'SETTLED'`, so the pass 2 filter (`YES`) will not select it again. A pass 2 row that fails becomes `Source Failed Payment` and is a pass 1 candidate later. A pass 2 success that updates `TransactionID = 0` (BR-011 with BR-005) does not change the real transaction's `intPaid`, so that row can be selected again. **Inferred** from the filters and the missing `TransactionId`. Not observed against a database.
- The business meaning of `freshFine`, of provider id versus provider name, and of receipt spelling `strRecieptNumber`.
