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,300 +2,309 @@
## 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.
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.
## Routes & Pages
**Testing Philosophy**
- Prefer unit tests for pure business logic (calculations, validations, transformations)
- Use integration tests for database interactions and cross-system flows
- Use UI tests only for end-to-end happy paths that touch multiple pages
- Every behavior must be user-visible; no tests for implementation details
| 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
## Testing Patterns
### Transaction List
### Pattern: Grid Page Behaviors
Most list pages in Integreat follow the same pattern:
1. Fetch IDs via Datomic query with filters
2. Hydrate results via `pull-many`
3. Render table with sortable columns
4. Support selection (individual / all / all-filtered)
5. Action buttons appear conditionally based on permissions and selection state
#### 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
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
#### 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`)
### Pattern: Admin Insights Behaviors
Insights pages display AI recommendations for coding transactions:
1. Fetch unapproved transactions with `outcome-recommendation` data
2. Display recommendation buttons sorted by frequency
3. Allow approving (coding) or rejecting recommendations inline
4. Use infinite scroll instead of pagination
#### 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
**Test implications:** Unit test the recommendation sorting and filtering logic. Integration test the approve/reject endpoints. UI test the infinite scroll and animation behaviors.
#### Pagination
- Default 25 rows per page
- Pagination controls rendered by grid helper
- Total matching count and sum of all matching amounts displayed
### Pattern: Permission Gates
Every mutating operation checks:
1. `assert-can-see-client` — user has access to the client
2. `assert-not-locked` — transaction date >= client locked-until or bank account start-date
3. `can?` — user has the specific permission for the activity
#### Actions
- "Add Transaction" button (primary color) links to `::route/new` (not yet implemented)
- Row action buttons (edit/delete) are commented out in source
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
#### 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
## Transaction List Page
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 1.1 | It should display a table with columns: Client, Vendor, Description, Date, Amount, Links | UI | [ ] |
| 1.2 | It should hide the Client column when only one client with one location is selected | Integration | [ ] |
| 1.3 | It should display the description from `description-original` in the Description column | UI | [ ] |
| 1.4 | It should fall back to `description-simple` in italics in the Vendor column when no vendor is assigned | UI | [ ] |
| 1.5 | It should render dates in `MM/DD/YYYY` format | UI | [ ] |
| 1.6 | It should right-align amounts and format them as `$X,XXX.XX` | UI | [ ] |
| 1.7 | It should display a links dropdown with links to associated Payment page or Client Overrides | UI | [ ] |
| 1.8 | It should show checkboxes for bulk selection on each row | UI | [ ] |
| 1.9 | It should group table rows by vendor name (or "No vendor") when sorted by Vendor | Integration | [ ] |
| 1.10 | It should show the grid title "Transaction" and entity name "register" | UI | [ ] |
| 1.11 | It should display a breadcrumb showing "Transactions" linking to the list page | UI | [ ] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 2.1 | It should filter transactions by vendor typeahead selection | Integration | [ ] |
| 2.2 | It should filter transactions by bank account via radio card selector with "All" plus client's bank accounts | Integration | [ ] |
| 2.3 | It should dynamically reload the bank account filter when the `clientSelected` event fires | Integration | [ ] |
| 2.4 | It should filter transactions by date range (start/end dates) | Integration | [ ] |
| 2.5 | It should filter transactions by description with 1000ms debounced search | Integration | [ ] |
| 2.6 | It should filter transactions by amount range (min/max) | Integration | [ ] |
| 2.7 | It should support exact-match navigation to a specific transaction by ID, bypassing other filters | Integration | [ ] |
| 2.8 | It should render the exact-match ID as a removable pill when present in query params | UI | [ ] |
| 2.9 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.1 | It should sort by client name ascending/descending | Integration | [ ] |
| 3.2 | It should sort by vendor name ascending/descending, handling missing vendors by grounding empty string | Integration | [ ] |
| 3.3 | It should sort by description ascending/descending | Integration | [ ] |
| 3.4 | It should sort by date ascending/descending | Integration | [ ] |
| 3.5 | It should sort by amount ascending/descending | Integration | [ ] |
| 3.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
| 3.7 | It should default to ascending sort on the implicit `sort-default` field | Integration | [ ] |
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.1 | It should display 25 transactions per page by default | Integration | [ ] |
| 4.2 | It should display the total matching count and sum of all matching amounts | Integration | [ ] |
| 4.3 | It should render pagination controls via the grid helper | UI | [ ] |
### Action Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 5.1 | It should display an "Add Transaction" button (primary color) linking to the new transaction route | UI | [ ] |
### CSV Export Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 6.1 | It should export all filtered results (not just the current page) as CSV | Integration | [ ] |
| 6.2 | It should include CSV headers: Id, Client, Vendor, Description, Date, Amount | Integration | [ ] |
| 6.3 | It should use raw data values instead of formatted display values in the CSV | Integration | [ ] |
---
## 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
### Basic Details
### External Import
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 7.1 | It should display a new transaction form at `GET /transaction2/new` | UI | [ ] |
| 7.2 | It should allow selecting a location via location selector sub-component | UI | [ ] |
| 7.3 | It should allow selecting an account via account typeahead sub-component | UI | [ ] |
| 7.4 | It should allow adding line items via line item sub-component | UI | [ ] |
| 7.5 | It should submit the new transaction via `POST /transaction2/new` | Integration | [ ] |
---
## 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
### External Transaction Entry
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`
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.1 | It should display an external transaction entry page at `GET /transaction2/external-new` | UI | [ ] |
### Admin Insights / Coding
### CSV/Text Import
#### 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!"
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.2 | It should display an import form for CSV/text paste at `GET /transaction2/external-import-new` | UI | [ ] |
| 8.3 | It should parse pasted data via `POST /transaction2/external-import-new/parse` | Integration | [ ] |
| 8.4 | It should execute the import via `POST /transaction2/external-import-new/import` | Integration | [ ] |
| 8.5 | It should deduplicate transactions via SHA-256 synthetic keys during import | Unit | [ ] |
| 8.6 | It should auto-match imported transactions to existing pending payments by check number or amount | Integration | [ ] |
| 8.7 | It should auto-match imported transactions to expected deposits | Integration | [ ] |
| 8.8 | It should auto-code imported transactions via transaction rules | Integration | [ ] |
| 8.9 | It should categorize imports as `:import`, `:extant`, `:suppressed`, `:error`, or `:not-ready` | Integration | [ ] |
#### 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
## Admin Insights / Coding
#### 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
### Insights Page Display
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 9.1 | It should display the insights page at `/transaction/insights` for admin users | UI | [ ] |
| 9.2 | It should show the title "Transaction Insights" and breadcrumbs: Transactions > Insights | UI | [ ] |
| 9.3 | It should display a data grid with headers: Client, Account, Date, Description, Amount, Actions | UI | [ ] |
| 9.4 | It should show up to 50 recommendations at a time with no pagination | Integration | [ ] |
| 9.5 | It should implement infinite scroll via `hx-trigger="intersect once"` on the last row | UI | [ ] |
| 9.6 | It should display "That's the last of 'em!" when no more recommendations are available | UI | [ ] |
| 9.7 | It should show an empty grid when no unapproved transactions have recommendations | UI | [ ] |
### Recommendation Rows
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 10.1 | It should show unapproved transactions from the last 300 days that have `outcome-recommendation` data | Integration | [ ] |
| 10.2 | It should display each row with: client code, bank account code, date, description, amount | UI | [ ] |
| 10.3 | It should display the amount as a rounded dollar tag (green for positive, red for negative) | UI | [ ] |
| 10.4 | It should show up to 3 recommendation buttons per row, sorted by frequency (highest count first) | Integration | [ ] |
| 10.5 | It should display each recommendation button as `Vendor Name | Account Name` with a count badge | UI | [ ] |
| 10.6 | It should render recommendation buttons in `:primary` color if seen by client, `:secondary` otherwise | UI | [ ] |
### Coding Actions
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 11.1 | It should approve and code a transaction via `POST /transaction/insights/code/:id` | Integration | [ ] |
| 11.2 | It should set the approval status to `approved` and assign vendor and account when coding | Integration | [ ] |
| 11.3 | It should distribute the amount across valid locations using `spread-cents` when coding | Unit | [ ] |
| 11.4 | It should re-render the row with `live-added` class and Alpine.js disappear animation after coding | UI | [ ] |
| 11.5 | It should reject a recommendation via `DELETE /transaction/insights/disapprove/:id` | Integration | [ ] |
| 11.6 | It should clear `outcome-recommendation` on the transaction when rejecting | Integration | [ ] |
| 11.7 | It should re-render the row with `live-removed` class and disappear animation after rejecting | UI | [ ] |
| 11.8 | It should open an explain modal via `GET /transaction/insights/explain/:id` | UI | [ ] |
| 11.9 | It should display similar transactions from Pinecone vector search in the explain modal | Integration | [ ] |
| 11.10 | It should display Date, Description, Amount, Vendor, Account, and Similarity Score for similar transactions | UI | [ ] |
| 11.11 | It should filter similar transactions to scores > 0.95 | Unit | [ ] |
| 11.12 | It should show the top 10 similar transactions plus the target transaction highlighted | UI | [ ] |
| 11.13 | It should show only the target transaction with no similar matches if Pinecone API fails | Integration | [ ] |
---
## 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
### Approval Workflow Behaviors
### 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)
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 12.1 | It should set transactions to `unapproved` status on import | Integration | [ ] |
| 12.2 | It should exclude `suppressed` transactions from all list queries including GraphQL | Integration | [ ] |
| 12.3 | It should show `requires-feedback` transactions in the dashboard tasks card | Integration | [ ] |
| 12.4 | It should allow admin-only bulk status changes via GraphQL mutation `bulk_change_transaction_status` | Integration | [ ] |
| 12.5 | It should block modifying locked transactions (before `client/locked-until` or `bank-account/start-date`) | Integration | [ ] |
### 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
### Coding Behaviors
### 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
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 13.1 | It should allow coding transactions with one or more expense accounts | Integration | [ ] |
| 13.2 | It should validate that account totals equal 100% of the transaction amount server-side | Unit + Integration | [ ] |
| 13.3 | It should require the location to match the account's fixed location if one is set | Integration | [ ] |
| 13.4 | It should distribute amounts proportionally across all client locations when location is "Shared" | Unit | [ ] |
| 13.5 | It should reserve location "A" for liabilities/equities/assets | Integration | [ ] |
| 13.6 | It should allow admin-only bulk coding via GraphQL mutation `bulk_code_transactions` | Integration | [ ] |
### 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
### Bank Account Filtering Behaviors
### 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`
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 14.1 | It should show the bank account filter only when a client is selected | UI | [ ] |
| 14.2 | It should dynamically update the bank account filter via HTMX when `clientSelected` event fires | Integration | [ ] |
| 14.3 | It should validate that the selected bank account belongs to the current client | Integration | [ ] |
| 14.4 | It should default to "All" if the selected account doesn't belong to the current client | Integration | [ ] |
### 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
### CSV Export Behaviors
## Edge Cases
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 15.1 | It should use the same filters for CSV export as the table view | Integration | [ ] |
| 15.2 | It should export all matching rows bypassing pagination | Integration | [ ] |
| 15.3 | It should include the ID column in CSV but not in the HTML view | Integration | [ ] |
### 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
### Payments & Linking Behaviors
### 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
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 16.1 | It should auto-match transactions to payments by check number or amount on import | Integration | [ ] |
| 16.2 | It should create a cleared payment and set the transaction to `approved` with Accounts Payable account when linking | Integration | [ ] |
| 16.3 | It should revert the transaction to `unapproved` and clear payment/accounts when unlinking | Integration | [ ] |
| 16.4 | It should allow a transaction to pay multiple autopay invoices, creating a payment that clears them all | Integration | [ ] |
| 16.5 | It should allow a transaction to pay multiple unpaid invoices for outstanding balances | Integration | [ ] |
### 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
### Permission Behaviors
### No Client Selected
- Bank account filter area renders empty (no radio cards)
- All other filters still function
- Query uses all visible clients
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 17.1 | It should require `:activity :view :subject :transaction` permission to view transactions | Integration | [ ] |
| 17.2 | It should require `:activity :insights :subject :transaction` permission to access the insights page | Integration | [ ] |
| 17.3 | It should restrict bulk status changes to admin only | Integration | [ ] |
| 17.4 | It should restrict bulk coding to admin only | Integration | [ ] |
| 17.5 | It should require power user role with client visibility check to edit transactions | Integration | [ ] |
| 17.6 | It should require power user role to match/unlink transactions | Integration | [ ] |
| 17.7 | It should redirect unauthenticated users to `/login` for all SSR routes | Integration | [ ] |
### 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
### Import Processing Behaviors
### 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
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 18.1 | It should assign client and bank account during import | Integration | [ ] |
| 18.2 | It should set initial status to `unapproved` on import | Integration | [ ] |
| 18.3 | It should extract check number from description if present during import | Unit | [ ] |
| 18.4 | It should attempt auto-match to pending payment during import | Integration | [ ] |
| 18.5 | It should attempt auto-match to expected deposit during import | Integration | [ ] |
| 18.6 | It should apply transaction rules for auto-coding during import | Integration | [ ] |
| 18.7 | It should apply default vendor if set during import | Integration | [ ] |
| 18.8 | It should deduplicate via SHA-256 of `date-bank-account-description-amount-index-client` | Unit | [ ] |
| 18.9 | It should skip suppressed transactions on re-import | Integration | [ ] |
### 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
### Empty State Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 19.1 | It should render an empty state when no transactions match filters | UI | [ ] |
| 19.2 | It should show `$0.00` for sum amount when no transactions match | UI | [ ] |
| 19.3 | It should render pagination controls showing 0 results when no transactions match | UI | [ ] |
---
## 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)
| Entity | Requirements |
|--------|-------------|
| **Users** | Admin user with `:user/role "admin"`; power user with access to specific clients; regular user with client visibility restrictions; unauthenticated user |
| **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 (`unapproved`, `approved`, `requires-feedback`, `excluded`, `suppressed`); 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 |
### Clients
- Minimum 2 clients with different locations
- Client with multiple bank accounts
- Client with single location (to test client column hiding)
## Existing Tests to Preserve
### 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
- `test/clj/auto_ap/ssr/transaction_test.clj` — Transaction SSR routes and behaviors
- `test/clj/auto_ap/integration/routes/transaction_test.clj` — Transaction import routes
- `test/clj/auto_ap/integration/graphql/transactions.clj` — GraphQL transaction operations
## 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
- Datomic (primary store, history for unvoid)
- Pinecone (vector similarity search for transaction insights explain feature)
- Solr (index updated on transaction changes via `solr/touch-with-ledger`)
- HTMX (all SSR interactions: filtering, sorting, pagination, insights coding)
- Alpine.js (filter state for exact match pill, row disappear animations)