# 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: 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 **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: 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 **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: 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 **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 | [ ] | ### 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. ### 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)