# 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)
