Files
integreat/docs/testing/behaviors/transaction.md
Bryce b499d460f3 docs: add comprehensive test behavior documentation for all pages
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.
2026-05-04 12:15:20 -07:00

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