# PRD — bigpond_frontend_qa

Evidence date: 2026-10-19.

Source: `git@bitbucket.org:overdrive/bigpond_frontend_qa.git`, branch `main` at `6b421ff` (2025-07-21, `init`).

Provenance labels:

- **Documented** — stated in `README.md`, `CHANGELOG.md`, `NewKnowledgeBase.md`, or a comment that describes intended behaviour.
- **Implemented** — present in code on this commit.
- **Inferred** — reasonably derived from code, not written as a requirement.
- **Planned** — described as future work, or left commented as a route to turn on.
- **Unknown** — not established.

There is no PRD in the source repository. `README.md` describes “BigPond” as a financial document platform. The landing page calls the product Audit Whizz. Both are recorded. Where they disagree, both statements are kept and the code is the implemented behaviour.

## Product purpose

**Documented** (`src/components/welcome/index.jsx`): Audit Whizz analyses accounting data and supporting evidence and generates audit workpapers so staff spend less time on repetitive matching.

**Documented** (`README.md`): BigPond manages, analyses, and reconciles financial documents, with custom flows, AI matching, upload, and export.

**Inferred.** An audit firm (called a company in the database and “Audit Firms” in the admin nav) has clients. Staff either run a fixed Xero Testing upload (Excel transactions, optional invoice images, bank CSV) or build a Workpapers flow that extracts fields from statements, invoices, and supporting documents and matches them to the previous step.

## Users and roles

| Actor | What the code allows | Provenance |
|---|---|---|
| Anonymous visitor | Landing page, Login button, Contact Us form | Implemented |
| Cognito user who is not in `users`, or whose `initUser` call fails | Stays on the full-page loader. `currentUser.role_id` stays `""` | Implemented. `src/router/index.jsx` only renders app shells for role 1 and 3 |
| Inactive user (`users.active === 0`) | “Your User is Inactive, Please Contact Your Administrator” | Implemented. `src/components/inactivePage` |
| Admin (`users.role_id === 1`) | Audit Firms, Clients, Users, Xero Testing, Workpapers. Default route `/companies`. Cognito group `Admin` when created from the UI with role `"1"` | Implemented |
| User (`users.role_id === 3`) | Clients, Xero Testing, Workpapers. Default route `/clients`. Cognito group `User` for any role other than the string `"1"` | Implemented |
| Any other `role_id` (including 2) | Loader only. No nav | Implemented. Whether role 2 exists in `user_role` is **Unknown** |
| Public self-signup | Hidden. `Authenticator hideSignUp={true}` | Implemented |

`src/pages/auditPage` is routed for both roles and renders the heading “Audit Page”. It is not in the sidebar.

## Systems the product talks to

1. The browser.
2. Amazon Cognito in `ap-southeast-2`. Username attribute is email. The pool is an imported Amplify auth resource (`bigpond10dd5625`). Password policy in Amplify parameters: length 8, lower, upper, number, symbol. MFA is off.
3. API Gateway APIs `apibigpond`, `apibigpondfiles`, `manageFileProcessTable`, `tempFileProceesTable`.
4. MySQL. Lambdas read host, port, and database name from SSM `rdsDetails` and the password from Secrets Manager. `processUnprocessedFiles` uses secret `DatabaseCredentials` instead.
5. S3. Amplify storage resource `bigpondpuloads`. Workbook download hardcodes bucket `big-pond-uploads-bucket123209-dev`. `generatefile` also names bucket `bigpond-uploads`.
6. OpenAI chat completions.
7. Xero: OAuth redirect `https://www.auditwhizz.net/callback`, connections API, tenant lookup.
8. Amazon SES in `ap-southeast-2` for the contact form and Xero emails.
9. CloudWatch via `frontendLogs`, which prints the error payload and returns `"Logs created"`. It does not write a database row.

## Major workflows

### WF-1 — Read the public site and send a message

Trigger: visitor opens `/`.

`LandingPage` renders navigation, the Audit Whizz welcome, and a footer.

LOGIN goes to `/login`.

CONTACT US opens `ContactForm`. Required fields: name, email (regex), message. Submit `POST`s the Amplify API path `/contactmail`. `contactmail` sends SES mail to `bhawin@auditwhizz.net` with the name, email, and message. Success UI closes the modal after 5 seconds.

CORS allow-list on that Lambda: `https://www.auditwhizz.net` and `http://localhost:3000`. Any other origin gets 403.

Provenance: **Implemented**. The marketing sentences are **Documented** in the component.

### WF-2 — Sign in

Trigger: visitor opens `/login`.

Amplify Authenticator, sign-up hidden. On route `authenticated`, the app navigates to `location.state.from.pathname` or `/companies`.

`BigPondRouter` then calls `checkUser`:

1. Read Amplify auth. Session id is the identity id after `:`, with `-` replaced by `_`.
2. `GET /initUser/{email, cognito_uuid, s3_session}`.
3. The Lambda selects `users` joined to `company` by email, writes `cognito_uuid` and `s3_session`, and `CREATE TABLE IF NOT EXISTS master_table_{s3_session}` with the bank, invoice, receipt, and textract columns listed in `bigpondexpress/src/app.js`.
4. If a row returns, the client posts `/updateuseruploadprocess/{id}`.

If `active === 0`, the inactive page replaces the app. Role 1 and role 3 get different route sets. Everyone else sees `Loader`.

Provenance: **Implemented**.

Failure: `initUser` with no matching email throws when it reads `userRows[0]` (`setErrorRes`). The UI then has no role and keeps loading. **Inferred.**

### WF-3 — Admin maintains audit firms

Trigger: Admin opens `/companies`.

`GET /getcompany` lists `id, name, active` and audit columns. Add posts `/addcompany/{name, active, userid}`. Update posts `/updatecompany`. Delete posts `/deletecompanybyid/{id}` (hard delete).

`GET /getactivecompanies` exists for `active = 1`. The client list used when adding users filters `active === 1` in the browser.

Provenance: **Implemented**. The nav label “Audit Firms” is **Documented** in `NavAdmin.jsx`. The table name remains `company`.

### WF-4 — Maintain clients and connect Xero

Trigger: Admin or User opens `/clients`.

List: Admin calls `GET /getcompanyclients` (all firms). A non-admin path in `clietTable.jsx` uses the company-scoped query. **Inferred** from the role check in that file. The router does give role 3 the clients page.

Add client (`addClient.jsx`) requires name, contact name, and an email matching the same regex as user create. Insert columns: `company_id, name, contact_email, contact_name, active`. New clients default `active: false` in the form initial state.

Update can set `active` to 0. The API then reads `xero_token` and `connection_id` and `DELETE`s `https://api.xero.com/connections/{connection_id}`. A Xero error is logged and the SQL update still returns success.

A table button calls `sendXeroUrl`, which hits the Xero Express routes (`/sendurl/:companyid` and related). The email Lambda is `sendxeroemail`. The exact mail template was not fully transcribed; the function exists and uses SES.

Callback (`/callback`, available signed-in and signed-out):

1. Read `code` (and `scope`, `session_state`) from the query string.
2. `GET /getcompanyclientsid/{id}` then `/clientdetails/{company_id}` for `client_id` and `client_secret`.
3. `GET /xerotoken/...` then `/xerotenants/{access_token}`.
4. Decode the access token and `POST /updatecompanyxero` with token, refresh token, tenant, connection id, and contact email.

On failure the page navigates to `/error`, which tells the user to try later and returns home.

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

### WF-5 — Admin maintains users

Trigger: Admin opens `/users`. Role 3 has no `/users` route.

List: role 1 uses `GET /getusers` (all users). The table code would call `/getusersbycompany/{company_id}` for any other role.

Add user (`addUser.jsx`):

- Full name required.
- Email must match `/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i`.
- Admin must pick a company. The company `<select>` value is bound to `company_id` while `onChange` writes `companyid`. **Inferred** defect: the control’s displayed value and the submitted field are different properties.
- Admin must pick role User (`3`) or Admin (`1`). Non-admin company users never see the role control; they also cannot open this page.
- Optional “Is Active?”. Checked maps to `active` 1.
- `signUp` calls Cognito `AdminCreateUser` and `AdminAddUserToGroup` in `ap-southeast-2`. Group is `Admin` only when `role_id === "1"`, otherwise `User`. Desired delivery medium is EMAIL.
- On `"success"`, `POST /adduser` inserts `users` with `workflow_type` `"Expense Document"`.
- Cognito `UsernameExistsException` shows “Email Already Exists”. `InvalidParameterException` names Password or Username from the error text.
- Success copy tells the admin the person must read the verification email, log in, and finish signup. The modal waits 10 seconds and then closes.

Bulk import (`bulkImportUser.jsx`, button “BULK IMPORT”):

- CSV columns `company_name,fullname,email` (**Documented** in `CHANGELOG.md` and implemented in the modal).
- Each row is forced to `role_id` 3 and `active` 1.
- Company name is resolved to an id against companies the caller can list.
- Preview, then `POST /bulkaddusers` with `{ users: [...] }`.
- The API inserts the same shape as `/adduser`, counts `successful` and `failed`, and returns per-email errors. It does not create Cognito users. **Inferred:** a bulk-imported person has a MySQL row and still needs a Cognito account before login works. **Requires confirmation** that this split is intended.

Delete is `POST /deleteuserbyid/{id}` (hard delete). Update is `POST /updateuser` for name, email, role, active, and company.

Provenance: **Implemented**, except the Cognito gap on bulk import, which is **Inferred**.

### WF-6 — Xero Testing workbook

Trigger: Admin or User opens `/workflow` (“Xero Testing”).

The screen loads clients for `currentUser.company_id` where `active === 1`.

The user picks:

- Client. `POST /updateuserclient/{id, client_id}`. Changing client or workflow while files are already uploaded opens a confirm modal instead of saving immediately.
- Workflow type: `Expense Document` or `Revenue Document`. Default on insert is `Expense Document`. `POST /updateuserworkflowtype`.

Uploads use Amplify Storage, access level `private`:

| Step | Expense Document | Revenue Document |
|---|---|---|
| Excel transactions | `.xlsx`, max 1 | same |
| Invoice images | `.pdf`, `.png`, `.jpeg`, `.tiff`, max 50 | not shown |
| Bank CSV | `.csv`, max 50, plus a bank-number dropdown from `GET /getsessionbanknumbers/{sessionid}` | same, numbered as step 3 |

After an Excel upload the UI polls `GET /getuseruploadprocess/{id}` every 5 seconds until `xlsx_process === 1`, then resets that flag and refreshes S3 listings. CSV upload waits 10 seconds and refreshes. There is no max poll count in `WorkflowUploads.jsx`.

Generate buttons appear when Expense has at least one Excel, one PDF, and one CSV, or when Revenue has at least one Excel and one CSV (`getExplorerMsg.js`).

`canGenerateWorkFlow` is stricter for Expense: it stays false until `newJsonfiles.length >= newPdfFiles.length`. Revenue sets `canGenerate` true whenever the Expense condition is false, including when files are missing. The buttons are still hidden until `showButtons` is true. **Inferred** interaction of those two flags: the visitor cannot click Generate until the file counts pass `getExplorerMsg`, and for Expense the click stays disabled until JSON sidecars catch up with PDFs.

Generate calls `GET /generatefile/{identityId}`. The Lambda reads the session master table and writes an Excel workbook. The UI treats the body string `"File generated"` as success.

Download builds the key:

`{Expense|Revenue} Testing {current_client_name} {year}.xlsx`

under `private/{identityId}/` in bucket `big-pond-uploads-bucket123209-dev`, then a signed URL.

Expense only: “DOWNLOAD ZIP FILE” calls `ZipfileWithSignedUrl` (`ZipObjects`).

`POST /deletemastersessiontable/{sessionid}` and `/clearmastersessiontable/{sessionid}` exist. When the UI calls them was not traced end to end. **Unknown** which button clears a session.

Provenance: **Implemented**.

### WF-7 — Workpapers (custom match)

Trigger: Admin or User opens `/custommatch`.

On load, `GET /getcustommatchflowsbycompanyid/{company_id}`. Rows with `type === "flow"` are parsed from `json_value`. Saved flow definitions are stored with empty `files` arrays. Local flows in `localStorage` key `custom_match_custom_flows` are merged by name; the later entry wins.

The user adds steps. Default step: empty description, type “Please Select Document Type”, no fields, no files, `isMultiRow` false, `isExcelDocument` false (`constants.js`).

Document types and their starter fields:

- 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: user-defined fields.

Field types: text, date, currency, percentage, number.

Accepted files: PDF (`.pdf`) and Excel (`.xlsx`, `.xls`).

Processing is implemented in `src/pages/customMatchPage/services/fileProcessing` (S3 upload, page images, Excel export) and in Lambda `openAIFileProcessing`. Prompts in `buildPrompt.js`:

- Extract the fields the step asks for.
- Match to the previous step only when amount, date, and company name all line up (exact amount or a clear partial payment, exact or close date, same business). The prompt says to drop the record if any of the three fail, and to prefer no rows over a weak match.
- Large multi-page PDFs (`pdfPages > 3` and `isMultiRow` and `onlyRelevantMatches`) use `buildLargeDocumentPrompt`. `CHANGELOG.md` says batches of 3 pages. That batch size is **Documented** in the changelog; confirm against `openAIFileProcessing` before treating 3 as a fixed rule. The index file sets model `gpt-4.1`.

Confidence bands in the UI (`CONFIDENCE_LEVELS`): 7–10 high (green), 4–6 medium (amber), 0–3 low (red). The prompt’s “10 = perfect match” scale is **Documented** in `buildPrompt.js`.

Results open in a drawer (`ResultsTable`) backed by DataTables (copy, CSV, Excel, PDF) and by localStorage keys `custom_match_page`, `result_table_db`, `match_table_db`, `match_results`. Writes to those keys go through a per-key queue (`safeLocalStorage.js`) so concurrent updates do not clobber each other. **Documented** in `README.md` and **Implemented**.

Save flow: name must be non-empty and must not already exist in the in-memory list (browser `alert`). `POST /addcustommatchflow` with `type: "flow"`, `active: 1`, `companyid`, and `json_value` of the steps without files. Update and delete routes exist (`/updatecustommatchflow`, `/deletecustommatchflowbyid`).

`ZipWorkPapers` builds a zip of that user’s S3 workpapers, keeps `Step{n}-{description}/` folders, adds `Results.xlsx`, deletes the previous zip, and returns a signed URL. The results table exposes “Download ZIP” (**Documented** in `CHANGELOG.md`).

`workpaper_logs` records OpenAI calls (user, firm, operation, document, tokens, model, duration, success). **Documented** in `NewKnowledgeBase.md`. The `CREATE TABLE` is not in the repo. **Unknown** whether the table already exists in MySQL.

Provenance: **Implemented** unless marked Documented or Unknown.

### WF-8 — Scheduled file processing

`processUnprocessedFiles` is scheduled `rate(5 minutes)` (`function-parameters.json`). It loads `DatabaseCredentials` and `OpenAIKey`, reads a file from `process.env.S3_BUCKET`, and calls `gpt-4o`. First step summarises PDF or Excel. Later steps compare with the previous result.

`openAIFileProcessing` also has a schedule `rate(1 minute)` in its CloudFormation template. `openaistart` invokes Lambda `openAIFileProcessing-dev`.

Provenance: **Implemented** as infrastructure. Whether both schedules are enabled in the deployed stack is **Unknown / Requires Confirmation**.

## Functional requirements

### FR-001 — Public landing

The unauthenticated site shows the Audit Whizz welcome, a LOGIN control, and a CONTACT US control.

Evidence: `welcome/index.jsx`, `router/basic/index.jsx`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Contact message

A visitor can send name, email, and message. Empty fields and a failed email regex block submit. A successful call sends mail to `bhawin@auditwhizz.net`.

Evidence: `contactForm/index.jsx`, `amplify/backend/function/contactmail/src/index.js`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Sign-in only

The login screen does not offer self-registration.

Evidence: `components/login/index.jsx` `hideSignUp`.

Implementation: Implemented.

Confidence: High.

### FR-004 — App access requires a MySQL user and an active flag

After Cognito succeeds, the shell depends on `users` for that email. `active === 0` shows the inactive page. Role 1 and role 3 see their nav. Other values see a loader.

Evidence: `authContext.jsx`, `router/index.jsx`, `inactivePage`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Per-login session table

`initUser` creates `master_table_{session}` if it does not exist and stores the Cognito identity and session on the user.

Evidence: `bigpondexpress/src/app.js` `/initUser`.

Implementation: Implemented.

Confidence: High.

### FR-006 — Admin manages firms, clients, and users

Role 1 can list, add, update, and delete companies, clients, and users. Role 3 can open clients and cannot open `/companies` or `/users`.

Evidence: `router/index.jsx`, `NavAdmin.jsx`, `NavUser.jsx`, `bigpondexpress` routes.

Implementation: Implemented. API Gateway also allows guest calls; see FR-016.

Confidence: High for the UI. Medium for “the API rejects a non-admin”, because the Lambda does not check role.

### FR-007 — New users are Cognito users in a group

Single-user create calls `AdminCreateUser` and adds group `Admin` or `User`, then inserts MySQL with workflow type Expense Document.

Evidence: `util/signUp.js`, `/adduser`.

Implementation: Implemented.

Confidence: High.

### FR-008 — Bulk import creates MySQL users only

CSV `company_name,fullname,email` becomes role 3, active 1, workflow Expense Document, via `/bulkaddusers`.

Evidence: `bulkImportUser.jsx`, `/bulkaddusers`.

Implementation: Implemented.

Confidence: High. Cognito provisioning for these rows is **Unknown / Requires Confirmation**.

### FR-009 — Two Xero Testing workflows

The user selects Expense Document or Revenue Document and a client. Expense requires Excel, PDF-or-image, and CSV before generate controls show. Revenue requires Excel and CSV. Expense generate stays disabled until JSON file count is at least the PDF count.

Evidence: `mainDropDown.jsx`, `WorkflowUploads.jsx`, `getExplorerMsg.js`, `canGenerateWorkFlow.js`.

Implementation: Implemented.

Confidence: High.

### FR-010 — Generated workbook name

The download file name is `{workflow first word} Testing {client name} {current year}.xlsx` in the hardcoded dev bucket.

Evidence: `explorerButtons.jsx`.

Implementation: Implemented.

Confidence: High. Whether that bucket is production is **Unknown**.

### FR-011 — Xero connection on a client

Staff can send a Xero link. The callback exchanges the code for tokens and tenants and stores them with `updatecompanyxero`. Deactivating a client attempts to delete the Xero connection.

Evidence: `clietTable.jsx`, `callback/details.jsx`, `updatecompanyclient`, `XeroExpress`.

Implementation: Implemented.

Confidence: Medium. The mail body of `sendxeroemail` was not fully quoted here.

### FR-012 — Workpaper steps

A signed-in user can add steps, choose a document type, edit fields, upload PDF or Excel, and process them.

Evidence: `customMatchPage`, `constants.js`, `StepCard.jsx`.

Implementation: Implemented.

Confidence: High.

### FR-013 — Match rule used by the model

The large-document and matching prompts require amount, date, and company name together, and tell the model to return JSON only.

Evidence: `openAIFileProcessing/src/buildPrompt.js`.

Implementation: Implemented as prompt text. The model can still return non-JSON. Enforcement after the call was not fully traced.

Confidence: Medium.

### FR-014 — Saved flows are per company

A named flow with empty file arrays is stored in `custom_match_flows` for `company_id`, type `flow`, and in `localStorage`. Duplicate names are rejected in the browser.

Evidence: `customMatchPage/index.jsx`, `/addcustommatchflow`.

Implementation: Implemented.

Confidence: High.

### FR-015 — Exports

Results can be copied or exported to CSV, Excel, and PDF from DataTables. Zip download is a separate Lambda.

Evidence: `constants.js` `TABLE_CONFIG`, `ZipWorkPapers`.

Implementation: Implemented.

Confidence: High.

### FR-016 — API auth does not match the UI

`apibigpond` path entries in `cli-inputs.json` set `permissions.setting` to `protected` and then grant `guest` create, read, update, and delete as well as `auth`. The Express handlers do not check the caller’s role.

Evidence: `amplify/backend/api/apibigpond/cli-inputs.json` (sample `/getusers`, `/getcompany`).

Implementation: Implemented.

Confidence: High that the file says this. Whether API Gateway actually deploys guest access is **Unknown / Requires Confirmation**.

### FR-017 — Errors from the client are posted to `/log`

`errorLog` posts `{ errorMessage, file, functionName, lineNumber }` to `/log`. `frontendLogs` prints them and returns 200 for the two allowed origins.

Evidence: `src/util/errorLog.js`, `frontendLogs/src/index.js`.

Implementation: Implemented.

Confidence: High.

## Constraints

- Browser app. No React Native or server-rendered UI.
- Cognito email usernames. MFA off in the imported auth parameters.
- MySQL session tables are created with string-concatenated names (`master_table_${s3_session}`). The session segment is derived from the identity id.
- OpenAI output is constrained by prompt text, not by a schema validator in the portions read.
- Several secrets and ids are committed (`.env`, user pool id in `signUp.js`, bucket names, SES recipient). This PRD does not repeat secret values.

## Assumptions and disagreements

| Topic | README / changelog | Code on `6b421ff` |
|---|---|---|
| Product name | BigPond | Landing page: Audit Whizz. Package: `bigpond` |
| Custom flow page | `README.md` documents `customFlowData` v2.1 and `customFlowPage` | Routed page is `customMatchPage`. `customFlowPage` directory is absent. Nav link is commented out |
| Model | GPT-4 and GPT-4o | `openAIFileProcessing` sets `gpt-4.1`. Scheduled `processUnprocessedFiles` sets `gpt-4o` |
| Pipeline branch | `bitbucket-pipelines.yml` key `{master}` | Git default branch is `main` |
| Tests | `npm test` script exists | `App.test.js` expects “learn react”, which `App.js` does not render |
| Contributing guide | `README.md` links `CONTRIBUTING.md` | File absent |

## Unknown / Requires Confirmation

- Live URL, Amplify env (dev vs prod), and which S3 bucket production uses.
- Whether guest API permissions are deployed and approved.
- Whether bulk import is supposed to create Cognito users.
- Whether `user_role` contains ids other than 1 and 3, and what id 2 means. The CRUD routes for roles exist (`/getuserrole`, `/adduserrole`, …). No UI for them was found.
- Whether `workpaper_logs` and `custom_match_flows` were created outside this repo.
- Whether both OpenAI schedules are enabled.
- Intended owner of the contact inbox `bhawin@auditwhizz.net`.
- Whether deactivating a client should fail the save when the Xero disconnect fails. Today the SQL update still succeeds.
