# PRD — legitimate

Evidence date: 2026-10-10. Source: `git@bitbucket.org:overdrive/legitimate.git`, `main` at `0a9b1c8` (2026-02-11).

There is no prior PRD in the repository or in Silicon Ark. Requirements below were recovered from code, SQL scripts, and config. Provenance is marked on each item. Secrets in config are not copied.

## Product purpose

Staff operate Neo Legitimate from a browser. They capture public-transport operating-licence applications, take receipts, verify parties and routes, publish gazettes, run adjudication hearings, and issue licences. The same system holds associations, members, vehicles, and routes, and it purifies records that came from earlier systems named NTLIS, LTPS, and OLAS.

Provenance: **Inferred** from `Neo.Legitimate.Common/Enums.cs`, the module controllers, and KZN commit titles. A written product statement was not found.

The checked-in web application name is "Neo Legitimate". Commit subjects say LGKZN / KZN. Whether the legal operator is the KwaZulu-Natal Provincial Regulatory Entity is **Unknown / Requires Confirmation**.

## Users and roles

People sign in with forms authentication. Anonymous users are denied (`<deny users="?" />`). The login URL is `~/Auth/Login`. The forms timeout is 2880 minutes with sliding expiration.

Provenance: **Implemented** in `Neo.Legitimate.Web/Web.config`.

Roles are integer ids on `Role` in `Enums.cs`. Whether every id still has a database row is **Unknown**.

| Role id | Name in code | Provenance |
|---|---|---|
| 1 | LegitimateSystemAdministrator | Implemented |
| 2 | SystemUser | Implemented |
| 1000 | ApplicationAdministrator | Implemented |
| 1001 | DataCapturer | Implemented |
| 1002 | PaymentClerk | Implemented |
| 1003 | RegisteryClerk | Implemented (spelled this way in code) |
| 1004 | Verifier | Implemented |
| 1005 | ExceptionHandler | Implemented |
| 1006 | GazettingManager | Implemented |
| 1007 | AgendaOfficer | Implemented |
| 1008 | AdjudicationSecretary | Implemented |
| 1009 | ChairmanOfTheAdjudicationPanel | Implemented |
| 1010 | InspectionOfficer | Implemented |
| 1011 | IssuingOfficer | Implemented |
| 1012 | LicenceSignatory | Implemented |
| 1013 | AdjudicationPanelmember | Implemented |
| 1014 | AdjudicationOverride | Implemented |
| 1015 | RouteApprover | Implemented |
| 1016 | DataPurification | Implemented |
| 1017 | Auditor | Implemented |
| 1018 | ApplicationReportsRole | Implemented |
| 1019 | OperatingLicenceReportsRole | Implemented |
| 1020 | ViewOnly | Implemented |
| 1021 | NewRouteCreator | Implemented |

Access to a screen is a securable, not only a role. `SecurityHelper.UserHasAccessToSecurable` allows the action when the user's securable has `CanRead` or `CanEdit`. The securable catalogue is the `Securable` enum. Modules start at 1000 (applications), 2000 (operating licences), 3000 (associations), 4000 (routes), 5000 (members), 6000 (vehicles), 7000 (admin), 8000 (cash-up).

External ASMX callers are not staff users. They pass an interface key. See integrations.

A fingerprint sign-in path exists and is switched off in the checked-in web config (`FingerprintAuthenticationEnabled` = `false`).

## Main workflows

### W1 — Staff sign-in

Trigger: browser opens a page while anonymous. Forms auth redirects to `Auth/Login`, which redirects to `SignIn`.

Inputs: user name and password. If fingerprint auth is enabled, a fingerprint string instead.

Behaviour (`AuthController.SignInUser`):

1. If the profile is already not anonymous, the JSON result sets `alreadyLoggedIn` true and `success` false.
2. Otherwise `DomainAuthenticationProvider.ValidateUser` runs. Password retrieval is disabled. Password reset is enabled. A question and answer are required. Email must be unique. `passwordFormat` is `Clear`.
3. On success, `Session["DateLoggedIn"]` is set and an audit row is written: application "User logged in".
4. Return URL is `BaseUrl` plus the `LastPage` cookie when that cookie exists, otherwise `BaseUrl`.

Forgot password asks `GetResetQuestion` then `ResetPassword` with the user name, question, and answer.

Output: JSON `{ success, returnUrl, passwordExpired: false, alreadyLoggedIn }`. `passwordExpired` is hard-coded false.

Provenance: **Implemented**. The membership provider type is `Pilotfish.Afx.Security.DomainMembershipProvider`. How the password is stored is inside Afx, which is not in this repository. The config says the format is Clear.

### W2 — Home and tasks

Trigger: `Home/Default` after sign-in. The default MVC route is `Applications/Default`, not Home.

The home model loads active workflow templates, the user's in-progress tasks (status 1), and workflow instances in statuses 1, 5, and 6 (in progress, not started, error). Lookups come from the Afx integration store: `ActiveWorkflows`, `WorkflowInstanceStatus`, `WorkflowTemplateSteps`.

Provenance: **Implemented** in `HomeController`.

### W3 — Start and capture an application

Trigger: applications module. Securable `ApplicationsModule_Home_StartNewApplication` (1001).

Application request types in `ApplicationRequestType` (ids 1–28) include new operating licence, add routes, COV replacement, carrying capacity, LTPS conversion, OLAS conversion, normal and deceased transfer, renewal, late renewal, duplicate, temporary special licence, data purification, and combinations of those.

`ApplicationRequestHelper` classifies transfer, COV, and renewal combinations. The file comment says the same classification must be kept in `ApplicationRequest/Views/ApplicationRequest/Scripts/UI.cshtml`.

Capture is a workflow form (`Workflows.ApplicationRequest` = 1) with sections such as applicant and mode. Pre-check validation in `Applications/Scripts/Default-Events.cshtml` includes:

- OL/Permit number required when the type is not 1 (new operating licence).
- ID number required.
- Service type required for a new operating licence or when the source application is LTPS (2).
- Association required when service type is unscheduled (2) and mode is midibus (2) or minibus (3).
- Number of buses required for a new bus application (mode 1, type 1).
- The registration-number requirement for unscheduled midibus/minibus is commented out.

Applicant section (`ApplicantDetails.cshtml`):

- Email, when present, must be a valid email.
- Either a SARS income-tax number or a reason (max 1000 characters) is required.
- Unscheduled service requires a verified association.
- If no verified association is selected, region is required.
- On a transfer type, the applicant and the existing licence holder must be different people.

Provenance: **Implemented**. Fees are not in C#. See business rules for `Fn_GetReceiptCost`.

### W4 — Receipt and cash-up

Trigger: securables under `ApplicationsModule_Receipts` (1003) and `CashupModule` (8000).

Staff can create a receipt, flag it received, assign it, generate and print it, reprint it, cancel it, set it to zero, add an application to a receipt, and run a cash-up report. Those are separate securables (1051–1055, 1073, 1074, 1083, 1084).

`App.Fn_GetReceiptCost` multiplies a type cost, plus extra renewal and conversion costs in some cases, by vehicle count. Temporary replacement (type 27) adds neither extra. Type 31 is named in SQL as "Renewal (Pre 02-08-2024 cost adjustment)" and is excluded from those extras. Type 31 is not in the C# enum.

A COV whose vehicle registration changes to a number ending in `ZN`, with the same chassis, has the COV type-3 cost subtracted. The SQL comment cites SUP0007250: not charging for a KZN number change to ZN.

Provenance: **Implemented** in SQL and securables. The rand amounts live in `App.ApplicationRequestTypes` and were not in the scripts that were read, except the table shape (`Cost money`).

### W5 — Verification queue

Trigger: a system trigger of type `SendNewVerificationTaskInQueue` (1).

`SystemTriggerHandler` dequeues a trigger, loads the verification task, and invokes SignalR `MessageHub.AddVerificationToList`. The hub also supports `SetVerificationAsAssigned` and `RemoveVerificationFromList`. The hub is mapped at `/signalr`.

Verifier screens are `VerificationConsole` (default, pre-check, verification, audit). Bulk vehicle verification is a separate console.

Provenance: **Implemented**.

### W6 — Gazette

Gazette detail statuses: AwaitingGazetteFinalisation (1), AwaitingNoticePeriodExpiry (2), Finalised (3), AwaitingGazettePrintDate (4).

Staff with the matching securable can create a gazette, finalise and publish, print, and bypass gazette expiry.

The length of the notice period is not hard-coded in the C# that was read. It is **Unknown / Requires Confirmation** (likely data or Afx workflow).

Provenance: **Implemented** for statuses and permissions. Notice length **Unknown**.

### W7 — Adjudication

Types: Hearing (1), Meeting (2).

Detail statuses: AwaitingAgendaFinalisation (1), AwaitingAdjudicationHearingDecisions (2), AdjudicationHearingFinalised (3), AwaitingHearingQuorum (4).

While status is 4, the panel page says at least two users are required for a hearing and at least two for a meeting. Start is enabled when `PanelMembers.length >= 2`. Commit `b1c576e` records the change from three panel members to two.

Remove Application is visible only when status is 1 or 4 (`AdjudicationConsole/Applications.cshtml`). Add application is visible only in status 1 and asks for a reference number of length 10. Finalise agenda is offered only in status 1 and only with securable 1013. Print agenda is status 2 or 4. Download minutes is status 3.

Meeting date, time, and location can be edited only in status 1. Postpone is offered in status 2 or 4.

Provenance: **Implemented**.

### W8 — Issue a licence

`LicenceIssueRequest` is workflow 5. Licence statuses: AwaitingIssuing (1), Issued (2), ExpiringWithin60Days (3), Expired (4), TemporarilySuspended (5), Archived (6).

For a temporary special licence (type 11), the issue controller rejects a duration over 14 days, and rejects an expiry before the issue date. The comment says the same rule must be kept on the application save.

Issuing securables separate test print, actual print, issue without print, reprint, amend routes, and issuing expiries.

Provenance: **Implemented**.

### W9 — Related requests

| Workflow id | Enum | What it is |
|---|---|---|
| 1 | ApplicationRequest | Operating-licence application |
| 2 | NewRouteRequest | New route, or verification required |
| 3 | NewAssociationRequest | New association, or verification required |
| 4 | NewMemberRequest | New member, or verification required |
| 5 | LicenceIssueRequest | Issue |
| 6 | NewVehicleRequest | New vehicle, or verification required |

Licence upliftment extension has a controller and entity id 1019. It is not in the `Workflows` enum. Whether it runs as workflow 5 or its own template is **Unknown / Requires Confirmation**.

Route statuses: Active, InActive, NotVerified, Cancelled. Route types: Standard, Radius, Area of operation, Temporary. Route changes: existing on licence, add new, add existing, remove.

### W10 — Purification

A data-purification role and a large set of application, route, and operating-licence screens correct originating licence numbers, member identity, service type, routes, blocked licences, blocked ID numbers, LTPS permits, bypassed licences, allowed new licences, and OLAS links.

Provenance: **Implemented** as controller actions and securables. The business approval for each correction screen is **Unknown / Requires Confirmation**.

## Integrations

### I1 — Legitimate ASMX

`LegitimateInterface.asmx` in `Neo.Legitimate.LegitimateWebService`.

| Method | Inputs | Output | Auth |
|---|---|---|---|
| `IsAlive` | interface key | true when the key matches | Shared key via `IsValid` |
| `GetApplications` | key, operating-licence number, id number, registration, chassis | XML from stored procedure `App.Pr_ApplicationPrecheckInterfaceSearch` | Same |
| `GetAssociationMasterData` | key | XML from app setting `GetAssociationsMasterdataStoredProcedure` | Same |
| `GetOLASApplications` | key, operating-licence number | XML from `dbo.Pr_ImportedOLASLicenceSearch` on the OLAS connection | Same |

Invalid key returns XML error text "Invalid interface key". Exceptions return `GetXmlError` with the exception message.

The checked-in service `Web.config` contains `LegitimateConnection`, `LegitimateOLASConnection`, `LegitimateInterfaceKey`, and `GetApplicationsStoredProcedure`. It does not contain `GetAssociationsMasterdataStoredProcedure`. Calling `GetAssociationMasterData` against that file would fail when it reads the missing setting. Whether production config adds the key is **Unknown / Requires Confirmation**.

Catalogs in that file: `LegitimateKZN`, `Legitimate_OLAS`. The web app file instead names `LegitimateKZN_QA` and `Legitimate_LTPS`.

### I2 — NLTIS and LTPS ASMX

`NLTISInterface` and `LTPSInterface` each expose `IsAlive` and `GetApplications` (key plus licence/permit, id, registration, chassis). They read the procedure name from `GetApplicationsStoredProcedure`. NLTIS code uses OLAS-named types (`OlasApplication`, `OlasMember`, `OlasAssociation`, `OlasOperatingLicence`).

Provenance: **Implemented**. Which external system calls these endpoints is **Unknown**.

### I3 — Afx services on the web app

`Neo.Legitimate.Web/AfxServices` hosts Workflow, Integration, Query, Form, Document, Correspondence, Audit, and Comment `.svc` endpoints. `Deploy/` repeats a similar set and adds Notifications and Validation.

Provenance: **Implemented** as endpoints. Their contracts are in Pilotfish Afx, not in this repository.

### I4 — Email

Commit `0a9b1c8` forces TLS 1.2 in the Windows service before the scheduler starts. The comment says the Messages worker uses SMTP and gives SendGrid as an example. The worker class is not in this repository. SMTP host, from-address, and failure behaviour are **Unknown / Requires Confirmation**.

### I5 — Sentry

`Application_Start` initialises Sentry when `SentryDsn` is non-empty, including Entity Framework breadcrumbs. The checked-in web config has a DSN. The value is omitted here.

### I6 — Document scanner

`AffectedApplicationDocumentScanner` is a WinForms program. It starts at `LoginForm` and has a generated client for `AffectedApplicationService`. Purpose **Inferred**: scan documents against an application number. The service host URL is **Unknown** from the files that were readable.

## Constraints

- .NET Framework 4.6.2, MVC 4, SQL Server, Windows/IIS. No container or cloud manifest was found.
- Staff UI is server-rendered Razor plus Knockout (`data-bind` on the consoles).
- `aspnet:MaxJsonDeserializerMembers` is 1000000. `httpRuntime maxRequestLength` is 2097151 KB.
- Compilation `debug` is true in the checked-in web and service configs. WCF `includeExceptionDetailInFaults` is true on the web app.
- Views exist under both `Neo.Legitimate.Web/Views` and the module projects. The web project includes its own copies. They can drift.

## Requirements discovered from the implementation

### FR-001 — Authenticate staff

The system requires a signed-in user for MVC pages. Sign-in uses the Afx domain membership provider. Reset uses a security question.

Evidence: `Web.config` authentication and authorization; `AuthController`.

Implementation: Implemented. Confidence: High.

### FR-002 — Authorise by securable

A console action checks `SecurityHelper` against the `Securable` enum. Read or edit on that securable is enough.

Evidence: `SecurityHelper.cs`, `Enums.cs`, `AdjudicationConsole/Console.cshtml`.

Implementation: Implemented. Confidence: High. Duplicate ids are a defect risk. See business rules.

### FR-003 — Capture an application on a workflow

An application is workflow template 1 and entity `ApplicationRequestDetails` (1000). Types 1–28 are named in C#.

Evidence: `Enums.cs`, `ApplicationRequest` project.

Implementation: Implemented. Confidence: High for the enum. Confidence low for types 30 and 31, which appear only in SQL.

### FR-004 — Price a receipt

Receipt cost is `Fn_GetReceiptCost`, including the ZN registration COV waiver.

Evidence: `_database schema changes/20250208.01 Fn_GetReceiptCost.sql`.

Implementation: Implemented in the script. Whether the function is deployed on every catalog is **Unknown**.

### FR-005 — Queue verification live

New verification work is pushed to every connected `MessageHub` client.

Evidence: `MessageHub.cs`, `SystemTriggerHandler.cs`, `Startup.cs`.

Implementation: Implemented. Confidence: High.

### FR-006 — Adjudicate with two panel members

A hearing or meeting cannot start until two panel members are present. Remove Application is hidden after the agenda leaves statuses 1 and 4.

Evidence: `Panel.cshtml`, `Applications.cshtml`, commits `b1c576e` and `7c6f17e`.

Implementation: Implemented. Confidence: High.

### FR-007 — Limit a special licence to 14 days

Temporary special licence issue and expiry must be at most 14 days, and expiry must not be before issue.

Evidence: `LicenceIssueRequestController.cs`.

Implementation: Implemented on the issue save that was read. The comment says the application save must match. That second copy was not re-read. Confidence: Medium.

### FR-008 — Treat e-hailing as its own mode and service type

Mode 6 is E-Hailing. Service type 9 is E-Hailing and, in `Fn_AllowedServiceTypesForMode`, is the only type allowed for mode 6. Document types 6000–6005 describe platform agreement, NLTA labour compliance, SAPS criminal record, pending-investigation declaration, NTCMS traffic-offence report, and a SARS tax-compliance pin.

Evidence: `Enums.cs`, `DbScripts/01`, `06`, `07`.

Implementation: Implemented in enum and SQL scripts. The application UI still asks "Is Uber application?" when mode id is 4. Confidence: High for the scripts. The Uber control is a **Requires Confirmation** conflict.

### FR-009 — Search from other systems

ASMX methods return application XML for a valid interface key.

Evidence: `LegitimateInterface.asmx.cs`, `NLTISInterface.asmx.cs`, `LTPSInterface.asmx.cs`.

Implementation: Implemented. Confidence: High.

### FR-010 — Report

Report groups: Application, OL, Association, Member, Route, General, Vehicle. `ReportsController` and `Neo.Legitimate.ServerReports` (SSRS `rptproj`) exist.

Evidence: `Enums.cs` `ReportGroups`, server reports project.

Implementation: Implemented as a project. The report catalogue inside the `rptproj` was not listed one by one. Confidence: Medium.

## Non-goals of this document

This PRD does not approve fees, notice periods, or workflow steps that live only in the database. Those remain **Unknown / Requires Confirmation** until a person compares them with the regulatory rules.
