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

22 KiB

Invoice Behaviors

Overview

Invoices are the core entity of the Integreat accounts payable system. They represent bills from vendors that clients must pay. The invoice subsystem supports full lifecycle management from creation/import through payment and voiding, with comprehensive filtering, bulk operations, and OCR-based automated entry via Glimpse.

User value:

  • Track all vendor bills in one place
  • Pay invoices individually or in bulk via checks, debits, cash, or credit
  • Import invoices from CSV/PDF files with automatic data extraction
  • Spread expenses across multiple locations
  • Manage invoice status (unpaid, paid, voided, scheduled)

Routes & Pages

Route Handler Purpose
GET /invoices ::all-page List all invoices
GET /invoices/unpaid ::unpaid-page List unpaid invoices
GET /invoices/paid ::paid-page List paid invoices
GET /invoices/voided ::voided-page List voided invoices
GET /invoices/table ::table HTMX table refresh
GET /invoices/new ::new-wizard Create new invoice wizard
POST/PUT /invoices/new ::new-invoice-submit Submit new/edit invoice
PUT /invoices/new/navigate ::new-wizard-navigate Wizard step navigation
PUT /invoices/new/due-date ::due-date Calculate due date from vendor terms
PUT /invoices/new/scheduled-payment-date ::scheduled-payment-date Calculate scheduled payment from vendor autopay
PUT /invoices/new/account/prediction ::account-prediction Show vendor default account suggestion
PUT /invoices/new/total ::expense-account-total Calculate expense account total
PUT /invoices/new/balance ::expense-account-balance Calculate balance vs invoice total
GET /invoices/new/account/new ::new-wizard-new-account Add new expense account row
GET /invoices/new/account/location-select ::location-select Location dropdown for account
GET /invoices/pay-button ::pay-button Render pay button with selected invoice totals
GET /invoices/pay ::pay-wizard Pay selected invoices wizard
POST /invoices/pay ::pay-submit Submit payment
POST /invoices/pay/using-credit ::pay-using-credit Pay using vendor credit balance
PUT /invoices/pay/navigate ::pay-wizard-navigate Pay wizard step navigation
GET /invoices/bulk-delete ::bulk-delete Bulk void confirmation dialog
DELETE /invoices/bulk-delete ::bulk-delete-confirm Execute bulk void
GET /invoices/bulk-edit ::bulk-edit Bulk edit expense accounts wizard
PUT /invoices/bulk-edit ::bulk-edit-submit Submit bulk edit
GET /invoices/bulk-edit/account ::bulk-edit-new-account Add new account row in bulk edit
PUT /invoices/bulk-edit/total ::bulk-edit-total Calculate percentage total
PUT /invoices/bulk-edit/balance ::bulk-edit-balance Calculate percentage balance
DELETE /invoices/:id ::delete Void single invoice
PUT /invoices/:id/undo-autopay ::undo-autopay Undo scheduled autopay
PUT /invoices/:id/unvoid ::unvoid Restore voided invoice
PUT /invoices/:id/edit ::edit-wizard Edit existing invoice
GET /invoices/import ::import-page Import invoices page
POST /invoices/import/upload ::import-file Upload and parse invoice files
GET /invoices/import/table ::import-table Import table refresh
DELETE /invoices/import/:id/disapprove ::disapprove Remove pending imported invoice
PUT /invoices/import/:id/approve ::approve Approve pending imported invoice
DELETE /invoices/import/disapprove ::bulk-disapprove Bulk remove pending imports
PUT /invoices/import/approve ::bulk-approve Bulk approve pending imports

Behaviors by Page

Invoice List Page

Unit Tests

  • fetch-ids generates correct Datomic queries for all filter combinations
  • fetch-page returns [results count outstanding-sum total-sum] tuple
  • sum-outstanding and sum-total-amount calculate aggregates correctly
  • selected->ids resolves "all selected" vs individual selection
  • hydrate-results groups and orders pulled entities by input IDs
  • payable-ids filters to only unpaid, non-scheduled invoices
  • can-undo-autopayment validates invoice state for undo eligibility

Integration Tests

  • Happy path: Load list page with invoices, verify table renders
  • Filter by vendor: Select vendor, table refreshes with filtered results
  • Filter by account: Select account, table refreshes
  • Filter by date range: Apply date range, results filtered
  • Filter by amount range: Set min/max amounts, results filtered
  • Filter by invoice number: Partial match on invoice number
  • Filter by check number: Search by check number
  • Exact match: Navigate from notification to specific invoice via exact-match-id
  • Sort by each column: Click column headers (client, vendor, date, due, invoice-number, total, outstanding-balance, location, description-original)
  • Pagination: Navigate pages, change per-page (default 25)
  • Selection: Check individual rows, select all, select all filtered
  • Action buttons appear conditionally: Based on permissions and selection state
  • Legacy redirects: Old routes redirect to new SSR routes

UI Tests (SSR)

  • Table renders with correct columns: Client, Vendor, Invoice #, Date, Due, Status, Account, Outstanding, Links
  • Status pills display correctly: Paid (primary), Voided (red), Scheduled (yellow), Unpaid (secondary)
  • Due date shows relative time: "today", "in X days", "X days ago" with color coding
  • Outstanding shows partial payment indicator when balance != total
  • Links dropdown shows payments, transactions, ledger entries, source files
  • Client column hidden when single client with single location
  • Row action buttons: Void (unpaid), Edit (unpaid/paid), Unvoid (voided), Undo autopay (eligible)
  • Pay button tooltip explains why disabled: no selection, multiple clients, mixed debit/credit
  • Break table grouping by vendor name when sorting by vendor

New Invoice Wizard

Unit Tests

  • new-form-schema validates required fields, money amounts, entity IDs
  • Vendor must have default expense account (check-vendor-default-account)
  • clientize-vendor applies client-specific terms, account overrides, autopay settings
  • assert-no-conflicting prevents duplicate invoice numbers per vendor/client
  • assert-invoice-amounts-add-up validates expense accounts sum to total
  • maybe-spread-locations spreads "Shared" location across all client locations
    • Existing tests in new_invoice_wizard_test.clj cover:
      • Equal spread across 2 locations
      • Negative amount spreading
      • Remainder distribution (33.34, 33.33, 33.33)
      • Negative remainder distribution
      • Single cent leftover tolerance (apply-total-delta-to-account)
  • $->cents and cents->$ round correctly
  • calculate-spread distributes base amount + remainder

Integration Tests

  • Happy path create:
    1. Open wizard, select client, vendor, date, invoice number, total
    2. Due date auto-calculates from vendor terms when client+date+vendor selected
    3. Scheduled payment auto-calculates from vendor autopay setting
    4. Save with default account → invoice created with vendor's default account
    5. Wizard navigates to "Next Steps" with Pay now / Add another / Close options
  • Happy path customize accounts:
    1. Select "Customize accounts" on basic details
    2. Add multiple expense account rows
    3. Select account, location (auto-populated from account or Shared), amount
    4. Total and balance update dynamically via HTMX
    5. Location dropdown updates when account changes
    6. Save validates totals match
  • Edit existing invoice:
    1. Open edit wizard for unpaid/paid invoice
    2. Vendor field disabled (cannot change)
    3. Modify amounts, add/remove accounts
    4. Save updates row in place

UI Tests (SSR)

  • Wizard renders as modal with Basic Details → Expense Accounts (optional) → Next Steps
  • Client typeahead searches companies
  • Vendor typeahead searches vendors, disabled when editing
  • Date inputs use normal date format
  • "Add due / scheduled payment date" link appears when dates not set
  • Account prediction radio shows vendor default account name
  • Expense account grid: Account, Location, Amount columns
  • New account row button adds empty row
  • Remove row button with Alpine.js transition
  • Total/Balance rows update on amount change
  • Validation errors display per field

Pay Wizard

Unit Tests

  • payment-form-schema validates payment structure
  • does-amount-exceed-outstanding? catches over/under payments
  • can-handwrite? validates single vendor and positive balance for handwritten checks
  • credit-only? detects when all vendor totals are negative (credit scenario)
  • pay-button* calculates vendor totals and detects credit payment scenarios

Integration Tests

  • Happy path - debit:
    1. Select unpaid invoices from same client
    2. Click Pay → Choose Payment Method modal opens
    3. Select bank account → click Debit
    4. Confirm "Pay in full" or switch to "Customize payments"
    5. Submit → payments created, checks printed if applicable
  • Happy path - print check:
    1. Select check-type bank account
    2. Choose "Print check"
    3. Payment processed, PDF URL returned for printing
  • Happy path - cash:
    1. Select cash-type bank account
    2. Payment processed immediately
  • Happy path - handwrite check:
    1. Single vendor, positive balance required
    2. Enter check number and date
    3. Payment created with pending status
  • Happy path - credit payment:
    1. Select invoices where vendor total is 0 (credit + debit balance)
    2. Pay button shows "Pay invoices using credit"
    3. Credit invoices offset payment invoices
    4. Balance payment created
  • Validation:
    • Cannot pay invoices from multiple clients
    • Cannot pay with mixed vendor debit/credit totals
    • Custom payment amounts cannot exceed outstanding balance
    • Handwritten check requires check number
    • Locked client dates blocked

UI Tests (SSR)

  • Pay button shows count and total amount
  • Warning pill appears when some selected invoices are locked/scheduled
  • Bank account cards show type-appropriate icons (cash=green, check=blue, credit=purple)
  • Tooltip on Pay button explains disabled state
  • Payment method selection buttons: Print check, With cash, Debit, Handwrite check
  • Payment details step shows invoice grid with vendor, number, total, pay amount
  • Mode toggle: "Pay in full" vs "Customize payments"
  • Success modal shows thumbs up, PDF download link for checks, scaling reminder

Bulk Edit

Unit Tests

  • assert-percentages-add-up validates percentages sum to 100%
  • maybe-code-accounts distributes expense accounts by percentage
    • Shared location spreads across all client locations
    • Rounds cents correctly with remainder handling
    • Validates account location matches

Integration Tests

  • Happy path:
    1. Select invoices via checkboxes
    2. Click "Bulk Edit" → wizard opens
    3. Add expense account rows with account, location, percentage
    4. Submit → all selected invoices coded with new expense accounts
  • Locked invoices excluded: Only invoices with date >= client locked-until are processed
  • Validation:
    • Percentages must sum to 100%
    • Account location must match account's configured location

UI Tests (SSR)

  • Wizard shows count of invoices to be edited
  • Account typeahead with location dropdown
  • Percentage input (displayed as whole number, stored as decimal)
  • Total/Balance percentage display
  • New account row button

Bulk Delete/Void

Unit Tests

  • void-invoices-internal:
    • Voids cash payments linked to selected invoices
    • Only voids invoices without linked invoice-payments
    • Only voids invoices with date >= client locked-until
    • Sets total, outstanding-balance, and expense account amounts to 0

Integration Tests

  • Happy path:
    1. Select invoices
    2. Click "Void selected" → confirmation modal
    3. Confirm → invoices voided, table refreshes with notification
  • Admin only: Bulk delete requires admin permission
  • Validation:
    • Paid invoices with linked payments require voiding payments first
    • Locked dates blocked

UI Tests (SSR)

  • Red alert icon in confirmation modal
  • Shows count of invoices to void
  • Modal closes on success with notification

Import Page

Unit Tests

  • import->invoice maps parsed data to invoice entity
  • validate-invoice checks required keys and client access
  • match-vendor finds vendor by code or throws with search hint
  • admin-only-if-multiple-clients flags source-url for admin-only when multiple clients
  • upload-schema validates force-client, force-vendor, force-location, force-chatgpt

Integration Tests

  • Happy path CSV/PDF import:
    1. Upload file(s) via drag-and-drop or file input
    2. File parsed (CSV direct, PDF via OCR when allowed)
    3. Invoices created with pending import status
    4. Results table shows success/failure per file
  • Force overrides:
    • Force client overrides parsed client
    • Force vendor overrides parsed vendor
    • Force location overrides parsed location
    • Force ChatGPT restricts to GPT-only parsing
  • Approve/disapprove:
    • Individual approve → status changes to imported
    • Individual disapprove → invoice deleted
    • Bulk approve/disapprove with selection
  • Validation:
    • Missing client, vendor, date, total → error
    • No client access → error
    • Vendor not found → error with search hint

UI Tests (SSR)

  • Upload zone with drag-and-drop styling (blue idle, green hover)
  • File list shows selected files
  • Force client/vendor/location typeaheads
  • "Only use ChatGPT" checkbox
  • Results table: File, Result, Template, Sample columns
  • Green/red styling for success/error
  • Error list shows first 5 errors
  • Import table shows pending invoices with approve/disapprove buttons
  • Filters: vendor, date range, check number, invoice number, amount range

Glimpse (OCR Import)

Unit Tests

  • textract->textract-invoice extracts fields from AWS Textract response
  • stack-rank sorts field descriptors by confidence
  • clean-customer normalizes customer names for search
  • deduplicate removes duplicate parsed values
  • textract-invoice->invoice converts to invoice with vendor terms and autopay

Integration Tests

  • Happy path:
    1. Upload PDF to Glimpse page
    2. File uploaded to S3, Textract job started
    3. Poll every 5s while IN_PROGRESS
    4. On SUCCESS, display extracted fields with alternatives
    5. User selects client, vendor, date, total, invoice number from alternatives
    6. Save creates invoice and links to textract job
  • Field extraction:
    • Total: from AMOUNT_DUE or TOTAL fields
    • Customer: from CUSTOMER_NUMBER or RECEIVER_NAME (with Solr fallback)
    • Vendor: from VENDOR_NAME with Solr search
    • Date: from INVOICE_RECEIPT_DATE, ORDER_DATE, DELIVERY_DATE
    • Invoice number: from INVOICE_RECEIPT_ID or PO_NUMBER

UI Tests (SSR)

  • Upload form with Dropzone integration (PDF only)
  • Progress indicator while analyzing
  • Side-by-side layout: PDF preview iframe + form
  • Form fields: Client, Location, Vendor, Date, Total, Invoice Number
  • Alternatives shown as clickable pills below each field
  • Disabled fields for client and vendor (must select from alternatives)
  • Success notification with link to view new invoice

Cross-Cutting Behaviors

Filtering

All list pages support these filters (applied via HTMX with debounce):

  • Vendor: Typeahead search, filters to specific vendor
  • Account: Typeahead search, filters invoices with that expense account
  • Date range: Start/end date for invoice date (uses scan-invoices for performance)
  • Due date range: Start/end due date
  • Amount range: Min/max total amount (inclusive)
  • Invoice number: Partial string match via .contains
  • Check number: Text search
  • Status: Route-param based (all, unpaid, paid, voided) or query-param
  • Import status: Filter by pending/imported
  • Exact match: Navigate to specific invoice by ID (bypasses other filters)
  • Scheduled payments: Filter to invoices with scheduled payment date
  • Unresolved: Invoices missing expense accounts or with unassigned accounts
  • Location: Filter by expense account location

Sorting

Click column headers to sort. Supported sort keys:

  • client (by client name)
  • vendor (by vendor name)
  • description-original
  • location (by expense account location)
  • date
  • due (nulls last with default 2050-01-01)
  • invoice-number
  • total
  • outstanding-balance

Default sort: descending by scan-invoices default. Toggle asc/desc.

Pagination

  • Default 25 per page
  • Configurable per-page
  • Offset-based pagination
  • Import table supports up to 250 for "all selected" operations

Selection

  • Individual row checkboxes
  • "Select all" checkbox selects all visible
  • "Select all filtered" selects all matching current filters (up to 250)
  • Selection state maintained via Alpine.js
  • Pay/Void/Edit/Bulk-Edit buttons use selection

Permissions

All invoice operations check permissions via can?:

Activity Required Permission
Create invoice {:subject :invoice :activity :create}
Edit invoice {:subject :invoice :activity :edit}
Delete/Void invoice {:subject :invoice :activity :delete}
Pay invoice {:subject :invoice :activity :pay}
Bulk delete {:subject :invoice :activity :bulk-delete}
Bulk edit {:subject :invoice :activity :bulk-edit}
Import {:subject :invoice :activity :import}

Additional checks:

  • assert-can-see-client - user must have access to invoice's client
  • assert-not-locked - invoice date must be >= client's locked-until date
  • Admin-only for bulk delete confirmation

Edge Cases

Voiding Paid Invoices

  • Single void: Blocked if invoice has linked non-voided payments. User must void payments first.
  • Bulk void: Automatically voids linked cash payments before voiding invoices.

Unvoiding

  • Restores invoice to original status, total, outstanding balance, and expense accounts
  • Uses Datomic history to reconstruct previous state
  • Requires edit permission and client access

Undo Autopay

  • Only available for paid invoices with scheduled payment and no linked payments
  • Resets status to unpaid, outstanding to total, clears scheduled payment
  • Locked dates blocked

Paying with Credit

  • Triggered when selected invoices for single vendor have net 0 balance (credit invoices + payment invoices)
  • Credit invoices (negative balance) offset payment invoices
  • Creates balance-credit payment

Location Spreading

  • "Shared" location in expense accounts spreads across all client locations
  • Base amount = total / location count (integer division in cents)
  • Remainder distributed 1 cent at a time to first N locations
  • Total delta applied to first account to handle rounding

Negative Amounts

  • Supported throughout (credit memos, overpayments)
  • Spread calculations handle negative totals
  • Pay button validates all-credits-or-debits (can't mix positive and negative vendor totals)

Zero Balances

  • Invoices with 0 outstanding balance show as paid
  • Zero-balance selections excluded from payment

Duplicate Invoice Numbers

  • assert-no-conflicting prevents same invoice number for same vendor/client
  • Edit wizard preserves existing invoice number unless changed

Missing Vendor/Account

  • New invoice wizard requires vendor with default expense account
  • Import validation fails if vendor not found
  • Unresolved filter finds invoices missing expense accounts

Locked Clients

  • Client has locked-until date
  • Invoices with date < locked-until cannot be: edited, paid, voided, imported, approved
  • Bulk operations filter out locked invoices
  • Pay wizard shows warning when some selections are locked

Large Invoice Counts

  • scan-invoices Datomic query used for efficient date/client filtering
  • Pagination limits to 25 per page by default
  • "All selected" capped at 250 for bulk operations
  • Outstanding/total sums calculated from all-ids, not just page

Test Data Requirements

Entities Needed

  • Clients: Multiple clients with different locations, some with locked-until dates
  • Vendors: With/without default accounts, terms, autopay settings, account overrides, terms overrides
  • Accounts: Expense accounts with/without invoice allowance, different locations
  • Bank Accounts: Check, cash, credit types, visible/hidden
  • Invoices: Various statuses, dates, amounts, with/without payments, with/without scheduled payments

Specific Test Scenarios

  • Client with multiple locations (for spreading)
  • Client with single location (client column hidden)
  • Vendor with terms (auto-due date) and autopay (auto-scheduled payment)
  • Invoice with partial payment (outstanding < total)
  • Invoice with linked payment (void blocked)
  • Voided invoice with history (unvoid test)
  • Paid invoice with scheduled payment, no linked payment (undo autopay)
  • Negative balance invoice (credit)
  • Imported pending invoice (approve/disapprove)
  • Locked client with pre/post-lock invoices

File Upload Test Data

  • Valid CSV with proper headers
  • PDF with embedded text
  • PDF requiring OCR
  • File with multiple clients (admin-only source-url)
  • File with missing required fields
  • File with unmatchable vendor

Dependencies

  • Datomic: Primary data store, history for unvoid
  • AWS S3: File storage for imports and Glimpse PDFs
  • AWS Textract: OCR for Glimpse PDF processing
  • Solr: Search for vendor/client matching in Glimpse
  • HTMX/Alpine.js: Frontend interactivity
  • Bidi: Route generation
  • Malli: Schema validation for forms and query params
  • clj-time: Date parsing and manipulation

Existing Tests to Preserve

Unit Tests

  • test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj
    • maybe-spread-locations-test - 6 test cases for location spreading logic

Integration Tests

  • test/clj/auto_ap/integration/routes/invoice_test.clj
    • import-uploaded-invoices - import single, duplicate prevention, location override, coded invoice
  • test/clj/auto_ap/integration/graphql/invoices.clj
    • test-add-invoice - add with valid/invalid accounts, vendor special accounts
    • edit-invoice - edit fields, vendor change blocked, conflicting invoice numbers
    • edit-expense-accounts - modify expense accounts
    • bulk-change-invoices - bulk coding with percentages
    • void-invoices - bulk void and unvoid
    • void-invoice - single void and unvoid