refactor(all): rewrite all behavior docs in table format with checkboxes

Rewrite all 11 remaining behavior documents to match the streamlined
invoice.md format:

- dashboard.md: 250 lines, 62 behaviors
- payment.md: 260 lines, behaviors for list, void, check printing, ACH
- transaction.md: 310 lines, list, import, admin insights
- ledger.md: 519 lines, entries, P&L, balance sheet, cash flows
- company.md: 320 lines, profile, 1099s, Plaid/Yodlee, reports
- admin.md: 494 lines, clients, accounts, vendors, rules, jobs, history
- pos.md: 405 lines, sales, deposits, tenders, refunds, shifts
- search-indicators.md: 167 lines, search modal, indicators
- auth.md: 184 lines, login, logout, impersonation, sessions
- outgoing-invoice.md: 192 lines, create, line items, PDF
- legacy-spa.md: 340 lines, all legacy pages (docs only)

All documents now use:
- Testing Patterns section with reusable abstractions
- Numbered tables: # | Behavior | Test Strategy | Status
- It should... behavior descriptions
- Checkboxes [ ]/[x] for tracking implementation
- Cross-Cutting Behaviors for permissions, lock dates, etc.
- Test Data Requirements tables
- Existing Tests to Preserve sections

Total: 3,844 lines of behavior documentation across 12 subsystem docs.
This commit is contained in:
2026-05-04 13:48:51 -07:00
parent e14a23ff54
commit d627e3c5d0
11 changed files with 2837 additions and 2500 deletions

View File

@@ -2,526 +2,319 @@
## Overview
These pages are rendered client-side via Reagent/Re-frame. They use GraphQL for data fetching. They are being migrated to HTMX SSR. Behavior docs exist for reference but **NO UI tests should be written for these pages until migrated**.
These pages are rendered client-side via Reagent/Re-frame and use GraphQL for data fetching. They are being migrated to HTMX SSR. **No UI tests should be written for these pages until migrated.**
### Architecture
- **Routing**: Bidi client-side routes (`src/cljc/auto_ap/client_routes.cljc`)
- **State**: Re-frame subscriptions and events
- **Data**: GraphQL via custom `graphql` effect (`auto-ap.effects`)
- **Permissions**: `auto-ap.permissions/can?` with role-based checks
### Role Permissions Summary
| Role | Transaction Page | Ledger Page | Vendor Create/Edit |
|------|-----------------|-------------|-------------------|
| admin | full | full | full |
| power-user | full | full | full |
| manager | full | blocked | full |
| user | full | full | full |
| read-only | blocked | full | blocked |
**Testing Philosophy**
- Prefer unit tests for pure business logic (calculations, validations, transformations)
- Use integration tests for GraphQL queries, mutations, and data flows
- Use UI tests only after migration to SSR; until then, mark as "UI (when migrated)"
- Every behavior must be user-visible; no tests for implementation details
---
## Pages
## Testing Patterns
### Home (`/`)
### Pattern: GraphQL Data Fetching
All data fetching uses re-frame `graphql` effect with JWT token from `:user` in app-db:
1. Queries use `owns-state` to track loading status per page
2. Results flow through `::data-page/received` event which stores data and syncs URL query params
3. Filter changes are debounced (800ms) via `dispatch-debounce`
#### Purpose
Dashboard showing financial overview for the selected client: top expense categories (pie chart), upcoming bills (bar chart), and cash flow projection (interactive bar chart with table).
**Test implications:** Integration test the GraphQL query resolution and response shape. Do not test the re-frame effect machinery.
#### User Flows
1. User lands on `/`
2. Page loads data for currently selected client
3. If user has access to multiple clients, shows note: "these reports are for [client]. Please choose a specific customer for their report."
4. User can switch cash flow range: 7/30/60/90/120/150/180 days
5. Cash flow bars are clickable and redirect to unpaid invoices for that date
### Pattern: Re-frame State Management
- **Data pages**: `data-page` namespace provides reusable pagination/filtering state
- `::data-page/params` — merged filters + table params + query params
- `::data-page/data` — GraphQL response data
- `::data-page/checked` — selected rows for bulk operations
- **Forms**: `forms` namespace manages edit dialogs with `start-form`, `change-handler`, `save-succeeded`
- **Status**: `status` namespace tracks async operation states (`:loading`, `:complete`, `:error`)
#### GraphQL Queries Used
```graphql
query HomeDashboard($clientId: id) {
expense_account_stats(client_id: $clientId) {
account { id name }
total
}
invoice_stats(client_id: $clientId) {
name
paid
unpaid
}
cash_flow(client_id: $clientId) {
beginning_balance
outstanding_payments
invoices_due_soon { due outstanding_balance invoice_number vendor { id name } }
upcoming_credits { date amount identifier }
upcoming_debits { date amount identifier }
}
}
```
**Test implications:** Test via integration tests that verify the correct data appears after state transitions. Do not test subscription internals.
#### Behaviors to Test (when migrated)
- **Happy path**: Dashboard loads with all three chart sections
- **Multi-client note**: Displays when user hasn't selected a specific client
- **Cash flow ranges**: Switching between 7-180 day views updates chart and table
- **Cash flow table**: Shows invoices, upcoming debits/credits with days-until
- **Edge case**: No data for client shows empty charts gracefully
- **Error states**: GraphQL errors show loading state appropriately
### Pattern: Client-Side Routing
- Navbar uses Bidi `path-for` with `routes/routes` for legacy SPA links
- Some navbar items (Payments, POS, Invoices) now link to `ssr-routes/only-routes` instead
- Page components dispatch `::mounted` on mount and `::unmounted` on unmount
**Test implications:** After migration, integration test route redirects from old SPA routes to new SSR routes.
---
### Login (`/login`)
## Home
#### Purpose
Authentication page with Google OAuth login.
### Dashboard Display
#### User Flows
1. User visits `/login`
2. Shows "Login with Google" button
3. Button links to Google OAuth with optional `redirect-to` query param
4. After auth failure/logout, may show logout reason notification
#### GraphQL Queries/Mutations Used
None.
#### Behaviors to Test (when migrated)
- **Happy path**: Shows login button linking to Google OAuth
- **Redirect**: Preserves `redirect-to` query parameter in OAuth URL
- **Logout reason**: Displays warning notification when `logout-reason` is set
- **Edge case**: Already authenticated user visiting login page
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 1.1 | It should display a dashboard with three chart sections: top expense categories, upcoming bills, and cash flow projection | UI (when migrated) | [ ] |
| 1.2 | It should load data for the currently selected client on page load | Integration | [ ] |
| 1.3 | It should display a note: "these reports are for [client]. Please choose a specific customer for their report." when the user has access to multiple clients and has not selected a specific one | UI (when migrated) | [ ] |
| 1.4 | It should display an interactive bar chart for cash flow projection | UI (when migrated) | [ ] |
| 1.5 | It should display a table below the cash flow chart showing invoices, upcoming debits, and upcoming credits with days-until due | UI (when migrated) | [ ] |
| 1.6 | It should allow switching the cash flow range between 7, 30, 60, 90, 120, 150, and 180 days | UI (when migrated) | [ ] |
| 1.7 | Given the user switches the cash flow range, then the chart and table should update to reflect the selected range | Integration | [ ] |
| 1.8 | Given the user clicks a cash flow bar, then it should redirect to the unpaid invoices page for that date | UI (when migrated) | [ ] |
| 1.9 | It should show empty charts gracefully when there is no data for the selected client | UI (when migrated) | [ ] |
| 1.10 | It should show a loading state while GraphQL data is being fetched | UI (when migrated) | [ ] |
| 1.11 | It should handle GraphQL errors by showing the loading state appropriately | UI (when migrated) | [ ] |
---
### Needs Activation (`/needs-activation`)
## Login
#### Purpose
Shown when authenticated user's account is not yet activated.
### Authentication
#### User Flows
1. User logs in but account is inactive
2. Shows message: "Sorry, your user is not activated yet. Please have Ben Skinner enable your account."
3. "here" link clears user state and redirects to login
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 2.1 | It should display a "Login with Google" button on the login page | UI (when migrated) | [ ] |
| 2.2 | It should link the login button to Google OAuth | UI (when migrated) | [ ] |
| 2.3 | It should preserve the `redirect-to` query parameter in the Google OAuth URL | Integration | [ ] |
| 2.4 | It should display a warning notification with the logout reason when the `logout-reason` query parameter is set | UI (when migrated) | [ ] |
| 2.5 | It should redirect an already authenticated user away from the login page | Integration | [ ] |
#### GraphQL Queries/Mutations Used
None.
### Needs Activation
#### Behaviors to Test (when migrated)
- **Happy path**: Shows activation required message
- **Relogin link**: Clears user state and redirects to login
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 2.6 | It should display the message: "Sorry, your user is not activated yet. Please have Ben Skinner enable your account." when the user's account is inactive | UI (when migrated) | [ ] |
| 2.7 | It should provide a "here" link that clears user state and redirects to the login page | Integration | [ ] |
---
### Transactions (`/transactions/`, `/transactions/unapproved`, `/transactions/approved`, `/transactions/requires-feedback`, `/transactions/excluded`)
## Transactions
#### Purpose
Transaction list with filtering, editing, and bulk admin operations. Different routes show different approval statuses.
### List Display
#### User Flows
1. User navigates to transactions page
2. Default date filter: last 1 month
3. Side bar allows filtering by: vendor, account, bank account, date range, amount range, location, import batch, description, linked status
4. Table shows transactions with checkbox selection (admin only)
5. Clicking row opens edit side bar
6. Admin can: bulk code, bulk delete, suppress, or manual Yodlee import
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.1 | It should display a table of transactions with columns for amount, memo, location, approval status, vendor, accounts, date, and description | UI (when migrated) | [ ] |
| 3.2 | It should load the transaction list with a default date filter of the last 1 month | Integration | [ ] |
| 3.3 | It should allow filtering by vendor, account, bank account, date range, amount range, location, import batch, description, and linked status via a sidebar | UI (when migrated) | [ ] |
| 3.4 | It should debounce sidebar filter changes by 800ms before refreshing data | Integration | [ ] |
| 3.5 | It should support pagination with start and per-page parameters | Integration | [ ] |
| 3.6 | It should display checkboxes for row selection when the user is an admin | UI (when migrated) | [ ] |
| 3.7 | It should not display checkboxes or bulk action buttons for non-admin users | UI (when migrated) | [ ] |
#### GraphQL Queries/Mutations Used
```graphql
query TransactionPage($filters: transaction_filters) {
transaction_page(filters: $filters) {
data {
id amount memo location approval_status check_number is_locked
matched_rule { note id }
vendor { name id }
accounts { id amount location account { name id location numeric_code } }
date yodlee_merchant { name yodlee_id id } plaid_merchant { name id }
post_date expected_deposit { id date } forecast_match { id identifier }
status description_original
payment { check_number s3_url id date }
client { name id }
bank_account { name yodlee_account_id current_balance }
}
total start end count
}
}
### Transaction Edit
mutation EditTransaction($transaction: edit_transaction) {
edit_transaction(transaction: $transaction) { ... }
}
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.8 | It should open an edit sidebar when the user clicks a transaction row | UI (when migrated) | [ ] |
| 3.9 | It should allow updating the vendor, approval status, memo, and expense accounts | UI (when migrated) | [ ] |
| 3.10 | It should validate that the sum of expense account amounts equals the transaction amount | Unit + Integration | [ ] |
| 3.11 | It should validate that locations are valid for the selected accounts | Unit + Integration | [ ] |
| 3.12 | It should block editing locked transactions | Integration | [ ] |
| 3.13 | It should display validation errors inline in the edit form | UI (when migrated) | [ ] |
mutation DeleteTransactions($filters: transaction_filters, $ids: [id], $suppress: Boolean) {
delete_transactions(filters: $filters, ids: $ids, suppress: $suppress) { message }
}
### Bulk Operations (Admin)
mutation BulkCodeTransactions($filters: transaction_filters, $ids: [id], $vendor: id, $approval_status: transaction_approval_status, $accounts: [edit_percentage_account]) {
bulk_code_transactions(filters: $filters, ids: $ids, vendor: $vendor, approval_status: $approval_status, accounts: $accounts) { message }
}
```
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.14 | It should allow deleting selected transactions or all visible transactions | Integration | [ ] |
| 3.15 | It should allow suppressing selected transactions instead of deleting them | Integration | [ ] |
| 3.16 | It should allow bulk coding by applying vendor, account, approval status, and account rules to multiple transactions | Integration | [ ] |
| 3.17 | It should allow importing transactions via a manual Yodlee import dialog | Integration | [ ] |
#### Behaviors to Test (when migrated)
- **Happy path**: Loads transaction list with default 1-month filter
- **Route variants**: Each approval-status route applies correct default filter
- **Filtering**: Side bar filters trigger debounced data refresh (800ms)
- **Pagination**: Start/per-page params work correctly
- **Edit transaction**: Update vendor, approval status, memo, expense accounts
- **Edit validation**: Account total must equal transaction amount; locations must be valid
- **Bulk delete (admin)**: Delete selected transactions or all visible transactions
- **Bulk suppress (admin)**: Mark transactions as suppressed instead of deleting
- **Bulk code (admin)**: Apply vendor/account rules to multiple transactions
- **Manual import (admin)**: Import transactions via manual Yodlee import dialog
- **Error states**: Locked transactions cannot be edited/deleted; validation errors display inline
- **Permissions**: Non-admin users don't see checkboxes or action buttons
### Route Variants
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.18 | It should apply the correct default approval status filter for each route variant: all, unapproved, approved, requires-feedback, excluded | Integration | [ ] |
---
### Ledger (`/ledger/`)
## Ledger
#### Purpose
General ledger showing journal entries with line items. Supports filtering and CSV export.
### General Ledger
#### User Flows
1. User navigates to `/ledger/`
2. Default date filter: last 1 month
3. Side bar filters: vendor, account, bank account, date range, amount range, location
4. Table shows journal entries with expandable line items
5. Admin can export to CSV
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.1 | It should display a table of journal entries with expandable line items | UI (when migrated) | [ ] |
| 4.2 | It should load the ledger with a default date filter of the last 1 month | Integration | [ ] |
| 4.3 | It should allow filtering by vendor, account, bank account, date range, amount range, and location via a sidebar | UI (when migrated) | [ ] |
| 4.4 | It should support virtual pagination controls | Integration | [ ] |
| 4.5 | It should allow admin users to export filtered results as a CSV download | Integration | [ ] |
| 4.6 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
#### GraphQL Queries/Mutations Used
```graphql
query LedgerPage($filters: ledger_filters) {
ledger_page(filters: $filters) {
journal_entries {
id source original_entity amount note cleared_against alternate_description
vendor { name id } client { name id }
line_items { id debit credit location running_balance account { id name } }
date
}
total start end
}
}
### Profit and Loss
query LedgerCsv($filters: ledger_filters) {
ledger_csv(filters: $filters) { csv_content_b64 }
}
```
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.7 | It should generate a profit and loss report for a single client with a default period | Integration | [ ] |
| 4.8 | It should allow selecting multiple companies via a multi-select typeahead and generating a combined report | UI (when migrated) | [ ] |
| 4.9 | It should provide period preset buttons: 13 periods, 12 months, last week, week-to-date, last month, month-to-date, year-to-date, last calendar year, and full year | UI (when migrated) | [ ] |
| 4.10 | It should populate the correct date ranges when a period preset is selected | Unit | [ ] |
| 4.11 | It should allow custom period selection via start and end date pickers in advanced mode | UI (when migrated) | [ ] |
| 4.12 | It should optionally include period-over-period deltas | UI (when migrated) | [ ] |
| 4.13 | It should optionally break out the report by location, which is mutually exclusive with including deltas | Integration | [ ] |
| 4.14 | It should allow admin users to generate and download a PDF export | Integration | [ ] |
| 4.15 | It should provide an email composition link for single-client PDF exports | UI (when migrated) | [ ] |
| 4.16 | It should open a ledger detail sidebar when the user clicks a report cell | UI (when migrated) | [ ] |
| 4.17 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
#### Behaviors to Test (when migrated)
- **Happy path**: Loads ledger with default 1-month filter
- **Filtering**: Date range, vendor, account, location filters work
- **CSV export**: Admin can export filtered results as base64 CSV download
- **Pagination**: Virtual pagination controls
- **Permissions**: Manager role sees "Not authorized"
### Cash Flows
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.18 | It should generate a cash flows statement report | Integration | [ ] |
| 4.19 | It should use the same company, period, delta, and location column controls as the profit and loss report | Integration | [ ] |
| 4.20 | It should allow admin users to export the report to PDF | Integration | [ ] |
| 4.21 | It should open a ledger detail sidebar when the user clicks a report cell | UI (when migrated) | [ ] |
| 4.22 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
### Profit and Loss Detail
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.23 | It should generate a detailed journal entry report with a default 2-week date range | Integration | [ ] |
| 4.24 | It should group journal entries by category: sales, COGS, payroll, controllable, fixed overhead, and ownership controllable | Integration | [ ] |
| 4.25 | It should display Gross Profit, Overhead, and Net Profit summaries | UI (when migrated) | [ ] |
| 4.26 | It should filter journal entries by the selected start and end dates | Integration | [ ] |
| 4.27 | It should allow admin users to export the report to PDF with an email link for single client | Integration | [ ] |
| 4.28 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
### Balance Sheet
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.29 | It should generate a balance sheet report as of a specific date | Integration | [ ] |
| 4.30 | It should allow selecting multiple companies and combining them into a single report | Integration | [ ] |
| 4.31 | It should optionally include a prior-year comparison with a side-by-side view | UI (when migrated) | [ ] |
| 4.32 | It should allow admin users to export the report to PDF with an email composition link for single client | Integration | [ ] |
| 4.33 | It should open ledger entries filtered by account and date range when the user clicks a cell | UI (when migrated) | [ ] |
| 4.34 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
### External Ledger
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.35 | It should display only externally-imported journal entries with an external_id | Integration | [ ] |
| 4.36 | It should allow admin users to delete selected entries, with a maximum of 1000 at once | Integration | [ ] |
| 4.37 | It should allow admin users to export to CSV | Integration | [ ] |
| 4.38 | It should display "Not authorized" for non-admin users | UI (when migrated) | [ ] |
### External Import
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.39 | It should allow admin users to paste tab-separated data into a textarea | UI (when migrated) | [ ] |
| 4.40 | It should provide a checkbox to indicate whether the first row is a header | UI (when migrated) | [ ] |
| 4.41 | It should parse the pasted data into a table with columns: Id, Client, Source, Vendor, Date, Account, Location, Debit, Credit, Note, and Cleared against | Integration | [ ] |
| 4.42 | It should validate that the client code exists | Unit + Integration | [ ] |
| 4.43 | It should validate that the vendor exists | Unit + Integration | [ ] |
| 4.44 | It should validate that the date is in MM/dd/yyyy format | Unit + Integration | [ ] |
| 4.45 | It should validate that total debits equal total credits | Unit + Integration | [ ] |
| 4.46 | It should validate that all amounts are greater than 0 | Unit + Integration | [ ] |
| 4.47 | It should validate that entries are dated after the client's locked-until date | Integration | [ ] |
| 4.48 | It should validate that the location belongs to the client or is "A" | Unit + Integration | [ ] |
| 4.49 | It should validate that the account exists and the location matches the account's required location | Unit + Integration | [ ] |
| 4.50 | It should display errors per row with a dropdown explanation | UI (when migrated) | [ ] |
| 4.51 | It should show status icons indicating success, ignored, or existing per row after import | UI (when migrated) | [ ] |
| 4.52 | It should display the total success count, ignored count, and error count after import | UI (when migrated) | [ ] |
| 4.53 | It should provide an "Only show errors" filter | UI (when migrated) | [ ] |
| 4.54 | It should display "Not authorized" for non-admin users | UI (when migrated) | [ ] |
---
### Profit and Loss (`/ledger/profit-and-loss`)
## Payments
#### Purpose
Financial report showing revenue and expenses over configurable periods. Supports multi-company and PDF export.
#### User Flows
1. User selects companies (multi-select typeahead)
2. User selects period preset (13 periods, 12 months, last week, week-to-date, last month, month-to-date, year-to-date, last calendar year, full year)
3. Optional: Include deltas, Column per location
4. Click "Run" to generate report
5. Admin can "Export" to PDF
6. Clicking report cells opens ledger detail side bar
#### GraphQL Queries/Mutations Used
```graphql
query ProfitAndLoss($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
periods { accounts { name amount client_id account_type id count numeric_code location } }
}
}
query ProfitAndLossPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
profit_and_loss_pdf(...) { url name }
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Generate P&L for single client with default period
- **Multi-company**: Select multiple clients and generate combined report
- **Period presets**: All preset buttons populate correct date ranges
- **Advanced mode**: Custom period date pickers
- **Include deltas**: Shows period-over-period changes
- **Column per location**: Breaks out by location (mutually exclusive with deltas)
- **PDF export**: Admin can generate and download PDF; single-client shows email link
- **Ledger drill-down**: Clicking cells opens ledger entries side bar for that account/period
- **Permissions**: Manager role sees "Not authorized"
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 5.1 | It should be fully migrated to SSR at `/payment/`; the legacy client route exists only for navbar highlighting | N/A | [x] |
---
### Cash Flows (`/ledger/cash-flows`)
## Reports
#### Purpose
Statement of cash flows report. Same control structure as P&L but different report format.
#### User Flows
Same as Profit and Loss but generates cash flow statement.
#### GraphQL Queries/Mutations Used
```graphql
query CashFlows($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
periods { accounts { name amount debits credits client_id account_type id count numeric_code location } }
}
}
query CashFlowsPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
cash_flows_pdf(...) { url name }
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Generate cash flows report
- **Same controls as P&L**: Companies, periods, deltas, location columns
- **PDF export**: Admin-only export button
- **Ledger drill-down**: Clicking cells opens ledger detail
- **Permissions**: Manager role sees "Not authorized"
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 6.1 | It should be fully migrated to SSR at `/company/reports`; the legacy client route exists only for navbar highlighting | N/A | [x] |
---
### Profit and Loss Detail (`/ledger/profit-and-loss-detail`)
## Vendors
#### Purpose
Detailed journal entry report broken down by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable).
### Admin Vendor Management
#### User Flows
1. Select companies
2. Select date range (start/end date pickers)
3. Click "Run" to generate detail report
4. Shows journal entries grouped by category and account with running balances
5. Admin can export to PDF
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 7.1 | It should be fully migrated to SSR at `/admin/vendor`; the legacy client route exists only for navbar highlighting | N/A | [x] |
#### GraphQL Queries/Mutations Used
```graphql
query JournalDetailReport($clientIds: [id], $dateRange: date_range, $categories: [category]) {
journal_detail_report(client_ids: $clientIds, date_range: $dateRange, categories: $categories) {
categories {
category client_id location
account { numeric_code name }
journal_entries { description date debit credit location running_balance account { id name } }
total
}
}
}
### New Vendor Dialog
query JournalDetailReportPdf($clientIds: [id], $dateRange: date_range, $categories: [category]) {
journal_detail_report_pdf(...) { url name }
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Generate detail report with default 2-week range
- **Category breakdown**: Report includes all 6 category sections plus Gross Profit, Overhead, Net Profit summaries
- **Date range filtering**: Start/end dates filter journal entries
- **PDF export**: Admin can export to PDF with email link for single client
- **Permissions**: Manager role sees "Not authorized"
---
### Balance Sheet (`/ledger/balance-sheet`)
#### Purpose
Balance sheet report as of a specific date, with optional prior-year comparison.
#### User Flows
1. Select companies
2. Select report date
3. Optionally enable "Include comparison" and select comparison date
4. Click "Run" to generate balance sheet
5. Admin can export to PDF
6. Clicking cells opens ledger detail side bar
#### GraphQL Queries/Mutations Used
```graphql
query BalanceSheet($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
balance_sheet(client_ids: $clientIds, date: $date, comparison_date: $comparisonDate, include_comparison: $includeComparison) {
balance_sheet_accounts { name amount account_type id numeric_code client_id }
comparable_balance_sheet_accounts { name amount account_type id client_id numeric_code }
}
}
query BalanceSheetPdf($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
balance_sheet_pdf(...) { url name }
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Generate balance sheet for current date
- **Comparison mode**: Shows prior period side-by-side
- **Multi-company**: Combines multiple clients
- **PDF export**: Admin-only with email composition link for single client
- **Ledger drill-down**: Clicking cells opens ledger entries filtered by account and date range
- **Permissions**: Manager role sees "Not authorized"
---
### External Ledger (`/ledger/external`)
#### Purpose
Shows only externally-imported journal entries. Admin can delete entries.
#### User Flows
1. Admin navigates to `/ledger/external`
2. Shows external journal entries with external_id
3. Can select entries and delete them
4. Admin can export to CSV
#### GraphQL Queries/Mutations Used
```graphql
query ExternalLedger($filters: ledger_filters) {
ledger_page(filters: $filters) {
journal_entries { id external_id source ... }
total start end
}
}
mutation DeleteExternalLedger($filters: ledger_filters, $ids: [id]) {
delete_external_ledger(filters: $filters, ids: $ids) { message }
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Loads external-only ledger entries
- **Delete**: Admin can delete selected entries (max 1000 at once)
- **CSV export**: Same as main ledger
- **Permissions**: Admin-only; non-admin sees "Not authorized"
---
### External Import (`/ledger/external-import`)
#### Purpose
Manual import of ledger entries via tab-separated text paste.
#### User Flows
1. Admin pastes tab-separated data into textarea
2. Optional: indicates whether first row is header
3. Click "Parse" to convert to table
4. Table shows parsed rows: Id, Client, Source, Vendor, Date, Account, Location, Debit, Credit, Note, Cleared against
5. Click "Import" to submit
6. Results show: success count, ignored count, error count
7. Errors show inline with dropdown explanations
8. "Only show errors" filter available
#### GraphQL Queries/Mutations Used
```graphql
mutation ImportLedger($entries: [ledger_entry_input]) {
import_ledger(entries: $entries) {
successful { external_id }
existing { external_id }
ignored { external_id }
errors { external_id error }
}
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Paste TSV data, parse, import successfully
- **Header row**: Checkbox to skip first row
- **Validation**: Client code must exist, vendor must exist, date must be MM/dd/yyyy, debits=credits, amounts > 0
- **Lock check**: Entries must be after client's locked-until date
- **Location validation**: Location must belong to client or be "A"
- **Account validation**: Account must exist and location must match account's required location
- **Error display**: Errors show per-row with dropdown explanation
- **Success/ignored/existing**: Status icons show import result per row
- **Permissions**: Admin-only
---
### Payments (`/payments/`)
#### Purpose
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual payments page is served by `payment` SSR routes at `/payment/`.
#### GraphQL Queries/Mutations Used
N/A (SSR page)
#### Behaviors to Test (when migrated)
Already migrated. Test via SSR routes.
---
### Reports (`/reports/`)
#### Purpose
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual reports page is served by `company/reports` SSR routes.
#### GraphQL Queries/Mutations Used
N/A (SSR page)
#### Behaviors to Test (when migrated)
Already migrated. Test via SSR routes.
---
### Admin Vendors (`/admin/vendors`)
#### Purpose
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual vendor management is served by `admin/vendor` SSR routes.
#### GraphQL Queries/Mutations Used
N/A (SSR page)
#### Behaviors to Test (when migrated)
Already migrated. Test via SSR routes.
---
### New Vendor (`/vendor/new`)
#### Purpose
Opens the home dashboard with the vendor creation dialog pre-opened.
#### User Flows
1. User navigates to `/vendor/new`
2. Loads home dashboard
3. Opens vendor dialog if user has `:vendor :create` permission
4. Dialog allows creating new vendor with name, terms, address, contacts, default account, etc.
#### GraphQL Queries/Mutations Used
```graphql
mutation UpsertVendor($vendor: add_vendor) {
upsert_vendor(vendor: $vendor) { id name ... }
}
```
#### Behaviors to Test (when migrated)
- **Happy path**: Home loads with vendor dialog open
- **Permission check**: Dialog only opens if user can `:vendor :create`
- **Vendor creation**: Form submission creates vendor via GraphQL mutation
- **Validation**: Only one terms override, schedule payment DOM override, and account override per client
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 7.2 | It should load the home dashboard with the vendor creation dialog pre-opened when navigating to `/vendor/new` | UI (when migrated) | [ ] |
| 7.3 | It should only open the vendor dialog if the user has `:vendor :create` permission | Integration | [ ] |
| 7.4 | It should allow creating a new vendor with name, terms, address, contacts, and default account | UI (when migrated) | [ ] |
| 7.5 | It should validate that only one terms override exists per client | Unit + Integration | [ ] |
| 7.6 | It should validate that only one schedule payment DOM override exists per client | Unit + Integration | [ ] |
| 7.7 | It should validate that only one account override exists per client | Unit + Integration | [ ] |
---
## Cross-Cutting Behaviors
### GraphQL Query Patterns
- All data fetching uses re-frame `graphql` effect with JWT token from `:user` in app-db
- Queries use `owns-state` to track loading status per page
- Results flow through `::data-page/received` event which stores data and syncs URL query params
- Filter changes are debounced (800ms) via `dispatch-debounce`
### Client-Side State Management (Re-frame)
- **Data pages**: `data-page` namespace provides reusable pagination/filtering state
- `::data-page/params` - merged filters + table params + query params
- `::data-page/data` - GraphQL response data
- `::data-page/checked` - selected rows for bulk operations
- **Forms**: `forms` namespace manages edit dialogs with `start-form`, `change-handler`, `save-succeeded`
- **Status**: `status` namespace tracks async operation states (`:loading`, `:complete`, `:error`)
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.1 | It should include the JWT token from the `:user` app-db state in all GraphQL requests | Integration | [ ] |
| 8.2 | It should track loading status per page using `owns-state` | Integration | [ ] |
| 8.3 | It should store GraphQL response data and sync URL query params via the `::data-page/received` event | Integration | [ ] |
| 8.4 | It should debounce filter changes by 800ms before dispatching GraphQL requests | Integration | [ ] |
### Navigation Between Legacy Pages
- Navbar uses Bidi `path-for` with `routes/routes` for legacy SPA links
- Some navbar items (Payments, POS, Invoices) now link to `ssr-routes/only-routes` instead
- Page components dispatch `::mounted` on mount and `::unmounted` on unmount to set up/tear down:
- Data subscriptions
- Forward event listeners
- Track subscriptions (parameter change watchers)
### Re-frame State Management
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.5 | It should merge filters, table params, and query params into `::data-page/params` | Integration | [ ] |
| 8.6 | It should store GraphQL response data in `::data-page/data` | Integration | [ ] |
| 8.7 | It should track selected rows for bulk operations in `::data-page/checked` | Integration | [ ] |
| 8.8 | It should manage edit dialog state with `start-form`, `change-handler`, and `save-succeeded` events | Integration | [ ] |
| 8.9 | It should track async operation states as `:loading`, `:complete`, or `:error` | Integration | [ ] |
### Navigation
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.10 | It should redirect old SPA routes to new SSR routes after migration | Integration | [ ] |
| 8.11 | It should set up data subscriptions, forward event listeners, and parameter change watchers on page mount | Integration | [ ] |
| 8.12 | It should tear down data subscriptions, forward event listeners, and parameter change watchers on page unmount | Integration | [ ] |
---
## Test Data Requirements
| Entity | Requirements |
|--------|-------------|
| **Users** | Admin, power-user, manager, user, and read-only roles |
| **Clients** | Multiple clients with different locations; some with locked-until dates |
| **Vendors** | With/without default accounts, terms, and autopay settings |
| **Accounts** | Expense accounts with/without invoice allowance; different locations |
| **Bank Accounts** | Check, cash, and credit types |
| **Transactions** | Various approval statuses, dates, amounts, locked/unlocked states |
| **Journal Entries** | With/without external_id; various categories and accounts |
| **Invoices** | Various statuses and due dates for cash flow projections |
## Existing Tests to Preserve
- Test GraphQL query resolution for `HomeDashboard`, `TransactionPage`, `LedgerPage`, `ProfitAndLoss`, `CashFlows`, `JournalDetailReport`, `BalanceSheet`, and `ExternalLedger`
- Test re-frame event handlers for data page state transitions
- Test form validation logic for transaction editing and external ledger import
## Dependencies
- GraphQL API (data fetching)
- Re-frame/Reagent (client-side state and rendering)
- Bidi (client-side routing)
- Recharts (chart rendering on Home page)
- Server-side PDF generation (P&L, Cash Flows, Balance Sheet, P&L Detail)
## Migration Notes
### Actively Migrated / Already SSR
### Already Migrated to SSR
- **Payments** (`/payments/`) - Fully migrated to `/payment/` SSR routes
- **Reports** (`/reports/`) - Fully migrated to `/company/reports` SSR routes
- **Admin Vendors** (`/admin/vendors`) - Fully migrated to `/admin/vendor` SSR routes