# PRD — legitimate_licences

Evidence date: 2026-10-10.

Source: `git@bitbucket.org:overdrive/legitimate_licences.git`, `main` at `707aa72` (2024-11-24, "first commit").

There is no prior PRD in Silicon Ark and no README in this revision. Requirements below are recovered from enums, controllers, views, Entity Framework models, the 2023 DbUp script, API routes, and configuration. Provenance is labelled.

Secrets found in source (SQL passwords, API key, Sentry DSN, PDF licence key) are not reproduced here.

## Product purpose

**Implemented.** Staff of a transport licensing authority use a web application named in configuration as **Neo Legitimate** to process operating-licence and accreditation work.

**Documented** in `DBUp/Scripts/2023/2205-EmailSenderUpdate-Legitimate.sql`: notifications name the Provincial Regulatory Entity and the National Public Transport Regulator. Print layouts also name the KZN Provincial Regulatory Entity and the National Public Transport Regulator.

**Inferred.** The system replaces or sits beside earlier licence systems. `SourceApplicationEnum` names NTLIS, LTPS, and Legitimate. Screens and securables refer to OLAS.

**Unknown / Requires Confirmation:** the formal product name used by the customer, and whether one deployment serves both PRE and NPTR or separate sites share this code.

## Users and roles

People sign in through forms authentication (`loginUrl` `~/Auth/Login`, timeout 30 minutes, sliding expiration). Anonymous users are denied except for `Auth`, `Content`, `Scripts`, `Images`, and `SignalR`.

`RoleEnum` in `Neo.LegitimateLicences.Common/Enums.cs` (implemented identifiers):

| Id | Role |
|---|---|
| 1 | Legitimate System Administrator |
| 2 | System User |
| 1000 | Application Administrator |
| 1001 | Data Capturer |
| 1002 | Payment Clerk |
| 1003 | Registery Clerk (spelling as in code) |
| 1004 | Verifier |
| 1005 | Exception Handler |
| 1006 | Gazetting Manager |
| 1007 | Agenda Officer |
| 1008 | Adjudication Secretary |
| 1009 | Chairman of the Adjudication Panel |
| 1010 | Inspection Officer |
| 1011 | Issuing Officer |
| 1012 | Licence Signatory |
| 1013 | Adjudication Panel member |
| 1014 | Adjudication Override |
| 1015 | Route Approver |
| 1016 | Data Purification |
| 1017 | Auditor |
| 1018 | Application Reports |
| 1019 | Operating Licence Reports |
| 1020 | View Only |
| 1021 | New Route Creator |
| 1022 | Inspection Capturer |
| 1023 | Referral Capturer |
| 1024 | Operating License Administrator |

Access to screens is checked with `SecurityHelper.UserHasAccessToSecurable` against `SecurableEnum`. Examples: start a new application (`ApplicationsModule_Home_StartNewApplication` = 1001), release TAT data (`InterDepartmentModule_ReleaseData` = 13001), business-rule bypass module (`BusinessRuleBypassRequestModule` = 12000). A failed check redirects to `Dashboard/Default` or throws "Security error" on some POST actions.

Other actors:

| Actor | Evidence | Status |
|---|---|---|
| Person who is not logged in | Forms auth deny, except the `Auth` location | **Implemented** |
| API caller | Must send headers `api_Key` and `api_User`. `api_User` is resolved with `SecurityDatabase.GetUser`. Mismatch returns HTTP 401 | **Implemented** |
| Desktop operator | Launches `Neo.LegitimateLicences.Desktop.exe` via protocol `legitimatedesktop` for `print` or `scan` | **Implemented** |
| Windows service | `ProjectService` starts `DatabaseServiceScheduler` | **Implemented** |
| Fingerprint user | `SignInUser` validates a fingerprint only when `FingerprintAuthenticationEnabled` is `true`. Committed value is `false` | **Implemented**, off in this config |
| System user | `Defaults.SystemUserID = 1`. SQL uses `System.Fn_GetSettingValue('SystemUserID')` | **Implemented** |

Self-registration was not found.

## Divisions and geography

`DivisionTypeEnum`: PRE = 1, NPTR = 2, TAT = 3.

The 2023 SQL script maps 1 to Provincial Regulatory Entity and 2 to National Public Transport Regulator. The `ELSE` branch keeps the text `TAT`. What TAT expands to is **Unknown / Requires Confirmation**.

`ProvinceEnum` lists the nine South African provinces plus National = 10.

A new gazette created with id `-1` sets `ProvinceID` to KwaZulu-Natal (`LegitimateDatabase.GetGazetteDetail`). Provenance: **Implemented**. Whether every office is KZN is **Unknown / Requires Confirmation**. `OfficeLocation` records exist, including a `BusinessRuleBypassApproverUserID`.

## Systems the product talks to

1. The operator's browser (MVC views under `Neo.LegitimateLicences.Web/Views`, SignalR).
2. SQL Server catalog `LegitimateNATIONAL`, through many EF contexts (applications, licences, workflow, audit, documents, comments, correspondence, reports, security, CRM, and others).
3. AFX HTTP services at `AfxServicesPath` (committed local value `http://localhost:8000/AfxServices`). `.svc` files in `Deploy` name workflow, query, notifications, integration, correspondence, document, comment, and audit services.
4. CDN at `AfxCdnPath` (committed local value `http://localhost:8001`).
5. The desktop app, via the API, for print data and scanned-document upload.
6. Sentry, when `SentryDsn` is non-empty. `Global.asax.cs` initialises the SDK and records web exceptions.
7. SMS and email rows written to `Crm.Messages` (`MessageTypeID` 2 for SMS, 1 for email in the 2023 script). The sender address is setting `SenderEmailAddress`. `WIndowServiceInstall` includes `Interop.Clickatell.dll`. No C# source call to Clickatell was found. eNatis is not named in this Git revision.
8. SignalR hub `MessageHub`, method `AddVerificationToList`, called by the system-trigger worker.
9. Earlier licence data through stored procedures such as `Verified.Pr_VerifiedLicenceToUseGet` and LTPS XML search on the API.

## Major workflows

### WF-1 — Sign in

`GET Auth/Login` redirects to `SignIn`. `POST Auth/SignInUser` accepts `userName`, `password`, and `fingerprint`.

1. If the profile is not anonymous, the JSON result sets `alreadyLoggedIn` true and `success` false.
2. If fingerprint mode is on and a fingerprint string is present, `DomainAuthenticationProvider.ValidateUser(fingerprint, false, ref userName)` runs.
3. Otherwise `ValidateUser(userName, password, false)` runs.
4. On success the session stores `DateLoggedIn`, the profile is loaded, and an audit row "User logged in" is written.
5. The JSON payload always returns `passwordExpired: false` in this method.

`SignOut` calls `DomainAuthenticationProvider.Signout` and redirects to `SignIn`.

Provenance: **Implemented**.

### WF-2 — Forgot password

`Auth/ForgotPassword` renders a view. `POST GetResetQuestion` returns the membership reset question. `POST ResetPassword` calls `DomainAuthenticationProvider.ResetPassword(userName, question, answer)`.

Membership is configured with `requiresQuestionAndAnswer="true"`, `enablePasswordReset="true"`, `enablePasswordRetrieval="false"`, `passwordFormat="Clear"`. Provenance: **Implemented**. Storing passwords in clear text is a security finding for human review. Password values are not copied here.

The `Auth` location allows anonymous users, so these actions are reachable without a forms ticket. Provenance: **Implemented**.

### WF-3 — Application lifecycle

`ApplicationStatusEnum`: None, Data Capture, Verification, Payment, Gazetting, Adjudication, Finalised, Cancelled.

`ApplicationSubStatusEnum` adds waiting states: correction, next gazette, gazette printing, gazette expiry, next adjudication (automated or manual), agenda finalisation, adjudication outcome.

`ApplicationRequestTypeEnum` (ids as coded; 12, 19, 20, and 21 are absent):

| Id | Type |
|---|---|
| 1 | New operating licence |
| 2 | Add authorisations / routes |
| 3 | Replacements COV |
| 4 | Increase carrying capacity |
| 5 | Conversion LTPS to Legitimate |
| 6 | Normal transfer |
| 7 | Deceased transfer |
| 8 | Renewal |
| 9 | New operating licence from source |
| 10 | Duplicate licence |
| 11 | Temporary special licence |
| 13 | Conversion OLAS to Legitimate |
| 14–18, 22–28 | Combined renewal, transfer, COV, and capacity variants |
| 29 | Personal details update |
| 30 | Change radius / permit route |
| 31 | Application for accreditation |
| 32 | Renewal of accreditation |
| 33 | Add operating licence to accreditation |
| 34 | Upliftment extension |
| 35 | New TAT request |
| 36 | Add new authorisations / routes |
| 37 | Amend authorisations / routes |

Workflow ids named in code: `ApplicationRequest` = 1, `NewRouteRequest` = 2, `NewAssociationRequest` = 3, `NewMemberRequest` = 4, `LicenceIssueRequest` = 5, `NewVehicleRequest` = 6, `MDApprovalRequest` = 13. Step types: Approval, Data Capture, Parallel Tasks, Automated, Document Verification.

Provenance: **Implemented** as enums and workflow ids. The step graph for each request type lives in the database and was not fully traced in C#.

### WF-4 — Payment and cash-up

Payment types: Cash, Debit/Credit card, EFT (`PaymentTypeEnum`).

Securables cover create receipt, flag as received, assign receipt, generate and print, reprint, cancel, set receipt to zero, add an application to a receipt, and cash-up including a cash-up report. Views include `Applications/Cashup.cshtml`. The desktop/ActiveX stack can print an application receipt and a payment advice.

Provenance: **Implemented** as permissions and screens. Fee amounts are **Unknown / Requires Confirmation** (not found as constants in the enums).

### WF-5 — Verification

Document verification statuses: Awaiting verification, Passed, Failed.

The Windows service dequeues a system trigger, loads verification task JSON, and invokes SignalR `AddVerificationToList`. The web verification console lists documents for a verifier.

Provenance: **Implemented**.

### WF-6 — Gazette

Gazette types: Internal, Publish.

Gazette detail statuses: Awaiting finalisation, Awaiting notice-period expiry, Finalised, Awaiting print date.

`GetGazetteDetail(-1)` sets print date to now and notice expiry to now plus `System` setting `GazettePeriodDays`. The current open gazette is the row still in Awaiting finalisation for the same gazette type and division.

Securables include create gazette, finalise and publish, print, and bypass gazette expiry.

Provenance: **Implemented**. The number of days is data, not code.

### WF-7 — Adjudication

Adjudication types: Hearing, Meeting.

Decisions: Approved, Postponed, Refused, Cancelled, Withdrawn, Struck off. Ids 3 and 7 are not in the enum.

Detail statuses: Awaiting agenda finalisation, Awaiting hearing decisions, Hearing finalised, Awaiting panel members, Postponed.

Securables include add hearing, add in-committee meeting, finalise agenda, print agenda, capture decisions, approve decisions, reverse decision, restart decisions, bypass adjudication, reschedule postponed applications, add/remove applications, download minutes, and print a decision letter.

Referrals: Tourism Authority and Planning Authority, with report definitions `LetterToTourismAuthority` and `LetterToPlanningAuthority`.

Provenance: **Implemented**.

### WF-8 — Issuing an operating licence or accreditation

Licence and accreditation statuses share the same ids: Awaiting issuing, Issued, Expiring within 60 days, Expired, Temporarily suspended, Archived, Awaiting new vehicle.

Issuing securables separate "awaiting supporting documents" from "awaiting verification and printing", and separate a test print from an actual print. Issue-without-print, reprint, amend routes, delink vehicle, and flag a VIN as correct are also securables.

Printout types: Receipt, Operating licence, Operating licence annexure, Accredited licence decal, Temporary replacement licence.

The desktop app fetches print data from `GET ~/1.0/Printing/GetPrintoutData` and draws it with the ActiveX printer classes.

Provenance: **Implemented**.

### WF-9 — Verified registers

Associations, members, routes, vehicles, and operating licences have consoles and list modules. Route statuses: Active, Inactive, Not verified, Cancelled. Route types: Standard, Radius, Area of operation, Temporary, Charter. Association and member verified statuses: Active, Inactive.

Vehicle types: Motor car, Minibus, Bus, Midibus, Other. Service types: Scheduled, Unscheduled, Charter, Tourist, Staff, Learners, Courtesy, Other, Temporary service.

`ApplicationLogic.VehicleServiceTypeCollectionGet` builds comma-separated service-type id lists per vehicle type from `GetServiceVehicleTypes()`. Provenance: **Implemented**.

Identity document types (`IdTypeEnum`): RSA identity document, passport, founding statement, temporary identity document, foreign identity document, certificate of incorporation, taxi association registration number.

### WF-10 — Purification and business-rule bypass

Purification securables cover correcting originating licence numbers, vehicles linked to the wrong licence, member contact details, vehicle and service type, routes, duplicate national route numbers, duplicate board route numbers, blocked licence lists, blocked ID numbers, LTPS licence lists, and licences that are both Legitimate and active in OLAS.

Business-rule bypass requests are a workflow entity (`EntityEnum.BusinessRuleBypassRequests` = 1032) with an in-progress list and an office-level approver user id.

Provenance: **Implemented** as screens and procedure names. The individual rules that may be bypassed are **Unknown / Requires Confirmation** beyond the type table `BusinessRuleBypassRequestType` (rows are data).

### WF-11 — Inter-department TAT release

`InterDepartment/ReleaseData` lists `GetTATApplicationsPendingDataRelease`. `POST ReleaseTATApplicationsPendingData` stores comments and releases one `applicationDetailID`. Both actions require securable `InterDepartmentModule_ReleaseData`.

Provenance: **Implemented**.

### WF-12 — Notifications

The 2023 script defines procedures that insert `Crm.Messages`. Observed rules:

- Upliftment reminder SMS uses setting `UpliftmentNotificationDays`, status awaiting upliftment (`LicenceStatusID = 1` in that procedure), skips numbers like `LTOKZN%`, and skips `Verified.LicenceExceptions`. The SMS body text says "more than 30 days" even though the threshold is the setting.
- Upcoming expiry SMS uses setting `RenewalGracePeriod`, skips numbers like `LT%`, skips licences that already have a non-cancelled renewal application (request type 8), and skips exceptions.
- Inspection notices in the same script are worded as coming from the National Public Transport Regulator.
- A summary email subject is `Legiti-mate status update`.

Provenance: **Implemented** in the script. Whether DbUp applies every script is covered in the technical documentation. Delivery of `Crm.Messages` to a phone or inbox is **Unknown / Requires Confirmation**.

## Integrations

| Integration | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| SQL Server `LegitimateNATIONAL` | App to database | Every screen and service | Applications, verified licences, workflow, CRM messages | SQL login in config | Not traced beyond EF/SQL exceptions |
| Desktop protocol | Browser to local exe | User prints or scans | Token in `legitimatedesktop:{print or scan}_{user}_{token}` | API key in the desktop config plus `api_User` | Message box on bad arguments; version check against `desktopAppVersion` from the API |
| API `~/1.0/*` | Desktop to API | Print or scan | XML or scan payload | Headers `api_Key`, `api_User` | HTTP 401 or an XML error document |
| SignalR `MessageHub` | Windows service to web | Dequeued system trigger | Verification task JSON | Hub URL from service setting `SignalRPath` | Catch writes "Error sending data" and still marks the trigger completed |
| Sentry | App to Sentry | Startup, request errors, DbUp result | Exception and a migration message | DSN in config | Init skipped when DSN is empty |
| AFX `.svc` services | Web to AFX | Workflow, documents, mail, audit | Service contracts in `Neo.Afx.Services` | **Unknown / Requires Confirmation** | **Unknown** |

`AuthorizationHeaderHandler` treats a path containing `/1.0/login` as API-key only. No controller action with route `~/1.0/login` was found. Provenance: **Implemented** handler branch with a **missing** route.

## Important constraints

- Forms authentication timeout is 30 minutes with sliding expiration.
- JSON deserialiser member limit is `1000000`.
- `customErrors` mode is `Off` and compilation `debug` is `true` in the committed `Web.config`.
- Passwords are configured as clear text in the membership provider.
- Several securable and entity ids are duplicated in `Enums.cs` (see business rules). Callers that share an id cannot be distinguished by that number alone.
- New gazette province is hard-coded to KwaZulu-Natal when the detail id is `-1`.
- Some print layouts hard-code KZN PRE or NPTR wording.
- This revision does not contain an e-hailing provider module. A local non-Git folder on the inspection machine does. That folder is not this commit.

## Requirements discovered from the implementation

| Id | Requirement | Provenance |
|---|---|---|
| FR-001 | A signed-in user can search (`Home/Search`) and open an advanced search | Implemented |
| FR-002 | A user with the matching securable can start and process an application through capture, verification, payment, gazette, and adjudication | Implemented |
| FR-003 | Receipts can be created, assigned, printed, reprinted, cancelled, zeroed, and included in a cash-up | Implemented as securables and controllers |
| FR-004 | Gazettes move through finalisation, print, and notice expiry, with an optional expiry bypass securable | Implemented |
| FR-005 | Adjudication hearings and meetings capture the decisions listed in `AdjudicationDecisionEnum` | Implemented |
| FR-006 | Approved work can be issued as an operating licence, annexure, accreditation certificate, decal, or temporary replacement, including a test print distinct from an actual print | Implemented |
| FR-007 | Associations, members, routes, and vehicles can be registered, verified, and corrected | Implemented |
| FR-008 | Purification users can correct known data defects and manage block lists | Implemented |
| FR-009 | A business-rule bypass can be requested and listed while in progress, with an office approver user | Implemented. Rule catalogue is data |
| FR-010 | TAT applications waiting for data release can be released with a comment by a user who has the inter-department securable | Implemented |
| FR-011 | The desktop helper prints or scans only when launched with three underscore-separated arguments and a recognised verb | Implemented |
| FR-012 | Background SMS/email rows are created for upliftment and upcoming expiry according to system settings | Implemented in SQL script |
| FR-013 | Startup runs DbUp against `ConnectionString` and reports success or failure to Sentry | Implemented |

## Assumptions (not confirmed)

- One web deployment serves the roles above for both PRE and NPTR. The code supports both division ids. Deployment topology is unknown.
- `Crm.Messages` are sent by the Windows service scheduler. The service starts a generic AFX scheduler. The job list is in database service tables, which are not fully scripted in this repo.
- The licence status name "Expiring within 60 days" matches a 60-day window. The expiry SMS uses `RenewalGracePeriod`, which may differ.
