docs: add comprehensive test behavior documentation for all pages

Add behavior documentation covering all SSR and legacy SPA pages:
- Testing strategy and type definitions (unit/integration/UI)
- Dashboard, Invoice, Payment, Transaction, Ledger pages
- Company/Settings, POS, Admin, Search, Auth pages
- Legacy SPA behavior docs (no UI tests until migrated)
- Edge cases, test data requirements, and dependencies per subsystem

Total: 3,600+ lines of behavior documentation to guide test authorship.
This commit is contained in:
2026-05-04 12:15:20 -07:00
parent 2993da5c82
commit b499d460f3
13 changed files with 3785 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
# Transaction Behaviors
## Overview
Transactions represent bank account activity imported from external sources (Plaid, Yodlee, Intuit). The SSR transaction pages provide an HTMX-based grid view for browsing, filtering, and exporting transactions, plus an admin insights page for AI-assisted vendor/account coding. The legacy client-side application (`/transactions`) remains the primary interface for day-to-day transaction management; the SSR `transaction2` routes are partially implemented.
## Routes & Pages
| Route | Handler | Purpose | Status |
|-------|---------|---------|--------|
| `GET /transaction2` | `::page` | Main transaction grid with filters | Implemented |
| `GET /transaction2/table` | `::table` | HTMX-swappable table body | Implemented |
| `GET /transaction2/csv` | `::csv` | CSV export of filtered transactions | Implemented |
| `GET /transaction2/bank-account-filter` | `::bank-account-filter` | Dynamic bank account radio cards | Implemented |
| `GET /transaction2/new` | `::new` | New transaction form | Route defined, handler not wired |
| `POST /transaction2/new` | `::new-submit` | Submit new transaction | Route defined, handler not wired |
| `GET /transaction2/external-new` | `::external-page` | External transaction entry | Route defined, handler not wired |
| `GET /transaction2/external-import-new` | `::external-import-page` | CSV/text import form | Route defined, handler not wired |
| `POST /transaction2/external-import-new/parse` | `::external-import-parse` | Parse import data | Route defined, handler not wired |
| `POST /transaction2/external-import-new/import` | `::external-import-import` | Execute import | Route defined, handler not wired |
| `GET /transaction/insights` | `:transaction-insights` | Admin AI coding insights page | Implemented |
| `GET /transaction/insights/table` | `:transaction-insight-table` | Insights data grid | Implemented |
| `GET /transaction/insights/rows/:after` | `:transaction-insight-rows` | Infinite scroll rows | Implemented |
| `POST /transaction/insights/code/:id` | `:transaction-insight-code` | Approve & code a transaction | Implemented |
| `DELETE /transaction/insights/disapprove/:id` | `:transaction-insight-disapprove` | Reject a recommendation | Implemented |
| `GET /transaction/insights/explain/:id` | `:transaction-insight-explain` | Similar transactions modal | Implemented |
## Behaviors by Page
### Transaction List
#### Grid Display
- Table displays columns: Client, Vendor, Description, Date, Amount, Links
- Client column is hidden when only one client with one location is selected
- Description column shows `description-original`; when vendor is missing, vendor column falls back to `description-simple` in italics
- Date rendered in `normal-date` format (e.g., `MM/DD/YYYY`)
- Amount right-aligned and formatted as `$X,XXX.XX`
- Links column shows dropdown with links to associated Payment page or Client Overrides
- Table supports checkboxes for bulk selection (`:check-boxes? true`)
- When sorted by Vendor, table breaks into grouped sections by vendor name (or "No vendor")
- Grid title is "Transaction" and entity name is "register"
- Breadcrumb shows "Transactions" linking to the list page
#### Filters
- **Vendor**: Typeahead search against vendor names (fetches from `:vendor-search` endpoint)
- **Bank Account**: Radio card selector with "All" plus client's bank accounts; dynamically reloads via `clientSelected` event
- **Date Range**: Standard date range picker (start/end dates)
- **Description**: Free-text input with 1000ms debounced search on `keyup changed`
- **Amount Range**: Two money inputs (gte/lte) with "to" label between them
- **Exact Match ID**: Hidden filter rendered as a removable pill/tag when present in query params
- All filters trigger table reload via HTMX (`change delay:500ms` or `keyup delay:1000ms`)
#### Sorting
- Sortable columns: Client, Vendor, Date, Amount, Description
- Default sort is ascending on the implicit `sort-default` field from `scan-transactions`
- Sorting is handled server-side with `add-sorter-fields` generating Datomic query clauses
- Vendor sort handles missing vendors by grounding empty string
#### Pagination
- Default 25 rows per page
- Pagination controls rendered by grid helper
- Total matching count and sum of all matching amounts displayed
#### Actions
- "Add Transaction" button (primary color) links to `::route/new` (not yet implemented)
- Row action buttons (edit/delete) are commented out in source
#### CSV Export
- CSV route exports all filtered results (not just current page)
- CSV headers: Id, Client, Vendor, Description, Date, Amount
- CSV uses raw data values (`:db/id`, `:transaction/amount`, etc.) instead of formatted display
### New Transaction
> **Note:** Routes are defined in `auto-ap.routes.transactions` but no handlers are wired in `auto-ap.ssr.transaction`. The "Add Transaction" button in the grid links to this route, which would currently 404. Legacy client-side new transaction functionality exists in the SPA.
Route definitions include:
- `GET /transaction2/new` - New transaction form
- `POST /transaction2/new` - Form submission
- `GET /transaction2/new/location-select` - Location selector sub-component
- `GET /transaction2/new/account-typeahead` - Account typeahead sub-component
- `GET /transaction2/new/line-item` - Add line item sub-component
### External Import
> **Note:** Routes are defined but handlers are not wired in the SSR transaction namespace. The ledger namespace (`auto-ap.ssr.ledger`) has a fully implemented external import flow that these routes may mirror.
Route definitions include:
- `GET /transaction2/external-new` - External transaction entry page
- `GET /transaction2/external-import-new` - Import form (CSV/text paste)
- `POST /transaction2/external-import-new/parse` - Parse pasted data
- `POST /transaction2/external-import-new/import` - Execute import
The import system (from `auto-ap.import.transactions`) supports:
- Deduplication via SHA-256 synthetic keys
- Auto-matching to existing pending payments by check number or amount
- Auto-matching to expected deposits
- Auto-coding via transaction rules
- Categorization: `:import`, `:extant`, `:suppressed`, `:error`, `:not-ready`
### Admin Insights / Coding
#### Insights Page
- Admin-only page at `/transaction/insights`
- Title: "Transaction Insights"
- Breadcrumbs: Transactions > Insights
- Displays data grid card with title "Transaction Insights"
- Grid headers: Client, Account, Date, Description, Amount, Actions
- No pagination; shows up to 50 recommendations at a time
- Infinite scroll via `hx-trigger="intersect once"` on last row
- When no more recommendations, shows "That's the last of 'em!"
#### Recommendation Rows
- Shows unapproved transactions from last 300 days that have `outcome-recommendation` data
- Each row displays: client code, bank account code, date, description, amount
- Amount displayed as rounded dollar tag (green for positive, red for negative)
- Up to 3 recommendation buttons per row, sorted by frequency (highest count first)
- Each button shows: `Vendor Name | Account Name` with a count badge
- Button color is `:primary` if the recommendation was seen by client, `:secondary` otherwise
#### Coding Actions
- **Approve (Code)**: `POST /transaction/insights/code/:id`
- Sets approval status to `approved`
- Assigns vendor and account
- Distributes amount across valid locations using `spread-cents`
- Row re-renders with `live-added` class and Alpine.js disappear animation
- **Reject (Disapprove)**: `DELETE /transaction/insights/disapprove/:id`
- Clears `outcome-recommendation` on transaction
- Row re-renders with `live-removed` class and disappear animation
- **Explain**: `GET /transaction/insights/explain/:id`
- Opens modal showing similar transactions from Pinecone vector search
- Displays: Date, Description, Amount, Vendor, Account, Similarity Score
- Similarity threshold: score > 0.95
- Shows top 10 similar transactions plus the target transaction highlighted
#### Pinecone Integration
- Fetches vector embedding for transaction ID from Pinecone index
- Queries for top 100 similar vectors
- Filters to scores > 0.95
- Enriches matches with vendor name and account numeric code from Datomic
## Cross-Cutting Behaviors
### Approval Workflow
- Approval statuses: `unapproved`, `approved`, `requires-feedback`, `excluded`, `suppressed`
- Transactions start as `unapproved` on import
- `suppressed` transactions are excluded from all list queries (including GraphQL)
- `requires-feedback` appears in dashboard tasks card
- Bulk status changes available via GraphQL mutation `bulk_change_transaction_status` (admin only)
- Locked transactions (before `client/locked-until` or `bank-account/start-date`) cannot be modified
### Coding Transactions to Accounts
- Transactions can be coded with one or more expense accounts
- Account total must equal 100% of transaction amount (validated server-side)
- Location must match account's fixed location if one is set
- Location "Shared" distributes amount proportionally across all client locations
- Location "A" is reserved for liabilities/equities/assets
- Bulk coding available via GraphQL mutation `bulk_code_transactions` (admin only)
### Bank Account Filtering
- Bank account filter only appears when a client is selected
- Filter dynamically updates via HTMX when `clientSelected` event fires
- Validates that selected bank account belongs to current client (`wrap-ensure-bank-account-belongs`)
- Defaults to "All" if selected account doesn't belong to current client
### CSV Export
- CSV route uses same filters as table view
- Exports all matching rows (bypasses pagination)
- Includes ID column in CSV but not in HTML view
### Payments & Linking
- Transactions can be linked to payments (auto-matched on import by check number or amount)
- Linking creates a cleared payment and sets transaction to `approved` with Accounts Payable account
- Unlinking a transaction reverts it to `unapproved` and clears payment/accounts
- Autopay invoices: transaction can pay multiple invoices, creating a payment that clears them all
- Unpaid invoices: similar flow for outstanding balances
### Permissions
- View transactions: `:activity :view :subject :transaction`
- Insights page: `:activity :insights :subject :transaction`
- Bulk status change: admin only (`assert-admin`)
- Bulk coding: admin only
- Edit transaction: power user, with client visibility check
- Match/unlink transactions: power user
- All SSR routes redirect unauthenticated users to `/login`
### Import Processing
- Transactions imported with `transaction->txs` pipeline:
1. Assign client and bank account
2. Set initial status to `unapproved`
3. Extract check number from description if present
4. Attempt auto-match to pending payment
5. Attempt auto-match to expected deposit
6. Apply transaction rules for auto-coding
7. Apply default vendor if set
- Deduplication via SHA-256 of `date-bank-account-description-amount-index-client`
- Suppressed transactions are skipped on re-import
## Edge Cases
### Empty Filter Results
- Table renders empty state when no transactions match filters
- Sum amount shows `$0.00`
- Pagination controls still render but show 0 results
### Invalid Bank Account Selection
- If user selects a bank account then switches to a different client that doesn't own that account, filter resets to "All"
- `wrap-ensure-bank-account-belongs` middleware enforces this
### Exact Match ID
- When `exact-match-id` is present, all other filters are ignored
- Query directly pulls the single transaction by ID (must belong to a visible client)
- Renders as a removable pill in the filter area
### No Client Selected
- Bank account filter area renders empty (no radio cards)
- All other filters still function
- Query uses all visible clients
### Insights with No Recommendations
- Insights page shows empty grid when no unapproved transactions have recommendations
- "That's the last of 'em!" message appears at bottom when infinite scroll exhausts results
### Disappearing Recommendations
- After coding or rejecting, the row animates out via Alpine.js (`x-data` with `mount-then-disappear`)
- The table does not auto-refresh; user must reload page to see new recommendations
### Pinecone Unavailable
- If Pinecone API fails, the explain modal will show only the target transaction with no similar matches
- No explicit error handling for Pinecone HTTP failures
## Test Data Requirements
### Users
- Admin user with `:user/role "admin"`
- Power user with access to specific clients
- Regular user with client visibility restrictions
- Unauthenticated user (for redirect tests)
### Clients
- Minimum 2 clients with different locations
- Client with multiple bank accounts
- Client with single location (to test client column hiding)
### Bank Accounts
- Bank account with `bank-account/name` and `bank-account/numeric-code`
- Bank account with `bank-account/start-date` (to test locked transactions)
- Multiple bank accounts under same client
### Transactions
- Transactions with all approval statuses
- Transactions with and without vendors
- Transactions with positive and negative amounts
- Transactions linked to payments
- Transactions with `outcome-recommendation` (for insights)
- Transactions with `exact-match-id` parameter
- Transactions dated before and after `client/locked-until`
### Vendors
- Vendor with name for typeahead matching
- Vendor linked to transactions
### Accounts
- Account with fixed location
- Account without fixed location
- Account with numeric code (for insights display)
### Payments
- Pending payment with matching check number and amount
- Cleared payment linked to transaction
## Dependencies
### External Services
- **Datomic**: All transaction data stored and queried via Datomic
- **Pinecone**: Vector similarity search for transaction insights (explain feature)
- **Solr**: Index updated on transaction changes (`solr/touch-with-ledger`)
### Frontend Libraries
- **HTMX**: All SSR interactions (filtering, sorting, pagination, insights coding)
- **Alpine.js**: Filter state (exact match pill), row disappear animations
- **Chart.js**: Not used on transaction pages
### Middleware Stack
- `wrap-must {:activity :view :subject :transaction}`: Enforces view permission
- `wrap-client-redirect-unauthenticated`: Auth redirects
- `wrap-schema-enforce`: Validates query params against Malli schema
- `wrap-apply-sort`: Applies server-side sorting
- `wrap-ensure-bank-account-belongs`: Validates bank account filter
- `wrap-copy-qp-pqp`: Copies query params to parsed query params
### GraphQL Mutations (Legacy/Client-Side)
- `bulk_change_transaction_status`: Admin bulk approval status changes
- `bulk_code_transactions`: Admin bulk coding with vendor/accounts
- `edit_transaction`: Single transaction edit
- `match_transaction`: Link to existing payment
- `match_transaction_autopay_invoices`: Create payment for autopay invoices
- `match_transaction_unpaid_invoices`: Create payment for unpaid invoices
- `unlink_transaction`: Remove payment link
- `delete_transactions`: Soft delete (suppress) or hard delete
- `match_transaction_rules`: Apply transaction rules to matching transactions