# PRD — personal_laundry_20240415

Evidence date: 2026-10-06.

Source: `git@bitbucket.org:overdrive/personal_laundry_20240415.git` at `4af604f` (`master`, 2024-03-19).

Provenance labels used below: **Documented Requirement**, **Implemented Behaviour**, **Inferred Behaviour**, **Planned Behaviour**, **Unknown**.

There is no product requirements document in the source repository. Requirements below are recovered from the two Word process files, the Pastel sample CSV, and the implementation. Where the Word file and the code disagree, both are recorded.

Database passwords, the Office 365 mailbox password, and commented `sa` passwords are not copied here.

## Product purpose

**Documented Requirement** and **Implemented Behaviour.** The system runs a commercial laundry's linen cycle for named customers (the sample and the email code use hotels):

1. The customer records what they are sending, by wash type and item, and attaches a reference and bag labels.
2. The laundry counts the physical items, including items that will not be washed (condemned).
3. Clean items are packed into labelled packages, packages are sealed into delivery bags, and the laundry prints an invoice and a delivery note.
4. Delivered work is exported as a Pastel invoice file.

Evidence:

- `PL Current linen management system with sample documentation.docx` narrates count, pack, bag, dispatch, invoice, and delivery note, using pre-list `32201` and bag seal `123456`.
- `PL Current linen mngt system - Client Pick Up System Trail.docx` narrates client login, wash-type selection, towelling, linen, and food-and-beverage counts, a reference, and a successful pickup.
- Floor stations `Station_01_Counting` through `Station_04_Dispatch`, website routes in `PersonalWeb/Global.asax`, and `PersonalWeb/App_Code/PastelExport.cs`.

**Unknown / Requires Confirmation:** legal entity, whether the system is still in production, and which SQL Server is the system of record. The public marketing site is `personal_laundry_website`, not this repository.

No success metric is written in the repository.

## Users and roles

| User | What the repository shows | Provenance |
|---|---|---|
| Laundry floor operator | Signs in to `PersonalLaundry` if `OperatorRole.CanLogin` is true. Password is entered on a 0–9 keypad or keyboard. Sees Count, Pack, Bag, and Dispatch | **Implemented Behaviour** in `PersonalLaundry/LoginForm.cs` and `ConsoleForm.cs` |
| Office administrator | Signs in to `PersonalAdmin` only if `OperatorRoleId` is 3 and `CanLogin` is true. Menu: Active Customers, Inactive Customers, Operators, Price Increase, Reports, Settings | **Implemented Behaviour**. The role *name* for id 3 is **Unknown** (database) |
| Customer contact | Signs in to `PersonalWeb` through `LoginCustomer`. Lands on `pickups`. Records a pickup and prints bag labels | **Implemented Behaviour** in `Login.aspx.cs` |
| Web operator | If `LoginCustomer` fails, `LoginOperator` is tried. A mobile user-agent is sent to `/Mobile/`. Anyone else is sent to `admin/export` | **Implemented Behaviour** |
| Authorising operator on a pickup | The customer page requires a second operator username and password before the pickup is saved. That id is stored on `Workload.OperatorAuthoriseId` | **Implemented Behaviour** in `Pickup.aspx.cs`. Comment dated 2018-01-18 |
| Driver / collector | Mobile page splits bags whose `OperatorLoginId` is 1 ("waiting") from bags whose operator id is not 1 ("collected") | **Inferred Behaviour** from `Mobile/Customer.aspx.cs`. The meaning of operator id 1 is not named in code |
| Anonymous web user | `web.config` denies `?`. A private-network request that carries `ReturnUrl` and does not set `RequireAuth=true` is given a Forms cookie for `test@test.com` and redirected | **Implemented Behaviour** in `Login.aspx.cs` `Page_Load` |
| Fingerprint user | Enrollment, capture, and verification forms exist and call DigitalPersona `DPFP` plus `SaveFingerprint` | **Implemented** as forms. They are not opened by the floor login or the console. Whether they are still used is **Unknown** |

Role names live in `OperatorRoles`. The code reads `RoleName` only to display it. It does not branch the floor console on the name. An older conflicted copy of `ConsoleForm` did require `RoleName == "Administrator"` for some actions. That copy is not the compiled `ConsoleForm.cs`.

`PersonalWeb/Admin/Operators.aspx.cs` lists operators with `OperatorLoginId > 1`. One code path also excludes `OperatorRoleId == 2`. Creating an operator on that page sets `OperatorRoleId` to 2. The meaning of role 2 is **Unknown**.

## Information the system keeps

**Implemented Behaviour** in `DataModel.edmx` and the generated entities. Display values for lookup tables are **Unknown** unless noted.

| Area | Tables and notable fields |
|---|---|
| Customer | `Customers`: name, billing profile, contact, three email addresses, address, VAT number, `PastelCode`, `DoublePrintBagLabel`, `IsActive` |
| Customer access | `CustomerLogins` (email, active flag), `CustomerLoginLog` |
| Operator | `OperatorLogins` (username, password column, role, shift), `OperatorRoles` (`RoleName`, `CanLogin`) |
| Catalogue | `InventoryItemTypeCategories`, `InventoryItemTypes`, `InventoryItemTypeSize`. Website groups inventory into the categories `Laundry`, `Linen`, and `Food and Bev` |
| Customer prices | `CustomerInventory`: `CostGeneralWash`, `CostRepeatWash`, `CostStainWash`, `CostRewashWash`, stored as nullable integers. Pastel export divides `Cost` by 100 |
| Pickup / workload | `Workloads`: customer, login, status, type, `Reference`, `OperatorAuthoriseId`, `TotalBags`. `CustomerWorkloadBags.UniqueReference`. `WorkloadItems` |
| Invoice | `Invoices`, `InvoiceItems` (`ItemCount`, `BadCount`, `Cost`, `CondemnedPrinted`), `InvoiceBags`, `InvoiceStatus` |
| Pack and dispatch | `Packages` (`ItemCount`, `ItemCountStain`), `Dispatches` (`BagSeal`, `DeliveryId`, `DateDelivered`), `DispatchPackages`, `DispatchOperator`, `DispatchStatus` |
| Billing | `BillingProfiles.IncludeRewashInInvoice`, `VAT` |
| Settings | `Settings` name/value rows. Known names are listed in `business-rules.md` |
| Audit | `CustomerInventoryAudit`, `WorkloadItemsAudit`, `WorkloadsAudit`, `DriverLog`, `DriverActions`, `Actions` |

`GetCustomerInventory` returns `ProductCode`. The admin inventory grid binds that column. The save button calls `UpdateInventoryItemType` with category, type, and size only, so this screen does not send `ProductCode` back. Where `ProductCode` is stored is inside the stored procedure and is **Unknown**.

## Main workflow

**Documented Requirement** (Word file) and **Implemented Behaviour** (stations and website).

```text
Customer login
  -> choose wash type
  -> enter counts (Laundry, Linen, Food and Bev)
  -> enter an 8-digit reference and total bags
  -> an operator authorises
  -> bag labels
Floor: Count bags and items (good count and bad/condemned count)
  -> Finish Counting (locks further counting, optional trolley labels, variance email, condemned email)
Floor: Pack open work into packages (package label)
Floor: Bag packages into a delivery bag and capture a seal
Floor: Dispatch — scan bag, print invoice, print delivery note
Office/web: export delivered dispatches to a Pastel file
```

Status ids used by the floor console, without the database's display names:

| Id check | Where | What the code uses it for |
|---|---|---|
| `WorkloadStatusId == 1` | Pickup page and mobile page | An open pickup. A new pickup of a type is hidden when that customer already has status 1 for a type with `CanStartCycle` |
| `WorkloadStatusId == 4` or `5` | Floor console | Counted toward the Pack button's "ready" total |
| `InvoiceStatusId == 3` | Bagging | Invoices offered for bagging, newest workload first, capped at 100 |
| `DispatchStatusId == 2` | Dispatch form | Dispatches that can be selected |
| `DispatchStatusId == 2` or `3` | Floor console | Counted toward the Bag button's "ready" total |

The names of those statuses are **Unknown / Requires Confirmation**.

## Functional requirements

### FR-001 — Floor operator sign-in

Requirement: A floor operator chooses a username from operators whose role has `CanLogin` and enters a password. `LoginOperator` must return a row.

Who: Floor operator.

Trigger: `PersonalLaundry` starts `LoginForm` from `ConsoleForm`.

Inputs: Username, password.

Expected behaviour: On success the console shows the username and role name and starts a refresh timer. On failure the password is cleared and an error dialog is shown.

Outputs: `ConsoleForm.OperatorLogin`.

Validation: Both fields required. Authentication is the stored procedure `LoginOperator`. The procedure body is not in git.

Error conditions: Database exceptions on load are written under `C:\LogFiles`. A null `InnerException` would throw again inside the logger.

Dependencies: SQL Server, `OperatorLogins`, `OperatorRoles`.

Implementation: **Implemented** in `PersonalLaundry/LoginForm.cs`.

Confidence: High for the screen. Low for how the password is compared.

Evidence: `LoginForm.cs`, `LoginOperator` function import.

### FR-002 — Idle return to the login screen

Requirement: After 10 minutes without input, the floor application restarts.

Who: Floor operator.

Trigger: Timer tick while the console is active. Comment: "specified by TK" (2018-01-09).

Inputs: Last-input tick from `GetLastInputInfo`.

Expected behaviour: If idle time is greater than 600000 ms, `Application.Restart()`.

Outputs: Process restart, which shows the login form again.

Validation: Checked when the clock second is divisible by 5.

Error conditions: None handled around the restart.

Dependencies: Windows last-input API.

Implementation: **Implemented** in `PersonalLaundry/ConsoleForm.cs`.

Confidence: High.

### FR-003 — Floor home counts

Requirement: The home screen shows a workload-status grid and turns Count, Pack, Bag, and Dispatch green when work is waiting.

Who: Signed-in floor operator.

Trigger: Login, and every timer tick when the second is divisible by 5.

Inputs: Views and counts described in the status table above.

Expected behaviour: Green with white text when the count is greater than zero, otherwise grey with black text.

Outputs: Button colours and `WorkloadStatusGrid`.

Implementation: **Implemented** in `ConsoleForm.LoadWorkflowStatusGrid`.

Confidence: High for the colours. The status names in the grid come from `vwWorkloadStatusReportWithCounts` and are **Unknown** without the database.

### FR-004 — Customer web sign-in

Requirement: A customer signs in with email and password. Success stores a Forms cookie for the login GUID and opens the pickup list.

Who: Customer contact.

Trigger: `LoginButton_Click` on `Login.aspx`.

Inputs: Email, password, client IP (passed to `LoginCustomer`).

Expected behaviour: `LoginCustomer` hit redirects to route `pickups-root`. A miss tries `LoginOperator`. Operator success redirects mobile user-agents to `/Mobile/` and everyone else to `admin/export`. Both fields empty, or both logins missing, shows "Invalid email address/password".

Outputs: Forms authentication cookie.

Validation: Non-empty email and password. Further checks are inside the stored procedures.

Error conditions: Invalid pair stays on the login page.

Dependencies: `LoginCustomer`, `LoginOperator`.

Implementation: **Implemented**.

Confidence: High.

Evidence: `PersonalWeb/Login.aspx.cs`.

### FR-005 — Private-network login bypass

Requirement: This behaviour is implemented. It is not described in the Word documents.

Who: Any browser on a private address that requests the login page with `ReturnUrl`.

Trigger: `Page_Load` when `ReturnUrl` is non-empty, the client address is loopback or RFC1918, and `RequireAuth` is not `true`.

Inputs: `ReturnUrl`, client IP.

Expected behaviour: Sets a Forms cookie for the literal name `test@test.com` and redirects to `ReturnUrl`. No database lookup is performed.

Outputs: Authenticated session under that name.

Error conditions: IPv6 addresses are resolved to an IPv4 address on the same machine. Parse failure yields IP value 0, which is not treated as private.

Implementation: **Implemented**. Whether this is still an accepted rule is **Requires Confirmation**.

Confidence: High that the code does this.

### FR-006 — Record a pickup

Requirement: The customer states the wash type, the quantities they are sending, a reference, and how many bags, and an operator authorises it.

Who: Signed-in customer, plus an authorising operator.

Trigger: Submit on `pickups/new` or `pickups/update/{id}`.

Inputs: Wash type (`WorkloadTypes` where `CanStartCycle` is true), form fields named `inv_{id}`, reference, total bags, operator username and password.

Expected behaviour:

- Reference is left-padded with zeros to 8 characters.
- If any workload already has that reference, the page shows "Reference already exists!" and does not save. The check does not exclude the pickup being edited.
- Operator username and password are required and must pass `LoginOperator`.
- `CreateWorkload` is called with the customer login id, type, item string, reference, and total bags.
- `OperatorAuthoriseId` is set to the authorising operator.
- The browser is sent to `pickups/baglabels/{workloadId}`.

Outputs: A workload, workload items, and bag-label page.

Validation: Comments dated 2016-10-20 (duplicate reference) and 2018-01-18 (authorisation, 8-digit reference). Total bags is parsed with `int.Parse` and will throw if it is not an integer.

Error conditions: Missing authorisation, duplicate reference, `CreateWorkload` returning null (page returns without a message).

Dependencies: `CreateWorkload`, `LoginOperator`, customer inventory for that customer.

Implementation: **Implemented** in `Pickup.aspx.cs`. The item-count rules inside `CreateWorkload` are **Unknown**.

Confidence: High for the page rules.

The Word pickup trail matches this screen: wash type, towelling, linen, food and beverage, reference, success. **Documented Requirement.**

### FR-007 — One open pickup per wash type

Requirement: When starting a new pickup, wash types that already have a workload in status 1 for this customer are removed from the list, unless `CanStartCycle` is false.

Who: Customer.

Trigger: Load of a new pickup.

Implementation: **Implemented** in `Pickup.aspx.cs` `LoadInventory`.

Confidence: High for the filter. What each `WorkloadType.Type` string is (for example general wash, rewash, stain) is **Unknown** except that Pastel later prints `WorkloadTypes.Type` as the freight method.

### FR-008 — Bag labels for a pickup

Requirement: After a pickup is saved, the customer prints labels whose number is the reference the floor uses to find the bags.

Who: Customer.

Trigger: Redirect to `pickups/baglabels/{id}`, and print route `pickups/baglabels/print/{id}`.

Expected behaviour: The Word file says the reference (example `32201`) identifies the bags for that order and is unique per order. The website enforces uniqueness of `Workloads.Reference` on submit.

Outputs: Bag label pages. Barcodes are JPEGs from `Handlers/BarcodeHandler.ashx` (`type` plus integer `value`), drawn by `Common.BarcodeGenerator`.

Implementation: **Implemented** as pages `BagLabels.aspx` and `BagLabelsPrint.aspx`.

Confidence: Medium. The label layout was not transcribed field by field.

### FR-009 — Count incoming bags

Requirement: The counter sees customers who still need counting and how many bags were sent, opens a bag by its reference, and counts items under Laundry, Linen, and F&B.

Who: Floor operator.

Trigger: Count button opens `CountingStation`.

Inputs: Bag reference, item type, quantity. Bad quantities are written with `UpdateInvoiceItemBadCount`.

Expected behaviour: The Word file describes green boxes per bag, coloured boxes per counted type, and a Finish Counting action that stops further counting and can print trolley labels. The code's Finish Counting path:

- Refuses to finish when the workload has no invoice or no invoice items.
- Asks for confirmation.
- Calls `FinishCounting(workloadId, operatorLoginId)`.
- Optionally prints trolley notes on the POS printer.
- Calls `sendEmail()`.

Outputs: Invoice item counts, `BadCount`, trolley labels, emails.

Implementation: **Implemented** in `Station_01_Counting`. Category colours are described in the Word file; the exact colour map in the WinForms designer was not transcribed.

Confidence: High for the finish path. Medium for the on-screen colour legend.

### FR-010 — Email when the laundry count differs from the customer count

Requirement: The Word file says this email was **not yet** in the system and should go to a defined recipient. The code sends it.

Who: Floor operator, by finishing the count. Recipients are chosen in code.

Trigger: `FinishCounting` succeeded and `vwInvoiceCountsRecon` has rows for the workload.

Inputs: Recon view (customer count, invoice count, item, category). Email subject and body from settings `Email-Subject: CountRecon Issue` and `Email-Body: CountRecon Issue`. Placeholders `[WorkOrderID]` and `[Items]`.

Expected behaviour:

- If the customer name is `S.S.W.F`, `Capital Mirage Hotel`, or `The Stack`, the primary customer email is the To address.
- Otherwise the To address is `tk@personalcleaners.co.za`.
- Customer CC addresses from `GetCustomerCCEmail` are added when the first value is non-empty, split on `;`.
- `tk@personalcleaners.co.za` and `maanie@overdrive.co.za` are always BCC on this message.
- Mail is sent through Office 365 SMTP on port 587 with SSL. The From header is `Company Name` and `Company Email` from settings.

Outputs: One HTML email, or none when the recon view is empty.

Error conditions: A missing settings row throws when the code indexes `[0]`. Send failures are logged.

Implementation: **Implemented** in `CountCustomersLaundry.sendEmail`. The Word file's "not in our system" line is **obsolete relative to this code**. Whether the three customer names and the BCC list are still approved is **Requires Confirmation**.

Confidence: High.

### FR-011 — Record condemned items and email them

Requirement: The Word file says a torn or damaged piece must be counted and identified as condemned, returned unwashed, and not cleaned. It also says this was not yet in the system.

Who: Floor operator, during counting.

Trigger: Bad-count entry via `UpdateInvoiceItemBadCount`. Email when `vwInvoiceCountsCondemned` has rows after finish.

Expected behaviour: The invoice page prints a row "{BadCount} x {Size} condemned" when `BadCount > 0`. The condemned email uses settings `Email-Subject: Condemn Issue` and `Email-Body: Condemn Issue`, the same recipient rule as FR-010, and lists item, category, and bad count.

`CondemnedPrinted` is set or cleared from the dispatch print form. A comment dated 2016-11-02 says condemned-printed is cleared for all condemned items on the first invoice or delivery.

Outputs: `InvoiceItems.BadCount`, condemned email, print flag.

Implementation: **Implemented**. The rule "returned uncleaned and not washed" is **Documented** in the Word file and is **not** a separate status in the C# that was read. How the plant floor treats those pieces physically is **Unknown**.

Confidence: High for the count and the email. Medium for the physical return.

### FR-012 — Pack counted items into packages

Requirement: After counting, each category is packed. The Word file says package size is agreed with the customer (example: duvet covers in piles of 25), a package label is printed, and colour shows unpacked (red), held back (orange), and fully packed (green).

Who: Floor operator.

Trigger: Pack button opens `CreatePackage`.

Inputs: Invoice item and quantity. `CreatePackage` is the stored procedure. `Packages` also has `ItemCountStain`.

Expected behaviour: Open work is what the console treats as workload status 4 or 5. The older packing query that filtered invoice status 2 or 3 is commented out.

Outputs: Package rows and a package label with a barcode the bagging screen can scan.

Implementation: **Implemented** as screens `CreatePackage`, `PackageInvoice`, `PackageInvoiceItem`, `PackageInvoiceType`. The red/orange/green rule is **Documented** in the Word file. Confirming each colour in the designer was not done; treat the legend as documented, and the existence of the pack station as implemented.

Confidence: Medium.

### FR-013 — Put packages into a delivery bag

Requirement: Packages are scanned or selected into a delivery bag. A seal number is captured. A bag label lists the customer and the packages.

Who: Floor operator.

Trigger: Bag button opens `BagForm`.

Inputs: Package barcode or selection, seal number.

Expected behaviour: Bagging lists invoices in status 3, at most 100, newest workload first. `DoublePrintBagLabel` on the customer is stored. The Word file's example seal is `123456`.

Outputs: `Dispatches.BagSeal`, `DispatchPackages`, optional bag label.

Implementation: **Implemented** in `Station_03_Bagging`.

Confidence: Medium for the seal validation. No C# check that the seal is unique was found in the form; uniqueness may be inside `CreateDispatch` and is **Unknown**.

### FR-014 — Dispatch, invoice, and delivery note

Requirement: Bags are scanned. The screen confirms contents. The operator prints an invoice and a delivery note. The Word file says the delivery note is printed in duplicate: one copy for the customer, one signed copy returned to the laundry.

Who: Floor operator.

Trigger: Dispatch button opens `DispatchForm`. Print path is `PrintNotesForm`.

Inputs: Bag barcode. Dispatch rows in status 2 can be selected.

Expected behaviour: `DeliveryId` and `DateDelivered` group a dispatch into a delivery. `UpdateDispatchDelivery`, `UpdateDeliveryId`, and `GetDispatchesReadyToPrint` support that grouping. A comment dated 2019-02-15 says rewash skips a null check.

The Word file asked for three groupings that were "not in our system":

| Grouping | What the code shows |
|---|---|
| Multiple packages in one bag | **Implemented** as `DispatchPackages` and the bagging screen |
| Multiple bags on one invoice | **Inferred** from `Dispatches.InvoiceId` (many dispatches can share an invoice). The procedure that creates them was not read |
| Multiple invoices on one delivery note | **Inferred** from `Dispatches.DeliveryId` and the print form's delivery id. The procedure was not read |

Outputs: Printed invoice and delivery note. `Invoice.Printed`. Condemned print flags.

Implementation: **Implemented** as dispatch and print screens. Duplicate-copy printing is **Documented** and was not confirmed in the printer code.

Confidence: Medium.

### FR-015 — Customer and office users can open invoices and dispatches

Requirement: The website lists invoices and dispatches and can open one.

Who: Customer (cookie is a login GUID) or operator.

Trigger: Routes `invoices`, `invoices/{action}/{customerid}/{id}`, `dispatches/{action}/{customerid}/{id}`.

Outputs: HTML invoice including condemned rows.

Implementation: **Implemented** in `Invoices.aspx`, `Invoice.aspx`, `Dispatch.aspx`.

Confidence: Medium. Authorisation that the GUID's customer matches `{customerid}` was not confirmed line by line.

### FR-016 — Mobile bag board

Requirement: A phone signed in as an operator sees customers and, for a customer, bags on open workloads (status 1). Bags with `OperatorLoginId` 1 are "waiting". Other operator ids are "collected".

Who: Operator on a mobile browser.

Trigger: Login redirect to `/Mobile/`, route `mobile/{id}`.

Implementation: **Implemented** in `PersonalWeb/Mobile`.

Confidence: Medium. There is no separate driver role check on the page.

### FR-017 — Office administration of customers

Requirement: An office user can add and edit customers, split them into active and inactive, manage each customer's logins, and open an inventory price screen.

Who: `PersonalAdmin` user. See the login-id discrepancy under FR-019.

Inputs: Name, billing profile, contact, address, VAT number, Pastel code, extra emails, double-print bag label, `IsActive`.

Expected behaviour: Active customers (`IsActive`) and inactive customers are separate menus. Each customer has Logins (checked when `CustomerLogins.IsActive`), Add a new login, and Update Inventory. Both menus also offer Add a new customer.

Implementation: **Implemented**. `IsActive` was added in commit `c98fb21` (comment 2023-08-31).

The website repeats customer editing at `admin/customers` and `admin/customers/{customerId}`, including Pastel reference.

Confidence: High.

### FR-018 — Prices and a percentage increase

Requirement: Each customer inventory row has four wash prices. A Price Increase screen applies percentage changes and warns that they cannot be undone.

Who: Office user.

Trigger: Update Inventory, or the Price Increase menu (comment "Added 2016/04/21").

Expected behaviour: Apply calls `UpdateCustomerInventoryByPercentages(operatorLoginId)` after a Yes confirmation. The operator id passed is whatever `ConsoleForm.operatorLogin` is at that moment (see FR-019).

`BillingProfiles.IncludeRewashInInvoice` exists. Whether invoice generation honours it is inside SQL and is **Unknown**.

Implementation: **Implemented** in `UpdateCustomerInventory.cs` and `PriceIncreaseForm.cs`.

Confidence: High for the warning and the procedure call. The percentage fields' exact meaning is **Inferred** from `GetCustomerPricingPercentages` and the form comment "price increase + stain %".

### FR-019 — PersonalAdmin attributes work to operator id 2

Requirement: This is implemented behaviour, not a written requirement.

Who: Whoever signs in to PersonalAdmin.

Trigger: `PopulateMenu`.

Expected behaviour: After a successful login, `PopulateMenu` loads `OperatorLogins` where `OperatorLoginId == 2` and replaces `this.operatorLogin` with that row. Later saves, including the price increase, use that id.

The login form itself only offers `OperatorRoleId == 3`.

Implementation: **Implemented** in `PersonalAdmin/ConsoleForm.cs` line that queries id 2. **Requires Confirmation** whether operator 2 is a shared service account on purpose.

Confidence: High that the replacement happens.

### FR-020 — Operators

Requirement: Office staff can add operators and can see which existing operators are allowed to log in (`CanLogin` shown as a check mark). The website can create an operator with role id 2 and a shift left blank, and can edit role through the grid.

Who: PersonalAdmin user, and a web operator on `admin/operators`.

Inputs: Username and password. The web page assigns `Password` from the text box with no hashing visible in the page.

Implementation: **Implemented**. Password storage format is **Inferred** to be whatever `LoginOperator` compares, which is not in git. The page stores the typed value.

Confidence: High for the screens.

### FR-021 — Reports

Requirement: PersonalAdmin can run an Outstanding Order report and a Bag Recon report for pickups and deliveries.

Who: Office user.

Trigger: Reports menu (comment "Added 20171024").

Inputs: Report parameters on `ReportConsoleForm`. Data from `rptOutstandingOrders`, `rptOutstandingOrders_Items`, `rptBagReconDeliveries`, `rptBagReconPickups`.

Outputs: Local ReportViewer documents `rptOutstandingOrderSubReport2.rdlc` and `rptBagReconReport.rdlc`.

Implementation: **Implemented**. An older outstanding-order method is commented out.

Confidence: High that the two reports exist. The business definition of "outstanding" is inside the procedures and is **Unknown**.

### FR-022 — Company settings

Requirement: Company name, address, phone, email, and VAT number are editable and stored as `Settings` rows. Email templates for count-recon and condemn messages are also settings.

Who: Office user for the company fields exposed on `SettingsForm`. Email template editors on that form were not confirmed; the keys exist on `Common.Settings`.

Implementation: **Implemented** as `Common.Settings` get/set. Web `admin/settings` is a second editor. Its field list was not fully transcribed.

Default when the web-service URL setting is missing: `http://pl-server`. A localhost default is commented.

Confidence: High for the company keys.

### FR-023 — Pastel export

Requirement: Delivered work is downloaded as a Pastel import file. The sample `pastel excel import file layout.csv` shows a `Header` row then `Detail` rows.

Who: Web operator. Login sends non-mobile operators straight to `admin/export`.

Trigger: Export page, for dispatches that `GetDispatchesReadyToExport` returns, between a start and end date.

Expected behaviour of each document, from `PastelExport`:

- Header document number is `IN` plus `DeliveryId` padded to 6 digits.
- Customer code is `Customers.PastelCode`.
- Period is calendar month plus 10 when the month is January or February, otherwise month minus 2.
- Order number is the workload reference, ` - `, and the delivery id.
- Amounts are exclusive (`N`), discount 0, exchange rate 1.
- Delivery address uses customer name and address, each cut with `Left`, which keeps `length - 1` characters when the string is longer than the limit.
- Sales analysis code is the delivery operator's username. Ship/deliver is that operator's shift. Freight method is the workload type name.
- Detail quantity is `ItemCount + ItemCountStain`.
- Unit price is `Cost / 100`. Inclusive price adds VAT.
- VAT rate is the first `VAT` row's `VAT1`. If no row exists, the rate is `0.14`.
- Item code is `II000-` plus `InventoryItemTypeId` padded to 3. An older per-customer code is commented out.
- Store is the literal `Web`. Line type is 4. Unit is `Each`. Tax type 1.
- Lines without a cost are dropped. If every line lacks a cost, the document is skipped.
- A second log lists how many invoices were exported and calls `GetDuplicatePrelists` for the date range.

Outputs: CSV text and an export log.

Implementation: **Implemented**. Matching the sample file is **Inferred**: the sample uses the same Header/Detail shape, customer codes such as `BEC001`, and `Each`. The sample's item codes (`01-001`) do not match the `II000-` code the exporter writes. **Requires Confirmation** which code Pastel is configured to accept.

Confidence: High for the exporter. Medium for fitness against a live Pastel company.

### FR-024 — ClickOnce update

Requirement: Clicking the floor console logo runs `InstallUpdateSyncWithInfo`.

Who: Floor operator.

Implementation: **Implemented** as a call. Publish is configured (`ApplicationVersion` `1.5.0.*`). The update URL's current host is **Unknown**.

### FR-025 — Barcode images

Requirement: Web pages can request a JPEG barcode.

Who: Any caller who can open the handler. The site denies anonymous users, except the private-network bypass in FR-005.

Inputs: Query `type` (parsed as `BarcodeGenerator.BarCodeType`) and integer `value`.

Outputs: `image/jpeg`, or an empty response when type or value is missing or value is not an integer.

Implementation: **Implemented** in `Handlers/BarcodeHandler.ashx`. `IsReusable` is false.

## Integrations

| System | Direction | Trigger | Data | Failure |
|---|---|---|---|---|
| SQL Server `PersonalLaundry` | Both | Every screen | Entities and stored procedures listed in `technical-documentation.md` | Desktop login shows a dialog or logs under `C:\LogFiles`. Web login shows an invalid-credentials label for a failed procedure result. Connection failures were not given a dedicated page |
| Office 365 SMTP `smtp.office365.com:587` | Out | Finish counting, when recon or condemned rows exist | HTML email | Exception logged. Commented hosts `192.168.100.6` and `smtp.vanilla.co.za` are not used |
| Pastel | Out, file | Admin export | CSV of header and detail lines | Skips a delivery with no priced lines. No API acknowledgement |
| POS receipt printer | Out | Trolley notes, package labels, bag labels, invoices, delivery notes | OPOS `PosPrinter` | Printer init retries 5 times on "It is not initialized." |
| DigitalPersona fingerprint reader | In | Enrollment and verification forms, if opened | Fingerprint template via `SaveFingerprint` | Forms are not on the login path that `Program.cs` starts |

There is no queue, no background worker, and no payment gateway in this repository.

## Constraints

- Floor and admin apps target .NET Framework 4.5, x86 for the floor app. The website targets .NET Framework 4.0.
- The website compiles with `debug="true"`, `customErrors mode="Off"`, and `trace enabled="true" localOnly="false"`.
- Business transitions are stored procedures. Changing C# without the database will not change those transitions.
- Git history does not contain the procedure source, so several rules are only visible at the call site.
- Credentials are stored in the repository. See `technical-documentation.md`.

## Conflicts to confirm

1. The Word process document says discrepancy email, condemned marking, and the three invoice groupings were not in the system. The C# implements email, bad counts, packages-in-bags, and delivery ids. The Word file reads as an earlier trail.
2. PersonalAdmin login restricts the user list to role id 3, then the menu builder overwrites the session operator with operator id 2.
3. The Pastel sample file's item codes differ from the codes `PastelExport` writes.
4. The solution references `AcmeTemplates\acme_v_2_1_3_html`, which is not in the repository.
5. A scan-host listing on 2026-09-28 named `sonar-project.properties` and `sonarqube_scan.bat`. They are not in commit `4af604f`.
