Files
integreat/docs/testing/behaviors/legacy-spa.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

20 KiB

Legacy SPA Behaviors

Overview

These pages are rendered client-side via Reagent/Re-frame. They use GraphQL for data fetching. They are being migrated to HTMX SSR. Behavior docs exist for reference but NO UI tests should be written for these pages until migrated.

Architecture

  • Routing: Bidi client-side routes (src/cljc/auto_ap/client_routes.cljc)
  • State: Re-frame subscriptions and events
  • Data: GraphQL via custom graphql effect (auto-ap.effects)
  • Permissions: auto-ap.permissions/can? with role-based checks

Role Permissions Summary

Role Transaction Page Ledger Page Vendor Create/Edit
admin full full full
power-user full full full
manager full blocked full
user full full full
read-only blocked full blocked

Pages

Home (/)

Purpose

Dashboard showing financial overview for the selected client: top expense categories (pie chart), upcoming bills (bar chart), and cash flow projection (interactive bar chart with table).

User Flows

  1. User lands on /
  2. Page loads data for currently selected client
  3. If user has access to multiple clients, shows note: "these reports are for [client]. Please choose a specific customer for their report."
  4. User can switch cash flow range: 7/30/60/90/120/150/180 days
  5. Cash flow bars are clickable and redirect to unpaid invoices for that date

GraphQL Queries Used

query HomeDashboard($clientId: id) {
  expense_account_stats(client_id: $clientId) {
    account { id name }
    total
  }
  invoice_stats(client_id: $clientId) {
    name
    paid
    unpaid
  }
  cash_flow(client_id: $clientId) {
    beginning_balance
    outstanding_payments
    invoices_due_soon { due outstanding_balance invoice_number vendor { id name } }
    upcoming_credits { date amount identifier }
    upcoming_debits { date amount identifier }
  }
}

Behaviors to Test (when migrated)

  • Happy path: Dashboard loads with all three chart sections
  • Multi-client note: Displays when user hasn't selected a specific client
  • Cash flow ranges: Switching between 7-180 day views updates chart and table
  • Cash flow table: Shows invoices, upcoming debits/credits with days-until
  • Edge case: No data for client shows empty charts gracefully
  • Error states: GraphQL errors show loading state appropriately

Login (/login)

Purpose

Authentication page with Google OAuth login.

User Flows

  1. User visits /login
  2. Shows "Login with Google" button
  3. Button links to Google OAuth with optional redirect-to query param
  4. After auth failure/logout, may show logout reason notification

GraphQL Queries/Mutations Used

None.

Behaviors to Test (when migrated)

  • Happy path: Shows login button linking to Google OAuth
  • Redirect: Preserves redirect-to query parameter in OAuth URL
  • Logout reason: Displays warning notification when logout-reason is set
  • Edge case: Already authenticated user visiting login page

Needs Activation (/needs-activation)

Purpose

Shown when authenticated user's account is not yet activated.

User Flows

  1. User logs in but account is inactive
  2. Shows message: "Sorry, your user is not activated yet. Please have Ben Skinner enable your account."
  3. "here" link clears user state and redirects to login

GraphQL Queries/Mutations Used

None.

Behaviors to Test (when migrated)

  • Happy path: Shows activation required message
  • Relogin link: Clears user state and redirects to login

Transactions (/transactions/, /transactions/unapproved, /transactions/approved, /transactions/requires-feedback, /transactions/excluded)

Purpose

Transaction list with filtering, editing, and bulk admin operations. Different routes show different approval statuses.

User Flows

  1. User navigates to transactions page
  2. Default date filter: last 1 month
  3. Side bar allows filtering by: vendor, account, bank account, date range, amount range, location, import batch, description, linked status
  4. Table shows transactions with checkbox selection (admin only)
  5. Clicking row opens edit side bar
  6. Admin can: bulk code, bulk delete, suppress, or manual Yodlee import

GraphQL Queries/Mutations Used

query TransactionPage($filters: transaction_filters) {
  transaction_page(filters: $filters) {
    data { 
      id amount memo location approval_status check_number is_locked
      matched_rule { note id }
      vendor { name id }
      accounts { id amount location account { name id location numeric_code } }
      date yodlee_merchant { name yodlee_id id } plaid_merchant { name id }
      post_date expected_deposit { id date } forecast_match { id identifier }
      status description_original
      payment { check_number s3_url id date }
      client { name id }
      bank_account { name yodlee_account_id current_balance }
    }
    total start end count
  }
}

mutation EditTransaction($transaction: edit_transaction) {
  edit_transaction(transaction: $transaction) { ... }
}

mutation DeleteTransactions($filters: transaction_filters, $ids: [id], $suppress: Boolean) {
  delete_transactions(filters: $filters, ids: $ids, suppress: $suppress) { message }
}

mutation BulkCodeTransactions($filters: transaction_filters, $ids: [id], $vendor: id, $approval_status: transaction_approval_status, $accounts: [edit_percentage_account]) {
  bulk_code_transactions(filters: $filters, ids: $ids, vendor: $vendor, approval_status: $approval_status, accounts: $accounts) { message }
}

Behaviors to Test (when migrated)

  • Happy path: Loads transaction list with default 1-month filter
  • Route variants: Each approval-status route applies correct default filter
  • Filtering: Side bar filters trigger debounced data refresh (800ms)
  • Pagination: Start/per-page params work correctly
  • Edit transaction: Update vendor, approval status, memo, expense accounts
  • Edit validation: Account total must equal transaction amount; locations must be valid
  • Bulk delete (admin): Delete selected transactions or all visible transactions
  • Bulk suppress (admin): Mark transactions as suppressed instead of deleting
  • Bulk code (admin): Apply vendor/account rules to multiple transactions
  • Manual import (admin): Import transactions via manual Yodlee import dialog
  • Error states: Locked transactions cannot be edited/deleted; validation errors display inline
  • Permissions: Non-admin users don't see checkboxes or action buttons

Ledger (/ledger/)

Purpose

General ledger showing journal entries with line items. Supports filtering and CSV export.

User Flows

  1. User navigates to /ledger/
  2. Default date filter: last 1 month
  3. Side bar filters: vendor, account, bank account, date range, amount range, location
  4. Table shows journal entries with expandable line items
  5. Admin can export to CSV

GraphQL Queries/Mutations Used

query LedgerPage($filters: ledger_filters) {
  ledger_page(filters: $filters) {
    journal_entries {
      id source original_entity amount note cleared_against alternate_description
      vendor { name id } client { name id }
      line_items { id debit credit location running_balance account { id name } }
      date
    }
    total start end
  }
}

query LedgerCsv($filters: ledger_filters) {
  ledger_csv(filters: $filters) { csv_content_b64 }
}

Behaviors to Test (when migrated)

  • Happy path: Loads ledger with default 1-month filter
  • Filtering: Date range, vendor, account, location filters work
  • CSV export: Admin can export filtered results as base64 CSV download
  • Pagination: Virtual pagination controls
  • Permissions: Manager role sees "Not authorized"

Profit and Loss (/ledger/profit-and-loss)

Purpose

Financial report showing revenue and expenses over configurable periods. Supports multi-company and PDF export.

User Flows

  1. User selects companies (multi-select typeahead)
  2. User selects period preset (13 periods, 12 months, last week, week-to-date, last month, month-to-date, year-to-date, last calendar year, full year)
  3. Optional: Include deltas, Column per location
  4. Click "Run" to generate report
  5. Admin can "Export" to PDF
  6. Clicking report cells opens ledger detail side bar

GraphQL Queries/Mutations Used

query ProfitAndLoss($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
  profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
    periods { accounts { name amount client_id account_type id count numeric_code location } }
  }
}

query ProfitAndLossPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
  profit_and_loss_pdf(...) { url name }
}

Behaviors to Test (when migrated)

  • Happy path: Generate P&L for single client with default period
  • Multi-company: Select multiple clients and generate combined report
  • Period presets: All preset buttons populate correct date ranges
  • Advanced mode: Custom period date pickers
  • Include deltas: Shows period-over-period changes
  • Column per location: Breaks out by location (mutually exclusive with deltas)
  • PDF export: Admin can generate and download PDF; single-client shows email link
  • Ledger drill-down: Clicking cells opens ledger entries side bar for that account/period
  • Permissions: Manager role sees "Not authorized"

Cash Flows (/ledger/cash-flows)

Purpose

Statement of cash flows report. Same control structure as P&L but different report format.

User Flows

Same as Profit and Loss but generates cash flow statement.

GraphQL Queries/Mutations Used

query CashFlows($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
  profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
    periods { accounts { name amount debits credits client_id account_type id count numeric_code location } }
  }
}

query CashFlowsPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
  cash_flows_pdf(...) { url name }
}

Behaviors to Test (when migrated)

  • Happy path: Generate cash flows report
  • Same controls as P&L: Companies, periods, deltas, location columns
  • PDF export: Admin-only export button
  • Ledger drill-down: Clicking cells opens ledger detail
  • Permissions: Manager role sees "Not authorized"

Profit and Loss Detail (/ledger/profit-and-loss-detail)

Purpose

Detailed journal entry report broken down by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable).

User Flows

  1. Select companies
  2. Select date range (start/end date pickers)
  3. Click "Run" to generate detail report
  4. Shows journal entries grouped by category and account with running balances
  5. Admin can export to PDF

GraphQL Queries/Mutations Used

query JournalDetailReport($clientIds: [id], $dateRange: date_range, $categories: [category]) {
  journal_detail_report(client_ids: $clientIds, date_range: $dateRange, categories: $categories) {
    categories {
      category client_id location
      account { numeric_code name }
      journal_entries { description date debit credit location running_balance account { id name } }
      total
    }
  }
}

query JournalDetailReportPdf($clientIds: [id], $dateRange: date_range, $categories: [category]) {
  journal_detail_report_pdf(...) { url name }
}

Behaviors to Test (when migrated)

  • Happy path: Generate detail report with default 2-week range
  • Category breakdown: Report includes all 6 category sections plus Gross Profit, Overhead, Net Profit summaries
  • Date range filtering: Start/end dates filter journal entries
  • PDF export: Admin can export to PDF with email link for single client
  • Permissions: Manager role sees "Not authorized"

Balance Sheet (/ledger/balance-sheet)

Purpose

Balance sheet report as of a specific date, with optional prior-year comparison.

User Flows

  1. Select companies
  2. Select report date
  3. Optionally enable "Include comparison" and select comparison date
  4. Click "Run" to generate balance sheet
  5. Admin can export to PDF
  6. Clicking cells opens ledger detail side bar

GraphQL Queries/Mutations Used

query BalanceSheet($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
  balance_sheet(client_ids: $clientIds, date: $date, comparison_date: $comparisonDate, include_comparison: $includeComparison) {
    balance_sheet_accounts { name amount account_type id numeric_code client_id }
    comparable_balance_sheet_accounts { name amount account_type id client_id numeric_code }
  }
}

query BalanceSheetPdf($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
  balance_sheet_pdf(...) { url name }
}

Behaviors to Test (when migrated)

  • Happy path: Generate balance sheet for current date
  • Comparison mode: Shows prior period side-by-side
  • Multi-company: Combines multiple clients
  • PDF export: Admin-only with email composition link for single client
  • Ledger drill-down: Clicking cells opens ledger entries filtered by account and date range
  • Permissions: Manager role sees "Not authorized"

External Ledger (/ledger/external)

Purpose

Shows only externally-imported journal entries. Admin can delete entries.

User Flows

  1. Admin navigates to /ledger/external
  2. Shows external journal entries with external_id
  3. Can select entries and delete them
  4. Admin can export to CSV

GraphQL Queries/Mutations Used

query ExternalLedger($filters: ledger_filters) {
  ledger_page(filters: $filters) {
    journal_entries { id external_id source ... }
    total start end
  }
}

mutation DeleteExternalLedger($filters: ledger_filters, $ids: [id]) {
  delete_external_ledger(filters: $filters, ids: $ids) { message }
}

Behaviors to Test (when migrated)

  • Happy path: Loads external-only ledger entries
  • Delete: Admin can delete selected entries (max 1000 at once)
  • CSV export: Same as main ledger
  • Permissions: Admin-only; non-admin sees "Not authorized"

External Import (/ledger/external-import)

Purpose

Manual import of ledger entries via tab-separated text paste.

User Flows

  1. Admin pastes tab-separated data into textarea
  2. Optional: indicates whether first row is header
  3. Click "Parse" to convert to table
  4. Table shows parsed rows: Id, Client, Source, Vendor, Date, Account, Location, Debit, Credit, Note, Cleared against
  5. Click "Import" to submit
  6. Results show: success count, ignored count, error count
  7. Errors show inline with dropdown explanations
  8. "Only show errors" filter available

GraphQL Queries/Mutations Used

mutation ImportLedger($entries: [ledger_entry_input]) {
  import_ledger(entries: $entries) {
    successful { external_id }
    existing { external_id }
    ignored { external_id }
    errors { external_id error }
  }
}

Behaviors to Test (when migrated)

  • Happy path: Paste TSV data, parse, import successfully
  • Header row: Checkbox to skip first row
  • Validation: Client code must exist, vendor must exist, date must be MM/dd/yyyy, debits=credits, amounts > 0
  • Lock check: Entries must be after client's locked-until date
  • Location validation: Location must belong to client or be "A"
  • Account validation: Account must exist and location must match account's required location
  • Error display: Errors show per-row with dropdown explanation
  • Success/ignored/existing: Status icons show import result per row
  • Permissions: Admin-only

Payments (/payments/)

Purpose

Already migrated to SSR. Legacy client route exists for navbar highlighting only. Actual payments page is served by payment SSR routes at /payment/.

GraphQL Queries/Mutations Used

N/A (SSR page)

Behaviors to Test (when migrated)

Already migrated. Test via SSR routes.


Reports (/reports/)

Purpose

Already migrated to SSR. Legacy client route exists for navbar highlighting only. Actual reports page is served by company/reports SSR routes.

GraphQL Queries/Mutations Used

N/A (SSR page)

Behaviors to Test (when migrated)

Already migrated. Test via SSR routes.


Admin Vendors (/admin/vendors)

Purpose

Already migrated to SSR. Legacy client route exists for navbar highlighting only. Actual vendor management is served by admin/vendor SSR routes.

GraphQL Queries/Mutations Used

N/A (SSR page)

Behaviors to Test (when migrated)

Already migrated. Test via SSR routes.


New Vendor (/vendor/new)

Purpose

Opens the home dashboard with the vendor creation dialog pre-opened.

User Flows

  1. User navigates to /vendor/new
  2. Loads home dashboard
  3. Opens vendor dialog if user has :vendor :create permission
  4. Dialog allows creating new vendor with name, terms, address, contacts, default account, etc.

GraphQL Queries/Mutations Used

mutation UpsertVendor($vendor: add_vendor) {
  upsert_vendor(vendor: $vendor) { id name ... }
}

Behaviors to Test (when migrated)

  • Happy path: Home loads with vendor dialog open
  • Permission check: Dialog only opens if user can :vendor :create
  • Vendor creation: Form submission creates vendor via GraphQL mutation
  • Validation: Only one terms override, schedule payment DOM override, and account override per client

Cross-Cutting Behaviors

GraphQL Query Patterns

  • All data fetching uses re-frame graphql effect with JWT token from :user in app-db
  • Queries use owns-state to track loading status per page
  • Results flow through ::data-page/received event which stores data and syncs URL query params
  • Filter changes are debounced (800ms) via dispatch-debounce

Client-Side State Management (Re-frame)

  • Data pages: data-page namespace provides reusable pagination/filtering state
    • ::data-page/params - merged filters + table params + query params
    • ::data-page/data - GraphQL response data
    • ::data-page/checked - selected rows for bulk operations
  • Forms: forms namespace manages edit dialogs with start-form, change-handler, save-succeeded
  • Status: status namespace tracks async operation states (:loading, :complete, :error)

Navigation Between Legacy Pages

  • Navbar uses Bidi path-for with routes/routes for legacy SPA links
  • Some navbar items (Payments, POS, Invoices) now link to ssr-routes/only-routes instead
  • Page components dispatch ::mounted on mount and ::unmounted on unmount to set up/tear down:
    • Data subscriptions
    • Forward event listeners
    • Track subscriptions (parameter change watchers)

Migration Notes

Actively Migrated / Already SSR

  • Payments (/payments/) - Fully migrated to /payment/ SSR routes
  • Reports (/reports/) - Fully migrated to /company/reports SSR routes
  • Admin Vendors (/admin/vendors) - Fully migrated to /admin/vendor SSR routes

Still Legacy SPA (prioritized by complexity)

  1. Transactions - High complexity (filters, edit form, bulk operations, manual import)
  2. Home/Dashboard - Medium complexity (charts, cash flow calculations)
  3. Ledger - Medium complexity (filters, CSV export)
  4. External Ledger - Low-medium complexity (subset of ledger + delete)
  5. External Import - Medium complexity (TSV parsing, validation, batch import)
  6. Profit and Loss - High complexity (multi-company, periods, PDF export)
  7. Cash Flows - High complexity (shares P&L infrastructure)
  8. Profit and Loss Detail - Medium complexity (category filtering, running balances)
  9. Balance Sheet - Medium complexity (comparison mode, drill-down)
  10. Login - Low complexity (static page)
  11. Needs Activation - Low complexity (static page)
  12. New Vendor - Low complexity (dialog on home page)

Migration Risks

  • Chart libraries: Home page uses Recharts (React). Replacement needed for SSR.
  • PDF generation: P&L, Cash Flows, Balance Sheet, P&L Detail all support PDF export via server-side generation.
  • Bulk operations: Transactions page has complex bulk coding/deletion with validation.
  • External import: TSV parsing and validation logic lives entirely in SPA; server-side equivalent needed.