# PRD — bigpond_frontend

Evidence date: 2026-10-19.

Source: `git@bitbucket.org:overdrive/bigpond_frontend.git`, branch `master` at `c5ef2b1` (2025-11-26, `change overide`).

Provenance labels:

- **Documented** — stated in `README.md`, `CHANGELOG.md`, `NewKnowledgeBase.md`, a page README, 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”. 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 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 string `"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.

`/logs` is routed for role 1 only. The Logs nav item in `NavAdmin.jsx` is commented out. Role 3 cannot open `/logs`; unknown paths redirect to `/clients`.

## 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. User pool id in source: `ap-southeast-2_tRyN44wjs`.
3. API Gateway APIs `apibigpond`, `apibigpondfiles`, `manageFileProcessTable`, `tempFileProceesTable`.
4. MySQL. `bigpondexpress` uses Lambda layer `createDBConnection`, which reads host, port, and database name from SSM parameter `rdsDetails` and username/password from a hardcoded Secrets Manager secret. `processUnprocessedFiles` declares secrets `DatabaseCredentials` and `OpenAIKey`.
5. S3. Amplify storage resource `bigpondpuloads` with configured name `big-pond-uploads-bucket`. Several Lambdas and `src/util/s3Objects.js` hardcode `big-pond-uploads-bucket123209-dev`. `generatefile` and `getTemplate.js` also name `bigpond-uploads`.
6. OpenAI chat completions. `openAIFileProcessing` uses model `gpt-4.1` and reads `process.env.REACT_APP_OPENAI_API_KEY`. `processUnprocessedFiles` uses model `gpt-4o`.
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. Contact mail is sent to `bhawin@auditwhizz.net` from `no-reply@auditwhizz.net`.
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`. 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** from the catch path in `authContext.jsx`.

### 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}`. The form rejects an empty name and a name that already appears in the list loaded in the browser (case-insensitive trim). Update posts `/updatecompany`. Delete posts `/deletecompanybyid/{id}` (hard delete).

`GET /getactivecompanies` exists for `active = 1`.

The duplicate-name check is in the browser only. The insert SQL has no uniqueness check in `app.js`.

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). Any other role calls `GET /getclientsbycompanyid/{company_id}`. **Implemented** in `clietTable.jsx`.

Add requires client name, contact name, and a valid email (`addClient.jsx`). Insert goes to `company_client`.

Update of a client with `active === 0` reads `xero_token` and `connection_id` and `DELETE`s `https://api.xero.com/connections/{connection_id}`. A Xero error is logged and the client update still returns success. **Implemented** in `updatecompanyclient`.

`POST /updatecompanyxero` stores `xero_token`, `xero_refresh_token`, `xero_tenant_id`, `connection_id`, sets `active = 1` on the client id, and copies the tokens onto every `company_client` row with the same `contact_email`.

`/callback` is available before login and after login. Redirect URI hardcoded in `XeroExpress` and `xerotoken` is `https://www.auditwhizz.net/callback`.

Provenance: **Implemented**.

### WF-5 — Admin maintains users

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

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.
- Role dropdown: User (`3`) or Admin (`1`). The form does not reject an empty role.
- Active is a checkbox.
- `src/util/signUp.js` calls Cognito `AdminCreateUser` and `AdminAddUserToGroup` in `ap-southeast-2` using the signed-in user's credentials. Group is `Admin` only when `role_id === "1"`; otherwise `User`. Delivery medium is EMAIL.
- Then `POST /adduser`. The insert sets `workflow_type` to `"Expense Document"`.

Duplicate Cognito user surfaces `UsernameExistsException` as “User has been registered on Cognito…”.

Bulk import (`bulkImportUser.jsx`):

- CSV headers must be `company_name,fullname,email`.
- Each row is forced to `role_id = 3` and treated as active.
- Company name must match an existing company (case-insensitive).
- Preview, then `POST /bulkaddusers`. The Lambda inserts one row at a time, counts success and failure, and does not roll back earlier rows.

Delete is `POST /deleteuserbyid/{id}` (hard delete).

Provenance: **Implemented**. Cognito recovery for a user created in Cognito but missing in MySQL is **Documented** in `CHANGELOG.md`. The exact recovery branch was not re-traced line by line in this pass.

### WF-6 — Xero Testing upload and workbook

Trigger: Admin or User opens `/workflow`. Nav label: **Xero Testing**.

The screen loads the signed-in user's company clients (`active === 1` only, filtered in `authContext.jsx`).

The user selects:

- Client.
- Workflow type: `Expense Document` or `Revenue Document` (`mainDropDown.jsx`). The choice is stored with `POST /updateuserworkflowtype`.

Uploads (`WorkflowUploads.jsx`), private S3 access:

| Step | Shown when | Types | Max count |
|---|---|---|---|
| 2. Transactions to be reviewed — Excel | Always | `.xlsx` | 1 |
| 3. Invoices — PDF | `workflow_type === "Expense Document"` | `.pdf`, `.png`, `.jpeg`, `.tiff` | 50 |
| 3 or 4. Bank statement export — CSV | Always (numbered 4 for Expense, 3 otherwise) | `.csv` | 50 |

CSV upload asks for a bank number from `GET /getsessionbanknumbers/{session}`. The selected bank is posted to `/updateuserselectedbank`.

`canGenerateWorkFlow`:

- If workflow is Expense Document and at least one xlsx and one pdf are present, generation stays disabled until JSON sidecar count is greater than or equal to the PDF count. The UI keeps polling S3 once a second while that is true.
- In every other case the function sets `canGenerate` to true and stops polling. That includes Revenue Document, and Expense Document while Excel or PDF is still missing.

**Inferred.** The JSON files are the output of an earlier extract step (Textract-style columns exist on `master_table_*`). The code does not label them.

Generate and zip actions live in `explorerButtons.jsx`. The download key uses bucket `big-pond-uploads-bucket123209-dev`. The generated file name starts with the first word of the workflow type plus `Testing`.

`generatefile` reads MySQL session data and writes an Excel workbook. `ZipObjects` zips S3 objects. `deleteSessionUploads` deletes the session's uploads.

Provenance: **Implemented**.

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

Trigger: Admin or User opens `/custommatch`. Nav label: **Workpapers**.

`CustomMatchPage` holds an ordered list of steps. Each step is a `StepCard`. Results render in `ResultsTable`.

Document types in `utils/constants.js`: Statement, Invoices, Supporting Document, Custom. Field types: text, date, currency, percentage, number. Statement, invoice, and supporting-document steps start with preset fields (company, dates, amounts, invoice numbers, balances).

The user uploads PDF or Excel, defines or accepts fields, and sends the file to `openAIFileProcessing`. The Lambda:

- Uses model `gpt-4.1`.
- Extracts records and, when a previous step exists, matches them.
- Large-document prompt (`buildLargeDocumentPrompt`) includes confidence `≥ 6`, amount tolerance within 1% or 5.00 (whichever is stricter), date window ±7 days, identifier rules, and entity-name normalisation.
- The same function sets the filtering stance string to “light filtering” even when `isFiltering` is false. `isFiltering` only adds a filter-criteria sentence.

**Documented** in `CHANGELOG.md` (2025-08-08): threshold is `≥ 6` when filtering is enabled and `≥ 8` otherwise. **Implemented** threshold in `buildLargeDocumentPrompt` is the constant `6`. This is a discrepancy.

`processUnprocessedFiles` is a separate schedule (`rate(5 minutes)`) that calls OpenAI with `gpt-4o`. `openAIFileProcessing` also has a CloudFormation schedule of `rate(1 minute)`.

Named flows are stored in MySQL table `custom_match_flows` (`company_id`, `type`, `json_value`, `active`) via `/addcustommatchflow`, `/getcustommatchflowsbycompanyid/{companyid}`, `/updatecustommatchflow`, `/deletecustommatchflowbyid`. The UI loads flows for `currentUser.company_id`.

Per-user processing rows go through `manageFileProcessTable` (actions include `createTable`, `addFile`, `getResults`, `deleteTable`, `updateFileProcessing`, `setLargeFiles`, `getLogs`). The table name is derived from the user id.

Exports: Excel, CSV, and PDF from the results table; Excel also uploaded to S3 for the workpaper zip (`exportExcelToS3.js`, `ZipWorkPapers`). `CHANGELOG.md` says export walks the match chain from step 1 through later steps and puts unmatched rows in a final section. That behaviour is **Documented**. The functions `buildAllExcelRows` and `findCompleteMatchingChain` are the intended implementation; this pass did not re-execute them.

Local persistence uses `localStorage` keys including `custom_match_page`, with a queue in `safeLocalStorage.js` / `storageQueue.js` for `custom_match_page`, `result_table_db`, `match_table_db`, and `match_results`.

`README.md` also documents a `customFlowData` schema version `v2.1`, a 5MB cap, 30-day temporary tables, and 2-day flow state. Those limits were not found as enforced constants in `customMatchPage` during this pass. Treat the README schema as **Documented** and the enforced limits as **Unknown**.

Provenance: **Implemented**, except the README storage limits and the changelog's dynamic threshold.

### WF-8 — Export workpaper logs

Trigger: an admin opens `/logs` directly. The sidebar link is commented out, so the screen is not reachable from the nav.

`logsPage/details.jsx` requires a from date and a to date. Optional filters: user email (dropdown search) and audit firm name (dropdown search). It posts `action: "getLogs"` to `manageFileProcessTable`.

`handleGetLogs` requires `fromDate` and `toDate`, rejects invalid dates and `fromDate > toDate`, queries `workpaper_logs`, and can filter `user_name` and `audit_firm_name`. Results are ordered by `created_at DESC`.

The page builds a CSV named `workpaper_logs_{from}_to_{to}.csv`. The button label is “Export”. `CHANGELOG.md` calls this Excel export. The file extension in code is `.csv`.

Provenance: **Implemented**. Nav visibility disagrees with `CHANGELOG.md` (**Documented** vs **Implemented**).

### WF-9 — Error page

`/error` renders: “Oops! We encountered a problem while trying to retrieve the data…” and a button that navigates to `/`. The route exists for anonymous visitors and for roles 1 and 3.

Provenance: **Implemented**. Which failures navigate here was not fully traced. **Unknown** which callers use it.

## Functional requirements

### FR-001 — Public landing and contact

The system shows the Audit Whizz landing page at `/` and sends a validated contact message by email.

- Actor: anonymous visitor.
- Trigger: open `/`, submit Contact Us.
- Inputs: name, email, message.
- Behaviour: client-side required checks; `POST /contactmail`; SES to `bhawin@auditwhizz.net`.
- Output: success state, modal closes after 5 seconds.
- Errors: validation messages; API failure goes through `errorLog`.
- Evidence: `welcome/index.jsx`, `contactForm/index.jsx`, `contactmail/src/index.js`.
- Implementation: Implemented.
- Confidence: High.

### FR-002 — Authenticated entry requires a MySQL user

A Cognito session does not by itself open the app. `initUser` must return a `users` row.

- Actor: Cognito user.
- Trigger: authenticator route becomes `authenticated`.
- Inputs: email, identity id, derived `s3_session`.
- Behaviour: update Cognito id and session; create `master_table_{session}` if missing; branch on `active` and `role_id`.
- Output: admin shell, user shell, inactive page, or loader.
- Errors: missing row or DB error leaves the loader up.
- Evidence: `authContext.jsx`, `router/index.jsx`, `bigpondexpress` `/initUser`.
- Implementation: Implemented.
- Confidence: High.

### FR-003 — Role-based navigation

Role 1 and role 3 see different routes. Other roles see a loader.

- Evidence: `router/index.jsx`, `NavAdmin.jsx`, `NavUser.jsx`.
- Implementation: Implemented.
- Confidence: High.
- Note: API handlers in `bigpondexpress` do not check `role_id`. A caller who can invoke the API can call `/getusers` and `/deletecompanybyid`. Amplify path config sets `"setting": "protected"` and also lists guest create/read/update/delete. Whether unauthenticated identities can call the API is **Requires Confirmation**.

### FR-004 — Audit firm, client, and user administration

Admins can create, update, and hard-delete firms, clients, and users. Users can list and manage clients of their own company.

- Validation: firm name non-empty and unique in the loaded list; client name, contact name, email; user full name, email, company.
- Evidence: company, client, and user modals; `bigpondexpress` routes from `/getcompany` through `/deletecompanyclientbyid` and user routes.
- Implementation: Implemented.
- Confidence: High.

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

Staff can email a Xero link and the callback stores tokens on `company_client`. Deactivating a client attempts to remove the Xero connection.

- Evidence: `XeroExpress`, `xerotoken`, `xerotenants`, `xerorefresh`, `sendxeroemail`, `callback`, `updatecompanyclient`, `updatecompanyxero`.
- Implementation: Implemented.
- Confidence: High for the routes and token columns. The exact email body was not copied here.

### FR-006 — Xero Testing workbook

Staff upload Excel, and for Expense Document also invoice images, plus a bank CSV, then generate a workbook and download a zip.

- Evidence: `workflowPage`, `WorkflowUploads.jsx`, `canGenerateWorkFlow.js`, `generatefile`, `ZipObjects`.
- Implementation: Implemented.
- Confidence: High for the upload rules. The workbook column layout inside `generatefile` was not line-reviewed in this pass.

### FR-007 — Workpapers extract and match

Staff define steps, upload documents, extract fields, match to the previous step, and export.

- Evidence: `customMatchPage`, `openAIFileProcessing`, `manageFileProcessTable`, `custom_match_flows` routes.
- Implementation: Implemented.
- Confidence: High for the workflow shape. Prompt thresholds: see the changelog discrepancy under WF-7.

### FR-008 — Workpaper processing logs

An admin who opens `/logs` can export `workpaper_logs` for a date range, optionally filtered by user and audit firm.

- Evidence: `logsPage/details.jsx`, `handleGetLogs`.
- Implementation: Implemented. Navigation entry is commented out.
- Confidence: High.

### FR-009 — SonarQube on master

Every pipeline run on branch `master` runs `sonarsource/sonarqube-scan:3.0.2` with repository variables `SONAR_HOST_URL` and `SONAR_TOKEN`. Project key: `overdrive_bigpond_frontend_AYqLOFK0uNwRc2P2S9QL`. Amplify function trees are excluded from CPD.

- Evidence: `bitbucket-pipelines.yml`, `sonar-project.properties`.
- Implementation: Implemented as pipeline config. Whether the pipeline is green was not checked.
- Confidence: High for the file contents.

## Integrations

| System | Purpose | Trigger | Direction | Auth | Failure |
|---|---|---|---|---|---|
| Cognito | Sign-in and admin user create | Login; Add User | App → Cognito | Amplify session; admin APIs use that session's credentials | Duplicate username message; other errors via `signUp.js` |
| API Gateway + Lambda | Product API | UI calls | Browser → AWS | Amplify `protected` paths. Guest verbs also listed | Lambda `setErrorRes` returns `{ error: "call error!" }` with HTTP 200 on many Express routes |
| MySQL | Firms, users, clients, flows, session tables, logs | Lambda | Lambda → RDS | SSM `rdsDetails` + Secrets Manager | Connection error logged; request still continues to the handler if `createConnection` throws inside the middleware `catch` |
| S3 | Uploads, workbooks, zips | Uploader and generate | Browser and Lambda → S3 | Amplify Storage private; some calls use hardcoded bucket names | Upload UI stays in a loading modal while polling |
| OpenAI | Extract and match | Step process; schedules | Lambda → OpenAI | `REACT_APP_OPENAI_API_KEY` or secret `OpenAIKey` depending on function | Warning log when the env key is missing (`openAIFileProcessing`) |
| Xero | OAuth and disconnect | Client connect; deactivate | Lambda → Xero | Tokens stored on `company_client` | Deactivate still succeeds if the Xero DELETE throws |
| SES | Contact and Xero mail | Form submit; send Xero email | Lambda → SES | IAM of the Lambda | Contact form `errorLog` |
| SonarQube | Static analysis | Push to `master` | Bitbucket → SonarQube | `SONAR_TOKEN` | Not described in repo |

## Important constraints

- Self-signup is hidden. Users are created by an admin through Cognito admin APIs from the browser.
- Only `role_id` 1 and 3 render the application shell.
- New users created through `/adduser` and `/bulkaddusers` get workflow type `Expense Document`.
- Bulk import cannot create admins. Role is forced to 3.
- CORS production origin is `https://www.auditwhizz.net`. Local origin allowed by several Lambdas is `http://localhost:3000`.
- Xero redirect is hardcoded to the production host.
- Express error helper returns JSON with the error text and does not by itself set an HTTP error status (`setErrorRes`).
- Company-name uniqueness is enforced in the add-company modal against the list already loaded, not in SQL.
- Three different S3 bucket strings appear in source. Whether they are one bucket is **Requires Confirmation**.

## Requirements discovered only from implementation

These are not written as product requirements in a PRD. They are how the code behaves:

- Session id is the Cognito identity id segment with hyphens turned into underscores, and that string is interpolated into `CREATE TABLE master_table_${s3_session}`.
- `initUser` returns the user row from before the cognito/session update.
- Deactivating a client deletes the Xero connection when a token is present.
- `updatecompanyxero` copies tokens to every client with the same contact email.
- Expense generation waits for one JSON file per PDF. Other workflow states enable Generate immediately.
- Logs UI exists without a nav link.
- `apibigpond/override.js` is JavaScript with a TypeScript type annotation.
- `App.test.js` does not test the product.

## Out of scope for this document

- Line-by-line workbook layout inside `generatefile`.
- The full OpenAI prompt text (thresholds and tolerances are recorded; the prompt body is in `buildPrompt.js`).
- Deployed environment values that are not in git (whether the OpenAI env var is set, which bucket the Amplify environment actually created, whether guest API access is live).
