# 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