Add behavior documentation covering all SSR and legacy SPA pages: - Testing strategy and type definitions (unit/integration/UI) - Dashboard, Invoice, Payment, Transaction, Ledger pages - Company/Settings, POS, Admin, Search, Auth pages - Legacy SPA behavior docs (no UI tests until migrated) - Edge cases, test data requirements, and dependencies per subsystem Total: 3,600+ lines of behavior documentation to guide test authorship.
15 KiB
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 todescription-simplein italics - Date rendered in
normal-dateformat (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-searchendpoint) - Bank Account: Radio card selector with "All" plus client's bank accounts; dynamically reloads via
clientSelectedevent - 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:500msorkeyup delay:1000ms)
Sorting
- Sortable columns: Client, Vendor, Date, Amount, Description
- Default sort is ascending on the implicit
sort-defaultfield fromscan-transactions - Sorting is handled server-side with
add-sorter-fieldsgenerating 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.transactionsbut no handlers are wired inauto-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 formPOST /transaction2/new- Form submissionGET /transaction2/new/location-select- Location selector sub-componentGET /transaction2/new/account-typeahead- Account typeahead sub-componentGET /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 pageGET /transaction2/external-import-new- Import form (CSV/text paste)POST /transaction2/external-import-new/parse- Parse pasted dataPOST /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-recommendationdata - 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 Namewith a count badge - Button color is
:primaryif the recommendation was seen by client,:secondaryotherwise
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-addedclass and Alpine.js disappear animation
- Sets approval status to
- Reject (Disapprove):
DELETE /transaction/insights/disapprove/:id- Clears
outcome-recommendationon transaction - Row re-renders with
live-removedclass and disappear animation
- Clears
- 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
unapprovedon import suppressedtransactions are excluded from all list queries (including GraphQL)requires-feedbackappears in dashboard tasks card- Bulk status changes available via GraphQL mutation
bulk_change_transaction_status(admin only) - Locked transactions (before
client/locked-untilorbank-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
clientSelectedevent 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
approvedwith Accounts Payable account - Unlinking a transaction reverts it to
unapprovedand 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->txspipeline:- Assign client and bank account
- Set initial status to
unapproved - Extract check number from description if present
- Attempt auto-match to pending payment
- Attempt auto-match to expected deposit
- Apply transaction rules for auto-coding
- 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-belongsmiddleware enforces this
Exact Match ID
- When
exact-match-idis 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-datawithmount-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/nameandbank-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-idparameter - 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 permissionwrap-client-redirect-unauthenticated: Auth redirectswrap-schema-enforce: Validates query params against Malli schemawrap-apply-sort: Applies server-side sortingwrap-ensure-bank-account-belongs: Validates bank account filterwrap-copy-qp-pqp: Copies query params to parsed query params
GraphQL Mutations (Legacy/Client-Side)
bulk_change_transaction_status: Admin bulk approval status changesbulk_code_transactions: Admin bulk coding with vendor/accountsedit_transaction: Single transaction editmatch_transaction: Link to existing paymentmatch_transaction_autopay_invoices: Create payment for autopay invoicesmatch_transaction_unpaid_invoices: Create payment for unpaid invoicesunlink_transaction: Remove payment linkdelete_transactions: Soft delete (suppress) or hard deletematch_transaction_rules: Apply transaction rules to matching transactions