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.
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
Testing Patterns
Pattern: Grid Page Behaviors
Most list pages in Integreat follow the same pattern:
- Fetch IDs via Datomic query with filters
- Hydrate results via
pull-many
- Render table with sortable columns
- Support selection (individual / all / all-filtered)
- Action buttons appear conditionally based on permissions and selection state
Test implications: Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
Pattern: Admin Insights Behaviors
Insights pages display AI recommendations for coding transactions:
- Fetch unapproved transactions with
outcome-recommendation data
- Display recommendation buttons sorted by frequency
- Allow approving (coding) or rejecting recommendations inline
- Use infinite scroll instead of pagination
Test implications: Unit test the recommendation sorting and filtering logic. Integration test the approve/reject endpoints. UI test the infinite scroll and animation behaviors.
Pattern: Permission Gates
Every mutating operation checks:
assert-can-see-client — user has access to the client
assert-not-locked — transaction date >= client locked-until or bank account start-date
can? — user has the specific permission for the activity
Test implications: Integration test each gate independently. UI tests only verify the happy path with a permitted user.
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 |
[ ] |
| # |
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.
Basic Details
| # |
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.
External Transaction Entry
| # |
Behavior |
Test Strategy |
Status |
| 8.1 |
It should display an external transaction entry page at GET /transaction2/external-new |
UI |
[ ] |
CSV/Text Import
| # |
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 |
[ ] |
Admin Insights / Coding
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 Behaviors
| # |
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 |
[ ] |
Coding Behaviors
| # |
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 |
[ ] |
Bank Account Filtering Behaviors
| # |
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 |
[ ] |
CSV Export Behaviors
| # |
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 |
[ ] |
Payments & Linking Behaviors
| # |
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 |
[ ] |
Permission Behaviors
| # |
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 |
[ ] |
Import Processing Behaviors
| # |
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 |
[ ] |
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
| 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 |
Existing Tests to Preserve
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
- 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)