# Business rules — personal_laundry

Evidence date: 2026-10-07. Source: `main` at `0b3d107`.

Rules are taken from code and from notes that match the code. Each rule is marked **Documented** or **Inferred**. A documented rule that also appears in code is still safe to test. An inferred rule is only what the code does.

No signed business-rules document exists in the repository. `CHANGELOG.md`, `scope/`, and `PersonalLaundry/documents/` are working notes.

## Identity and access

### BR-001 — POS login is company plus operator

**Inferred.** The operator chooses a company and a username, then enters a password. Stations receive that company id and that operator.

Evidence: `LoginForm`.

### BR-002 — Ten-minute idle returns to login

**Inferred**, with a comment that TK specified it. The console restarts the process after 600000 ms without input.

Evidence: `ConsoleForm.UpdateTimer_Tick`.

### BR-003 — Only an Administrator can reopen a finished count

**Documented** in `CHANGELOG.md` and **Inferred** from code that enforces it. `OperatorRole.RoleName` must equal `Administrator`. A manager code must already be stored. The typed code must match. Cancel or a wrong code leaves the workload closed.

Evidence: `CountCustomersLaundry.StartCountForWorkload`.

### BR-004 — Manager code is digits only, length at most 12

**Documented** in `CHANGELOG.md`. Stored as SHA-256 in `Settings.SettingName = "PostCountEditManagerCode"`. Non-digits are rejected in Admin Settings and in the POS dialog. An older alphanumeric code must be re-saved or it will not match the new format. That last sentence is **Documented** in the changelog, not re-tested here.

### BR-005 — Only role id 3 can reverse a condemned count

**Inferred.** `CountCalculator` blocks the reversal unless `OperatorRoleId` is 3, and the message says "Only Administrators".

This is a different check from BR-003, which uses the role name. If an Administrator row is not id 3, the two screens will disagree. **Requires Confirmation.**

### BR-006 — Operator roles above id 1 are assignable

**Inferred.** Admin operator screens filter `OperatorRoleId > 1`. A role with `CanLogin` false is how an operator is switched off without deleting the row (`OperatorsMenuPanel`).

What role id 1 is: **Unknown / Requires Confirmation.**

### BR-007 — Website users are either customer logins or operators

**Inferred.** Login tries `LoginCustomer` first. If that returns nothing, it tries `LoginOperator`. Customers go to pickups. Operators go to Pastel export, or to `/Mobile/` when the user agent matches the phone expression.

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

### BR-008 — Duplicate customer email is rejected

**Inferred.** `UpdateCustomer.NewLogin` trims and lowercases the email, rejects an empty email, and rejects an email already on `CustomerLogins`.

### BR-009 — Anonymous website users are denied

**Inferred** from configuration. `web.config` denies `?` and allows `*`. Forms authentication cookie name is `PersonalLaundry`.

### BR-010 — Private addresses can skip the password

**Inferred.** When `ReturnUrl` is present, the client IP is loopback or a private range, and `RequireAuth` is not `true`, the site issues a forms cookie for `test@test.com` and redirects. No password is checked.

This is implemented behaviour. It is not confirmed as an approved rule. **Requires Confirmation.**

### BR-011 — Personal Admin overwrites the signed-in operator

**Inferred.** `PopulateMenu` sets `operatorLogin` to the row with `OperatorLoginId` 2. Later customer forms receive that object.

**Requires Confirmation** before anyone treats operator 2 as the business actor of record.

## Customers and companies

### BR-012 — Inactive customers are hidden from counting and from count email

**Inferred.** Counting and `sendEmail` load the customer with `IsActive`. Admin still lists inactive customers in their own menu.

### BR-013 — Company id 2 is presented as Texcare

**Inferred.** Any other company id uses the Personal Laundry logo and caption. Company names come from the `Companies` table. The repository does not seed the names.

### BR-014 — Floor lists are limited to the selected company and to active customers

**Inferred.** Bagging loads invoices where `InvoiceStatusId` is 3, `CompanyID` matches the login, and the customer `IsActive` is true.

Evidence: `BagForm`.

### BR-015 — Double bag labels are a customer flag

**Inferred.** `Customers.DoublePrintBagLabel` selects a second print.

## Workload lifecycle

Numeric ids are what the code writes. Display names are stated only where a comment or script says them.

| Id | What the code does | Provenance |
|---|---|---|
| Workload 1, 2, 3 | Included in the counting list together with status 4 | **Inferred.** Names unknown |
| Workload 4 | Set by `FinishCounting`. Comments call this finished counting. Reopen rules in BR-003 apply | **Documented** and implemented |
| Workload 5 | Set by `CreateDispatch` for every workload on the sealed bag | **Inferred** from SQL. The IntelTag note calls this the bagged state |
| Workload 6 | Set on the workload from `zzDispatchForm` only | **Inferred.** Live `DispatchForm` sets dispatch status, not this workload status, in the section that was read |
| Invoice 2 | Set by `FinishCounting` | **Inferred** |
| Invoice 3 | Required before the invoice appears on the bagging list | **Inferred**. The writer that moves 2 to 3 was not found. **Unknown / Requires Confirmation** |
| Dispatch 1 then 2 | `CreateDispatch` inserts 1 and updates the new row to 2 | **Inferred** |
| Dispatch 3 | `DispatchForm` sets 3 and comments "Out for delivery" | **Inferred** |

### BR-016 — A workload that is finished can still be edited by an administrator

**Documented.** The counting list includes status 4 for that reason. `UpdateWorkloadItem` accepts status 1 or 4.

### BR-017 — A reopened count must leave an audit row and must fill missing invoice lines

**Documented.** On close after a status-4 session the POS calls `SyncInvoiceItemsFromWorkload` and `InsertPostCountEditAudit` with summary "Post-count adjustments".

One audit row per edit session: workload, operator, authorising manager login, timestamp, one-line summary.

### BR-018 — Finishing a count with no invoice lines is refused

**Inferred.** The station shows "It doesn't look like you have started counting..." and does not close.

## Invoices and bags

### BR-019 — One counting invoice per workload

**Documented** in `scope/IntelTag_MultiPrelist_What_Changed.md` and implemented in `GenerateInvoiceFromWorkloadCounts`. The procedure inserts only when the workload has no invoice row.

If historical delivery invoices still share that workload id, the `SELECT *` can return more than one row. The current rule is that those rows should already have been removed.

### BR-020 — A bag may contain several prelists of one customer

**Documented** and implemented. `CreateDispatch` accepts `@InvoiceIds >= 1` and requires `@CustomerIds = 1`.

### BR-021 — A bag may not mix customers

**Documented** in the verification note and implemented by the customer-count predicate. The result is an empty procedure result and no dispatch row.

### BR-022 — A package cannot be sealed twice

**Inferred.** `CreateDispatch` requires `@DispatchedPackageIds = 0`, counting existing `DispatchPackages` for the supplied package ids.

### BR-023 — The dispatch's invoice id is not the bag contents

**Documented.** `Dispatches.InvoiceId` is `MIN` of the counting invoices on the packages. Callers must use `DispatchPackages` to list prelists. The prelist number is the trimmed `Workloads.Reference`, or the workload id when the reference is blank.

Evidence: `vwBagPrelistContents`.

### BR-024 — Sealing moves every prelist on the bag to workload status 5

**Documented** in the IntelTag note and implemented in `CreateDispatch`.

### BR-025 — Do not create a second invoice at seal

**Documented.** `Phase2_MultiPrelistBag.sql` does not create `IsDeliveryInvoice`, `BagPrelistLink`, `DispatchInvoiceLink`, or `GenerateDeliveryInvoicesForBagSeal`. `scope/Why_The_Delivery_Invoice_Exists.md` says those must not be recreated.

The holdback pseudocode describes the opposite schema. That pseudocode is not the current rule.

### BR-026 — Fully bagged detection uses packages, not `Dispatches.InvoiceId`

**Documented.** A counting invoice leaves the bagging list when its packages are on `DispatchPackages`. Joining only through `Dispatches.InvoiceId` hides or keeps the wrong rows once a bag holds more than one prelist.

### BR-027 — One print row per delivery

**Documented.** `GetDispatchesReadyToPrint` groups dispatches by `DeliveryId` and keeps `MIN(InvoiceId)` plus `MAX(DateDelivered)`.

### BR-028 — First print versus reprint

**Documented** in `phase2-multi-prelist-verification.md`.

- First print of a delivery is a tax invoice, and every counting invoice on that delivery becomes `Printed = 1`.
- Reprint is a copy tax invoice.
- A one-prelist slip shows the receiving `DateCreated`.

The note says this was checked in SQL and a service harness, not by opening the dispatch form. **Requires Confirmation** on a till.

### BR-029 — Tax invoice grouping

**Documented** and implemented on the website.

- Group by category: Laundry, Linen, Food and Bev.
- A line is included in those groups only when `Cost` has a value.
- The same category, item type, size, and cost merge.
- Tax invoices do not use `Pre List #` as a section header.
- When a delivery spans prelists, the web invoice can still section the data by prelist (`bdfae9e`) while category headers remain.

### BR-030 — Bag slips group by prelist

**Documented.** Bag slips keep `Pre List #`. Views `vwBagPrelistContents` and `vwDeliveryNoteByPrelistBagSeal` expose one prelist identity per workload on the bag, plus categories on that prelist.

## Holdback

### BR-031 — Holdback demand is newest open prelist first

**Documented** in `NewKnowledgeBase.md` and implemented in `HoldbackService`.

### BR-032 — Holdback cannot pack more than the line still needs

**Documented** and implemented. Outstanding quantity is invoice item count minus clean, stain, and defective quantities already packed.

### BR-033 — On-hand lines merge when type, size, and condition match

**Inferred** from `AggregateOnHand`. Quantity must be greater than zero. Type, size, and condition are required. Customer is required before a plan is built.

### BR-034 — Defective holdback uses the pack defective path

**Documented.** After `CreatePackage`, set the defective count and write `QualityEvents` the same way packing does.

### BR-035 — Conditions are Clean, Stain, and Defective

**Documented** in the changelog description of the on-hand station.

## Pricing, reports, and references

### BR-036 — Order reference is the workload reference

**Documented.** The report uses `Workloads.Reference`, `Workloads.DateCreated`, and `OperatorLogins.Username`. It does not use `CustomerWorkloadBags.UniqueReference` or `BagReferences.UniqueReference`.

Blank references are excluded from that report.

### BR-037 — Reference search ignores customer and dates when text is entered

**Documented.** Non-empty text is a case-insensitive substring match across all customers and dates, and only for the Order Reference Report. Empty text uses the customer and the start and end dates. Customer id 0 means all customers on the filtered reports.

### BR-038 — Count cost comes from the receiving cost procedure

**Documented** by the script header. `GetItemCostForReceiving` is called by Inteltag when an item is scanned at count. The rate card itself lives in the database. The Price Increase screen can change prices. The increase formula was not extracted. **Unknown** beyond the screen existing.

## Email

### BR-039 — Email only when there is something to report

**Inferred.** Count-recon mail sends only when `vwInvoiceCountsRecons` has rows for the workload. Condemned mail sends only when `vwInvoiceCountsCondemneds` has rows. Both can send in one finish.

### BR-040 — Three customer names receive the customer address

**Inferred** from a 2016 comment that is still the branch condition. If `Customers.Name` is `S.S.W.F`, `Capital Mirage Hotel`, or `The Stack`, the primary customer email is the To address. Every other active customer sends To a hardcoded internal address. The comment marks that branch "dev".

CC addresses come from `GetCustomerCCEmail`, split on `;`.

BCC always adds two hardcoded addresses.

**Requires Confirmation** that the three names and the fallback are still the business rule.

### BR-041 — Email copy is configuration

**Inferred.** Subject and body text are settings rows, not hardcoded sentences, except the HTML table built around them. Tokens `[WorkOrderID]` and `[Items]` are replaced. Work order id is the workload reference or, if blank, the workload id.

From address is `Company Name` and `Company Email` settings.

## Pastel

### BR-042 — Export is by delivery, not by the old invoice call

**Inferred.** `Export.aspx.cs` constructs `PastelExport` with delivery id, date delivered, customer id, reference, and document number. An older invoice-based constructor call is commented out.

### BR-043 — Pastel customer code is `Customers.PastelCode`

**Inferred.** The website label is "Pastel Reference".

### BR-044 — File shape is mixed

**Inferred.** `PastelExport` joins some rows with commas and some with tabs. Values inside the reference and wash-type fields are joined with `"; "`. A commit subject that says the delimiter became semicolons does not match this file.

### BR-045 — Dispatch barcodes carry the Pastel code; invoice barcodes do not

**Documented** by the 2025-03-18 comment in `Dispatch.aspx.cs`.

## Inteltag

### BR-046 — Procedure names and parameters stay stable

**Documented.** `scope/IntelTag_MultiPrelist_What_Changed.md` says Inteltag keeps calling the procedures from the 8 April 2025 integration document with the same parameters. That document is not in the repository.

### BR-047 — Inteltag must not assume one invoice per bag

**Documented.** Several counting invoices may share one dispatch. `Dispatches.InvoiceId` is representative only.

## Rules that look like defects

These are implemented. They are not confirmed as intended policy.

| Rule | Why it needs a person |
|---|---|
| BR-010 private-network auto login | Issues a real forms cookie without a password |
| BR-011 operator id 2 | Admin actions may be recorded as the wrong person |
| BR-005 versus BR-003 | Administrator is a name in one screen and an id in another |
| BR-040 fallback labelled "dev" | Customers other than three named accounts may never receive count email |
| Hardcoded SMTP secret | A mailbox password is compiled into the POS. Not a business rule. Rotate it. The value is omitted here |

## Unknown

- Names for workload statuses 1, 2, 3, and 6, and for invoice statuses other than the ids above.
- The process that sets invoice status from 2 to 3.
- Whether `SwapSeal` has business conditions beyond the script existing.
- Price increase percentages, rounding, and effective dates.
- Whether condemned reversal (role id 3) is still offered on the live count screen.
- Production confirmation of the delivery-invoice cleanup.
