# Business rules — bigpond_frontend_qa

Evidence date: 2026-10-19.

Source: `git@bitbucket.org:overdrive/bigpond_frontend_qa.git`, `main` at `6b421ff`.

Rules are what the code enforces. A rule is **Inferred** when the code behaves that way and no comment or document states it as policy. A comment or README sentence that states the rule is marked **Documented** even when the only place it is written is source.

This file does not copy secrets, tokens, or key material.

## Identity and access

### BR-001 — People sign in with Cognito email; the app then requires a MySQL user

**Implemented.** Login is Amplify Authenticator. `checkUser` calls `/initUser` with the Cognito email. The SQL is `select ... from users left join company ... where email = ?`.

**Inferred.** A Cognito account with no `users` row cannot pass the role gate. The handler throws when `userRows[0]` is missing, and the router keeps showing `Loader`.

### BR-002 — Self-registration is off

**Implemented.** `hideSignUp={true}` on the login Authenticator.

**Inferred.** New people are created by an admin through `AdminCreateUser`, not by the public form.

### BR-003 — Inactive users are blocked after login

**Implemented.** `currentUser.active === 0` renders the inactive page and no routes. Copy: “Your User is Inactive, Please Contact Your Administrator”.

`active` is stored as 1 or 0. The client list in context keeps only `row.active === 1`.

### BR-004 — Only role 1 and role 3 get a workspace

**Implemented.** `role_id === 1` is the admin shell (default `/companies`). `role_id === 3` is the user shell (default `/clients`). Any other value, including the initial empty string, renders `Loader`.

**Documented** in the add-user form: option value 3 is labelled User, option value 1 is labelled Admin.

**Unknown / Requires Confirmation.** The meaning of any other `user_role` id. Write APIs for roles exist and no screen was found.

### BR-005 — Admin sees every firm; a user is scoped in the UI

**Implemented in the UI.**

- Users list: role 1 calls `/getusers`; any other role would call `/getusersbycompany/{company_id}`.
- Companies list for dropdowns: role 1 calls `/getcompany`; others call `/getcompanybyid/{company_id}`.
- Clients: role 1 uses the unscoped client list in `clietTable.jsx`.

**Implemented on the server:** those queries do not check the caller’s role. A caller who can hit the API can request another company’s id.

**Inferred.** Firm isolation is a front-end choice, not a database rule.

### BR-006 — Cognito group follows the role string

**Implemented.** `signUp` sets group `Admin` when `role_id === "1"`, otherwise `User`.

**Inferred.** A numeric `1` from a `<select>` becomes the string `"1"` in the DOM, so the Admin group is applied for the admin option. Any other value, including an empty role, becomes group `User`.

### BR-007 — New single users start on Expense Document

**Implemented.** `/adduser` and `/bulkaddusers` insert `workflow_type` `"Expense Document"`.

### BR-008 — Bulk import forces User and Active

**Documented** in `CHANGELOG.md` and **Implemented** in `bulkImportUser.jsx`: `role_id = 3`, `active = 1`, CSV columns `company_name,fullname,email`.

The company name must match a company the caller can list. Rows that fail the insert are counted in `failed` and do not roll back earlier rows. **Implemented.** There is no transaction around the loop.

**Inferred.** Bulk import does not call `signUp`, so it does not create the Cognito user.

### BR-009 — User create checks name and email in the browser

**Implemented.** Full name must be non-empty. Email must match `/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i`. Admin must have `companyid` set. The API insert does not repeat these checks.

The TLD is limited to 2–4 letters. **Inferred.** Addresses with longer public suffixes fail the form.

### BR-010 — Duplicate Cognito email is rejected with a fixed message

**Implemented.** `UsernameExistsException` shows “Email Already Exists, Please use a different Email.”

### BR-011 — Password policy is the imported pool’s policy

**Documented** in `amplify/backend/auth/bigpond10dd5625/parameters.json`: minimum length 8, lowercase, uppercase, numbers, symbols. MFA `OFF`. Username attribute email. Auto-verified attribute email.

The add-user success text says the person must use the email verification code and finish signup. **Documented** in `addUser.jsx`.

## Firms and clients

### BR-012 — A firm is a company row with a name and an active flag

**Implemented.** Insert and update write `name` and `active`. Delete is `DELETE FROM company`.

**Inferred.** There is no soft-delete. Child clients and users are not removed in the same statement. Foreign-key behaviour is **Unknown** (no migration in the repo).

### BR-013 — A client belongs to one firm and has a contact

**Implemented.** `company_client` columns written by the app: `company_id`, `name`, `contact_email`, `contact_name`, `active`.

**Implemented** in `addClient.jsx`: name required, contact name required, email must pass the same regex as BR-009. The form’s initial `active` is false.

### BR-014 — Only active clients are offered on Xero Testing

**Implemented.** `getClients` filters `row.active && row.active === 1`.

### BR-015 — Deactivating a client tries to drop the Xero connection

**Implemented.** If `active === 0` on update, the handler reads `xero_token` and `connection_id` and sends `DELETE https://api.xero.com/connections/{connection_id}` with `Authorization: Bearer {xero_token}`.

**Inferred.** Failure of that HTTP call does not fail the client update. The `catch` only logs.

### BR-016 — Xero tokens are stored on the client

**Implemented.** `/updatecompanyxero` accepts `xero_token`, `xero_refresh_token`, `xero_tenant`, `connection_id`, `clientContactEmail`. The callback page supplies them after the code exchange.

Redirect URI in `XeroExpress` is `https://www.auditwhizz.net/callback`. **Implemented.**

### BR-017 — Changing client or workflow with files present asks first

**Implemented.** `mainDropDown.jsx` opens a modal instead of saving when Excel, PDF, or CSV arrays are non-empty.

What the confirm action does to existing S3 objects was not fully traced. **Unknown** whether files are deleted or only the selected ids change.

## Xero Testing files

### BR-018 — Expense and Revenue do not require the same files

**Implemented.**

- Expense Document: generate controls need at least one `.xlsx`, one invoice file, and one `.csv`.
- Revenue Document: generate controls need at least one `.xlsx` and one `.csv`. Invoice upload is not rendered.

Messages are the literal strings “Excel file(s) required.”, “PDF file(s) required.”, “CSV file(s) required.” concatenated when the matching list is empty (`getExplorerMsg.js`).

### BR-019 — Upload counts are capped in the widget

**Implemented.** Excel `maxFileCount` 1. Invoice files and CSV `maxFileCount` 50. Excel accepts `.xlsx` only. Invoices accept `.pdf`, `.png`, `.jpeg`, `.tiff`. Bank file accepts `.csv`.

### BR-020 — Expense generate waits for a JSON file per PDF

**Implemented.** `canGenerateWorkFlow` sets ready only when workflow is Expense Document, at least one xlsx, at least one pdf, and `newJsonfiles.length >= newPdfFiles.length`.

**Inferred.** Each invoice is expected to gain a JSON sidecar before Generate enables. The producer of those JSON files is the processing Lambda, not this function.

### BR-021 — Excel processing is polled, not pushed

**Implemented.** After Excel upload the UI calls `/getuseruploadprocess/{id}` every 5 seconds until `xlsx_process === 1`, then calls `/updateuseruploadprocess/{id}` and refreshes files.

`/updateuseruploadprocess` is also called from `checkUser` on every login. **Inferred.** Login clears or rewrites the same flag the poll waits on. The SQL was not copied here; confirm the flag’s meaning before relying on it.

### BR-022 — CSV refresh is a fixed wait

**Implemented.** `loadCsv` waits 10 seconds and then refreshes the file list. It does not poll a status column.

### BR-023 — The workbook name includes workflow, client, and year

**Implemented.** `{first word of workflow_type} Testing {current_client_name} {getFullYear()}.xlsx`.

**Inferred.** “Expense Document” becomes “Expense Testing …”. “Revenue Document” becomes “Revenue Testing …”.

### BR-024 — Zip download is expense-only

**Implemented.** The zip button renders only when `workflow_type === "Expense Document"`, and only after generate has succeeded (`generated` true).

### BR-025 — Bank CSV is labelled with the selected bank number

**Implemented.** The CSV header appends the current bank number. The dropdown options are distinct `bank_number` values from `master_table` via `/getsessionbanknumbers/{sessionid}`. Selection posts `/updateuserselectedbank`.

## Workpapers

### BR-026 — A flow saved to the firm has a unique name and no files

**Implemented.** Save alerts and returns if the name already exists in the loaded list. Each step is stored with `files: []`. Database row `type` is `"flow"`, `active` is 1, `company_id` is the signed-in user’s company.

Load ignores rows whose `type` is not `"flow"`.

**Inferred.** File bytes are not part of the saved template. Re-opening a template starts with empty uploads.

### BR-027 — Local and database flows merge by name

**Implemented.** On load, local `custom_match_custom_flows` and database flows are put in a `Map` keyed by `name`. The database pass runs second, so a database flow replaces a local flow with the same name.

### BR-028 — Document types start with fixed fields

**Implemented** in `DOCUMENT_FIELDS` (`constants.js`):

- Invoices: Company Name, Date, Invoice Number, Amount, GST%, Total.
- Statement: Company Name, Invoice Number, Amount, Date, Open Balance, Closing Balance, Transaction Description, Debit, Credit, Balance.
- Supporting Document: Company Name, Date, Amount.

Custom steps do not get that starter list. **Implemented** (`DOCUMENT_FIELDS` has no Custom key).

### BR-029 — Matching instructions require amount, date, and company together

**Documented** in `buildPrompt.js` and sent to the model:

- Amount matches exactly, or is a clear partial payment.
- Date matches exactly or closely.
- Company name is the same business entity.
- If any of the three fails, the record is excluded.
- Prefer no records over a weak match.
- Dates as `dd/mm/yyyy`. Currency as `xxx,xxx.xx` without symbols. Nulls as `-`. Do not round.
- Response is JSON `{ "records": [ ... ] }` only.

**Inferred.** This is instruction text. It is not a deterministic matcher in application code.

### BR-030 — Confidence is banded for display

**Implemented.** 7–10 high, 4–6 medium, 0–3 low, with the colours in `CONFIDENCE_LEVELS`.

The prompt text says 10 is a perfect match on amount, date, and description (`buildPrompt.js`). **Documented.** How the numeric score is parsed from the model into those bands was not fully traced.

### BR-031 — Field type can be forced by the column name

**Documented** in `CHANGELOG.md` and **Implemented** as the functions named there in `utils/formatters.js` (`detectFieldTypeFromData` and the `isDateValue` / `isDecimalValue` / `isNumericValue` helpers).

The changelog states the priority: field-name patterns, then strong data patterns, then a fallback sample of up to 10 non-empty values. Date-like names stay DATE. Currency-like names (debit, credit, balance, amount, total) stay CURRENCY. Identifier-like names stay TEXT even when the values are numeric.

### BR-032 — Large PDFs can be split

**Documented** in `CHANGELOG.md`: when `isMultiRow` is true and `pdfPages > 3`, process in batches of 3 pages and merge records.

`StepCard.jsx` sets `largeDocument` when `pdfPages > 3 && step.onlyRelevantMatches && step.isMultiRow`.

**Requires confirmation** that the Lambda batch size is still 3. The changelog is the written rule; the Lambda body was not line-audited for the constant.

### BR-033 — localStorage writes for match keys are serialised

**Documented** in `README.md` and **Implemented** in `safeLocalStorage.js`. Queued keys: `custom_match_page`, `result_table_db`, `match_table_db`, `match_results`.

README also states a 5MB maximum, 2-day flow persistence, 30-day temporary tables, 300ms debounce, and version `v2.1` under `customFlowData`. Those figures were **not** confirmed in `customMatchPage` constants. Treat them as **Documented** for the removed custom-flow design until confirmed in the current page. **Requires confirmation.**

### BR-034 — Workpaper zip replaces the previous zip

**Documented** in `CHANGELOG.md`: previous zip files are removed before a new one is written; the archive includes step folders and `Results.xlsx`; the response is a signed download URL.

**Implemented** as `ZipWorkPapers`. Empty result returns 404 “No workpapers or results found for this user”.

## Contact and mail

### BR-035 — Contact form fields are all required

**Implemented.** Name, email (same style of regex), and message must be non-empty after trim. The request goes to `/contactmail`.

### BR-036 — Contact mail has one recipient

**Implemented.** SES `ToAddresses` is `bhawin@auditwhizz.net`. Region `ap-southeast-2`.

Whether that mailbox is still the right destination is **Unknown / Requires Confirmation**.

### BR-037 — Browser origins are allow-listed

**Implemented** on the Lambdas inspected (`contactmail`, `XeroExpress`, `frontendLogs`, `manageFileProcessTable`, `openaistart`): `https://www.auditwhizz.net` and `http://localhost:3000`.

`frontendLogs` returns 403 (body “Access Dinied”) for any other origin. `contactmail` returns 403 “Access Denied” on preflight from other origins.

## Data integrity notes (not approved policy)

### BR-038 — Several deletes are permanent

**Implemented.** User, company, client, and custom-match flow deletes are SQL `DELETE`, not an `active` flag.

### BR-039 — Session table names are derived from the identity id

**Implemented.** `s3_session` is the identity id segment with hyphens turned into underscores, and that string is interpolated into `CREATE TABLE master_table_...`.

**Inferred.** A session id that is not a safe identifier would change the SQL. The value comes from Cognito’s identity id shape (`region:uuid`), not from free text.

### BR-040 — API role checks are not in `bigpondexpress`

**Implemented.** Handlers run the SQL they are given. They do not read a role claim.

Combined with guest entries in `apibigpond/cli-inputs.json`, this is a security behaviour, not a product rule that guests should manage users. **Requires confirmation** before anyone treats guest access as intended.
