# PRD — nltis

Evidence date: 2026-10-08. Revision: `main` `a9ea681` (2024-11-24).

No product requirements document exists in the source repository. The requirements below are reconstructed from implementation. Each one is labelled.

Provenance labels: **Documented** (stated in a comment or config that describes intent), **Implemented** (present in code), **Inferred** (reasonable reading, not stated), **Unknown**.

The git slug is `nltis`. The string `nltis` does not appear in the tree. Assembly product is Neo Application Framework (Neo Africa, copyright 2020). See `project-plan.md` for the identity discrepancy. These requirements describe the framework in this repository, not a confirmed NLTIS product.

SQL password values are omitted.

## Product purpose

**Implemented:** a .NET Framework application framework for workflow-centred case work: user administration, workflow tasks, documents, correspondence, queries, notifications, audits, reports, and SQL-side validation and formatting.

**Inferred:** a host application (not in this repository) supplies the case screens. This repository supplies services, a user console, background workers, SQL CLR, and device controls.

**Unknown:** the business the EPWP catalog and the `nltis` slug were meant to serve.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Signed-in console user | `UserConsoleController` is marked `[Authorize]`. Pages take a `UserID` and read `Profile.UserID` as the actor | Implemented. The console `Web.config` has no `<authentication>` element, so the host site must supply auth. Unknown whether that host exists |
| Workflow participant | `WorkflowService` uses `HttpContext.Current.User.Identity.Name` when authenticated, else an empty user name | Implemented |
| Service caller with no identity | Same property returns `""` when `HttpContext` has no authenticated user | Implemented |
| SQL caller | CLR functions `Integration.*`, `System.*`, `Crm.Fn_GetFormattedMessages`, `HashPassword`, `CreatePassword`, `CreatePasswordSalt` | Implemented as scripts and C# SQL functions |
| Desktop or browser host of ActiveX controls | Fingerprint, scanner, and barcode printer controls | Implemented as WinForms user controls |
| Background worker identity | `WorkflowTaskProcessor` passes `UserID` and `UserEmail` from worker settings into handlers | Implemented. The Windows Service exe that hosts the worker is not in this repo |

No role names are hard-coded. Roles are loaded with `SecurityDatabase.GetRoles()` and stored as a comma-separated list of role IDs.

## Major workflows

### WF-1 — Maintain a user

1. Caller opens `UserConsole/Default`, `Settings`, `Roles`, `Security`, or `Buddies` with a `UserID`.
2. The controller loads the user through `SecurityDatabase` using `Profile.UserID` as the reader.
3. Posts go to `Save`, `ActivateUser`, `DeActivateUser`, `SaveSettings`, `SaveUserRoles`, `SaveFingerprint`, `SaveBuddy`, or `DeleteBuddy`.
4. Success returns JSON `{ ErrorMessage: "" }`. A thrown exception returns JSON whose `ErrorMessage` is the innermost exception message.

**Implemented** status values in the controller: activate writes status `1` (comment `Active`); deactivate writes status `2` (comment `Inactive`).

### WF-2 — Run a workflow

1. `WorkflowService.StartWorkflowInstance(workflowID, entityID, itemID, participants, referenceNumber)` starts an instance.
2. `GetOutstandingUserTasks` lists the current user's outstanding tasks.
3. `SubmitWorkflowTaskAction` records a status, an optional correction step, and comments.
4. `ReAssignWorkflowInstanceTask`, `EscalateWorkflowInstanceTask` (`returnEscalation` sends it back), and `CancelWorkflow` change assignment or end the instance.
5. `WorkflowTaskProcessor.Run` loads handlers from the database and executes tasks whose template step has a `WorkflowTaskHandlerID`.
6. A handler exception sets status `Error` (9) and calls `RaiseException`. Status `0` or below is not written. A failed status update calls `UpdateTaskStatusFailover` and stops that task.

Step types in `Neo.Afx.WinService.Workflows`: Approval = 1, DataCapture = 2, ParallelTasks = 3, Automated = 4.

### WF-3 — Documents

1. `DocumentService.UploadFile` accepts a file.
2. `CreateScanSession` records a scan session (entity, item, field key, document type, store location, user).
3. `GetDocument` / `GetDocuments` return document metadata as JSON strings.
4. `DocumentViewer.ashx` and `DocumentMerger.ashx` (deploy copy) render or merge documents.
5. The only document store type in code is `DocumentStoreTypeEnum.SQL = 2`.

### WF-4 — Correspondence

1. `CorrespondenceService.Create` stores a message (email or SMS) against an entity and item.
2. `Neo.Afx.WinService.Messages` sends email through SMTP settings from the database (`EmailSender`) and SMS through Clickatell (`SmsVodazone`) or Swift (`SmsSwift`).
3. `EmailViewer.ashx` displays a message.

### WF-5 — Queries, comments, notifications, audits

- Queries: create, update, update status, delete, get one, get by entity/item, filtered get, list statuses, sources, and types.
- Comments: create, update, delete, get one, get by entity/item.
- Notifications: create, mark as read, get for a user (optional unread only).
- Audits: `GetAudits(entityID, itemID, sourceID, actionID, order)` is read-only in the web service.

## Functional requirements

### FR-001 — User console pages

**Requirement:** A signed-in user can open another user's details, settings, roles, security, and buddies.

**Trigger:** GET `Default`, `Settings`, `Roles`, `Security`, `Buddies` with `UserID`.

**Inputs:** `UserID`. Actor id from `Profile.UserID`.

**Behaviour:** Loads the user when `UserID > 0`. Sets `ViewBag.Controller` and `ViewBag.Page` to the page name. Roles page joins all roles with the user's roles into `IsSelected`. Buddies page loads buddies, active users, and workflows.

**Output:** Razor view.

**Error:** Not specified in the controller. Database exceptions propagate.

**Evidence:** `Web/Afx.UserConsole/Controllers/UserConsoleController.cs`, views under `Views/UserConsole/`.

**Implementation:** Implemented.

**Confidence:** High for the pages that exist. Low for who is allowed to open them: `[Authorize]` does not check a permission for the target `UserID`.

### FR-002 — Save user profile

**Requirement:** The console saves known-as, last name, first name, and email for a user.

**Trigger:** POST `Save`.

**Inputs:** `User` (`UserID`, `KnownAs`, `LastName`, `FirstName`, `Email`).

**Behaviour:** Calls `SecurityDatabase.SaveUser`. The call passes `Email` twice and then `Profile.UserID`.

**Output:** JSON `{ ErrorMessage }`.

**Validation:** None in the controller.

**Error:** Innermost exception message in `ErrorMessage`.

**Evidence:** `UserConsoleController.Save`; `SecurityDatabase.SaveUser` in `Neo.Afx.Services.Admin.Data/Security/SecurityDatabase.cs`.

**Implementation:** The controller call and the current `SaveUser` signature do not match (7 arguments versus 10). Stored procedure `Pr_SaveUser` is not in this repository.

**Confidence:** High that the call is present. High that it does not match the data method. Unknown whether an older overload existed.

### FR-003 — Activate and deactivate a user

**Requirement:** The console sets a user active or inactive.

**Trigger:** POST `ActivateUser` or `DeActivateUser`.

**Inputs:** `User.UserID`. Actor is `Profile.UserID`. The status comment argument is an empty string.

**Behaviour:** `UpdateUserStatus(..., 1)` for active, `UpdateUserStatus(..., 2)` for inactive.

**Output:** JSON `{ ErrorMessage }`.

**Validation:** None in the controller. No check that the actor may change this user.

**Evidence:** `UserConsoleController` lines that call `UpdateUserStatus`.

**Implementation:** Implemented in the controller. Procedure body is not in the repo.

**Confidence:** High for the status codes the controller sends.

### FR-004 — Assign roles

**Requirement:** The console replaces a user's roles with the selected set.

**Trigger:** POST `SaveUserRoles`.

**Inputs:** List of `RoleAndUserCombined` and `userID`.

**Behaviour:** Selected role IDs are concatenated into a comma-separated string and passed to `SaveUserRoles`. An empty selection sends an empty string.

**Output:** JSON `{ ErrorMessage }`.

**Evidence:** `UserConsoleController.SaveUserRoles`.

**Implementation:** Implemented.

**Confidence:** High.

### FR-005 — Store a fingerprint

**Requirement:** The console stores a fingerprint template for a user.

**Trigger:** POST `SaveFingerprint`.

**Inputs:** Hex string `fingerPrint`, `userID`.

**Behaviour:** Converts the hex string to bytes and calls `SaveUserFingerPrint`.

**Output:** JSON `{ ErrorMessage }`.

**Validation:** Invalid hex becomes an error message. No quality check in the controller.

**Evidence:** `UserConsoleController.SaveFingerprint`. Capture control: `Neo.ActiveX.FingerprintReader/FingerprintReaderControl.cs`.

**Implementation:** Implemented.

**Confidence:** High for storage. Unknown which reader hardware is required beyond the vendored DigitalPersona assemblies.

### FR-006 — Workflow buddies

**Requirement:** A user can add or remove a buddy for a workflow.

**Trigger:** POST `SaveBuddy` or `DeleteBuddy`.

**Inputs:** `workflowID`, `buddyUserID`, `userID`. Actor is `Profile.UserID`.

**Behaviour:** `SaveUserWorkflowBuddy` or `DeleteUserWorkflowBuddy`.

**Output:** JSON `{ ErrorMessage }`.

**Evidence:** `UserConsoleController` buddies actions. View `Views/UserConsole/Buddies.cshtml`.

**Implementation:** Implemented. What a buddy is allowed to do is not defined in this repository. **Inferred:** a delegate for workflow tasks. **Unknown** until the SQL procedure is read.

**Confidence:** Medium.

### FR-007 — Workflow service

**Requirement:** A caller can start, update, query, action, reassign, escalate, and cancel workflow instances, and ask whether a task can be assigned to the current user.

**Trigger:** WCF operations on `WorkflowService`, namespace `neo:services:data`, hosted with `WebServiceHostFactory`.

**Operations:** `StartWorkflowInstance`, `SaveWorkflowInstance`, `GetWorkflowTemplates`, `GetWorkflowTemplate`, `GetOutstandingUserTasks`, `SubmitWorkflowTaskAction`, `SubmitWorkflowInstanceErrorAction`, `ReAssignWorkflowInstanceTask`, `EscalateWorkflowInstanceTask`, `CancelWorkflow`, `WorkflowInstanceTaskCanAssignToMe`.

**Output:** JSON strings from the data layer (`ToJson()`). Several action methods return `""` when the data result length is 0.

**Auth:** User name from the ASP.NET identity when authenticated.

**Evidence:** `Neo.Afx.Services.Web/Services/WorkflowService.svc` and `.svc.cs`. A comment in the code-behind gives a sample URL `http://localhost/Neo.Afx.Services/Workflows/WorkflowService.svc/GetWorkflowTemplate?entityID={0}`. The `.svc` file on disk is under `Services/`, not `Workflows/`.

**Implementation:** Implemented.

**Confidence:** High for the operation list. Medium for the public URL path.

### FR-008 — Workflow task statuses

**Requirement:** Task status values are the integers in `WorkflowInstanceTaskStatusEnum`.

**Implemented** in `Neo.Afx.Services.Data/Workflows/Enums.cs`:

| Value | Name |
|---|---|
| 1 | Outstanding |
| 2 | Approved |
| 3 | Rejected |
| 4 | Completed |
| 5 | RequestedCorrection |
| 6 | Cancelled |
| 7 | ReAssigned |
| 8 | New |
| 9 | Error |
| 10 | OnHold |
| 11 | Expired |
| 12 | Override |
| 13 | Escalated |
| 14 | EscalationSentBack |

The copy in `Neo.Afx.WinService.Workflows/Enums.cs` stops at Expired (11). It has no Override, Escalated, or EscalationSentBack.

**Confidence:** High that both enums exist. **Unknown** which set the database check constraint uses.

### FR-009 — Background workflow execution

**Requirement:** A worker executes outstanding automated tasks using handlers stored in the database.

**Trigger:** `WorkflowTaskProcessor.Run`.

**Behaviour:** First run builds a dictionary of `WorkflowTaskHandlerID` to a `WorkflowTask` (type name, method name, settings). Each task with a handler id is invoked. Exceptions become status Error. Status less than or equal to 0 is skipped. `DefaultTask.Run` does nothing and returns true.

**Evidence:** `Neo.Afx.WinService.Workflows/WorkflowTaskProcessor.cs`, `DefaultTask.cs`.

**Implementation:** Implemented as a library. No service host project.

**Confidence:** High.

### FR-010 — Documents service

**Requirement:** A caller can upload, copy, delete, retitle, re-parent, fetch, and start a scan session for documents.

**Operations:** `UploadFile`, `CopyDocuments`, `Delete`, `UpdateDocumentItemID`, `UpdateDocumentTitle`, `GetDocument`, `GetDocuments`, `CreateScanSession`.

**Evidence:** `Neo.Afx.Services.Web/Services/DocumentService.svc.cs`.

**Implementation:** Implemented.

**Confidence:** High for the operation list. Store type SQL = 2 is the only value in `DocumentStoreTypeEnum`.

### FR-011 — Correspondence service

**Requirement:** A caller can create, update, delete, and read messages and list message types and templates.

**Inputs on create:** `entityID`, `itemID`, `messageTypeID`, recipients, cc, bcc, subject, message, `isHtml`.

**Message types:** Email = 1, Sms = 2 (`Neo.Afx.Services.Data/Correspondence/Enums.cs`).

**Evidence:** `CorrespondenceService.svc.cs`.

**Implementation:** Implemented.

**Confidence:** High.

### FR-012 — Send email and SMS

**Requirement:** A background sender delivers queued email by SMTP and SMS by Clickatell or Swift.

**Behaviour:** `EmailSender` reads host, port, SSL, and credentials from `GetMailSetting()`. `SmsSender` switches on `SmsDeliveryMethodEnum`: Vodazone = 1 constructs `SmsVodazone` (Clickatell API id, user, password); Swift = 2 constructs `SmsSwift` (user id, user ref).

**Send states:** Ok, UnknownFormatError, InvalidCredentialsError, InvalidSchemaError, UnspecifiedError.

**Evidence:** `Neo.Afx.WinService.Messages`.

**Implementation:** Implemented. Settings rows are not in the repo.

**Confidence:** High.

### FR-013 — Queries

**Requirement:** A caller can create and manage a query attached to an entity item, including type, source, assignee role, assignee user, description, and response addresses.

**Statuses:** InProgress = 1, Resolved = 2, Cancelled = 3, Completed = 4.

**Evidence:** `QueryService.svc.cs`, `Neo.Afx.Services.Data/Queries/Enums.cs`.

**Implementation:** Implemented.

**Confidence:** High for codes. **Unknown** how Resolved differs from Completed in business terms. The enum comments only repeat the names.

### FR-014 — Comments, notifications, audits

**Requirement:**

- Comments can be created, updated, deleted, and listed for an entity item.
- Notifications can be created, marked read, and listed (optional unread only).
- Audits can be listed with optional source, action, and order.

**Evidence:** `CommentService.svc.cs`, `NotificationsService.svc.cs`, `AuditService.svc.cs`.

**Implementation:** Implemented. No create-audit operation on the web service.

**Confidence:** High.

### FR-015 — Integration data sources

**Requirement:** A caller can fetch a named data source and clear its cache.

**Operations:** `IntegrationService.GetData(name)` returns `XElement`. `ClearCache(name)` returns void. Namespace is `neo:afx:services` (other services use `neo:services:data`).

**Connection types:** SqlServer = 1, OleDb, Odbc, Oracle, Web.

**Command types:** Text = 1, StoredProcedure, Get, Post, StoredProcedureExec.

**Auth types:** Anonymous = 1, NetworkCredential = 2.

**Evidence:** `IntegrationService.svc.cs`, `Neo.Afx.Services.Data/Integration/Enums.cs`.

**Implementation:** Implemented. Named sources such as `OfficeLocations` appear only inside tests, not as data.

**Confidence:** High.

### FR-016 — SQL CLR helpers

**Requirement:** SQL Server can call formatting, integration, RSA ID validation, and geo-distance functions from `Neo.Afx.SqlServer`, and password functions from `Neo.Afx.SqlServer.Security`.

**Registration:** `RegisterNeoAfxSqlServer.sql` creates the assembly from `E:\MSSQL\CLR\Neo.Afx.SqlServer.dll` with `EXTERNAL_ACCESS`, after `ALTER DATABASE ... SET TRUSTWORTHY ON` and `sp_changedbowner 'sa'`. Database name is a SQLCMD variable.

**Functions registered by that script include:** `Integration.Invoke`, `Integration.InvokeQuery`, `Integration.GetData`, `System.GetFormattedEntity`, `System.GetEntityAsXml`, `System.GetEntityAsCsv`, `System.GetFormattedDataset`, `System.GetCleanStringASCII`, `System.GetCleanXmlASCII`, `Crm.Fn_GetFormattedMessages`, `System.Fn_IsValidRsaId`, `System.Fn_GetDistanceBetweenPoints`.

**Not registered by that script:** `IsValidRsaCompanyRegNum`, `IsValidRsaTrustNumber`. Those C# methods exist.

**Security script** creates `HashPassword`, `CreatePasswordSalt`, and `CreatePassword` from `C:\Development\Orca\Neo\SqlServer\V1\Deploy\Neo.SqlServer.Security.dll` with `SAFE`.

**Implementation:** Implemented as scripts. Whether they were applied to EPWP is unknown.

**Confidence:** High for script contents.

### FR-017 — RSA identity number check

**Requirement:** `IsValidRsaIdNumber` returns true only for a 13-character numeric string whose check digit matches the implemented sum.

**Behaviour:** Null, empty, length other than 13, or any non-digit returns false. Odd positions (1-based) are summed. Even positions are concatenated, multiplied by 2, and the digits of that product are summed with the first sum. The check digit is `10` minus the last digit of that total, compared with the 13th digit. A two-digit intermediate uses its second digit.

**Evidence:** `Neo.Afx.SqlServer/Validation/RsaIdValidation.cs`. Comment: "Does check digit validation on an RSA id number".

**Implementation:** Implemented. Date-of-birth and citizenship digits are not checked.

**Confidence:** High.

### FR-018 — Company and trust number format

**Requirement:** `IsValidRsaCompanyRegNum` and `IsValidRsaTrustNumber` return true when the value matches `^\d{4}/\d{6}/\d{2}$`.

**Evidence:** `RsaCompanyRegNumberValidation.cs`, `RsaTrustNumberValidation.cs`.

**Implementation:** Implemented as format checks. Comments say "check digit validation". The trust method's comment still says "Company Registration Number". No check digit is calculated. These functions are not in `RegisterNeoAfxSqlServer.sql`.

**Confidence:** High.

### FR-019 — Membership provider defaults

**Requirement:** When `DomainMembershipProvider` is initialized, unset config uses these defaults: max invalid attempts 5, attempt window 10 minutes, minimum non-alphanumeric characters 1, minimum length 7, password strength regex empty, password reset enabled, password retrieval enabled, question and answer required, unique email required, exceptions written to the event log, password format Hashed.

**Evidence:** `Neo.Afx.Core/Security/DomainMembershipProvider.cs` `Initialize`. Hash implementation is SHA1 (`SecurePassword.Hash`).

**Implementation:** Implemented in the provider. No `Web.config` in this repository registers the provider, so these defaults are not an active site policy.

**Confidence:** High for the code defaults. Unknown for production settings.

### FR-020 — Reports

**Requirement:** Reports can be produced as CSV, Excel, or PDF.

**Evidence:** `ReportGenerator` constants `FORMAT_CSV`, `FORMAT_EXCEL`, `FORMAT_PDF`, using DevExpress `XtraReports`. SSRS 2010 clients exist: ASMX web reference `ReportingServices2010`, and a WCF client under the folder name `Pilotfish.Afx.Services`.

**Metadata source recorded in the service map:** `http://hannes-pc/Reportserver_HANNESDESKTOP/ReportService2010.asmx`.

**Implementation:** Client proxies and a generator class are implemented. The folder name Pilotfish is the service-reference folder. The contract namespace inside it is SQL Server Reporting Services, not a separate Pilotfish API.

**Confidence:** High that the clients are SSRS. Unknown which reports exist on a server.

### FR-021 — Device controls

**Requirement:**

- Fingerprint control can open a reader and capture a fingerprint (`OpenReader`, `StartCaptureAsync`, `CaptureFingerAsync`).
- Document scanner can scan with UI options for colour, duplex, ADF, resolution, and source (`ScanDocument`).
- Barcode control can print labels (`PrintLabels` with barcode data, printed-by, copies, printer path, label size, margin, preview).

**Evidence:** ActiveX projects listed in `features.md`.

**Implementation:** Implemented. .NET 2.0 for fingerprint and printing. .NET 4.6.2 for the scanner.

**Confidence:** High for the methods. Unknown which hosts embed the controls.

### FR-022 — Checklist template status

**Requirement:** Checklist templates use Pending = 1, Active = 2, InActive = 3, Cancelled = 4.

**Evidence:** `Neo.Afx.Services.Data/Forms/Enums.cs`.

**Implementation:** Enum only. `FormService` is not in source. Forms entities exist under `Neo.Afx.Services.Data/Forms`.

**Confidence:** Medium. The web operation surface for forms is missing.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| SQL Server catalog `EPWP` or `VectorWorkflowsLive` | Both | Every service and worker | Entity Framework entities and stored procedures | SQL login in config. Password omitted | Exceptions surface as JSON `ErrorMessage` on the console, or WCF faults. `includeExceptionDetailInFaults` is true |
| SMTP | Out | Message worker | Host, port, SSL, credentials from `GetMailSetting()` | Network credential when not using default credentials | `MessageSendState` |
| Clickatell (named Vodazone in the enum) | Out | SMS worker | API id, user, password | Passed into `SmsVodazone` | `MessageSendState` |
| Swift SMS | Out | SMS worker | User id, user ref | Passed into `SmsSwift` | `MessageSendState` |
| SQL Server Reporting Services 2010 | Out | Report code that uses the generated client | Report server SOAP | Unknown. Metadata was taken from `hannes-pc` | Not specified in this repo |
| DigitalPersona, WIA, printer | Local device | ActiveX host | Fingerprint sample, scanned file bytes, label print | Local | Capture and scan methods return bool or raise the control's events |
| Named integration data sources | Out | `GetData` / SQL `Integration.GetData` | Connection type SQL, OLE DB, ODBC, Oracle, or HTTP | Anonymous or network credential, stored on the data source row | Not specified beyond exceptions |

## Constraints

- Services are WCF REST-style (`WebServiceHostFactory`), not ASP.NET Web API controllers. A Web API client assembly is referenced by the console project.
- `webHttpBinding` allows `maxReceivedMessageSize` and `maxBufferSize` of 2147483647 and `transferMode` Streamed.
- Service debug includes exception detail in faults, and metadata HTTP GET is enabled, in `Neo.Afx.Services.Web/Web.config`.
- User console targets .NET Framework 4.0. Web services target 4.6.2. They are different solutions.
- Entity id `1` is declared as `_entityID` on the console controller and is not read.
- Password hashing available to SQL is SHA1 of the UTF-8 password, Base64, via `SecurePassword`. A salt overload concatenates password and salt before hashing.

## Non-requirements (not found)

- No NLTIS screen, rule, or schema.
- No login page in this repository.
- No password-reset UI.
- No automated test that asserts behaviour.
- No deployment pipeline.
- No statement of supported browsers, scanners, or fingerprint readers beyond the vendored SDK binaries.

## Open questions

- What product does the slug `nltis` name?
- Is catalog `EPWP` / `VectorWorkflowsLive` still authoritative?
- Which status enum (11 values or 14) matches the database?
- Were `FormService` and `ValidationService` removed on purpose?
- Does any host still apply `[Authorize]` around `Afx.UserConsole`?
