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.
22 KiB
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-idsgenerates correct Datomic queries for all filter combinationsfetch-pagereturns[results count outstanding-sum total-sum]tuplesum-outstandingandsum-total-amountcalculate aggregates correctlyselected->idsresolves "all selected" vs individual selectionhydrate-resultsgroups and orders pulled entities by input IDspayable-idsfilters to only unpaid, non-scheduled invoicescan-undo-autopaymentvalidates 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-schemavalidates required fields, money amounts, entity IDs- Vendor must have default expense account (
check-vendor-default-account) clientize-vendorapplies client-specific terms, account overrides, autopay settingsassert-no-conflictingprevents duplicate invoice numbers per vendor/clientassert-invoice-amounts-add-upvalidates expense accounts sum to totalmaybe-spread-locationsspreads "Shared" location across all client locations- Existing tests in
new_invoice_wizard_test.cljcover:- 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)
- Existing tests in
$->centsandcents->$round correctlycalculate-spreaddistributes base amount + remainder
Integration Tests
- Happy path create:
- Open wizard, select client, vendor, date, invoice number, total
- Due date auto-calculates from vendor terms when client+date+vendor selected
- Scheduled payment auto-calculates from vendor autopay setting
- Save with default account → invoice created with vendor's default account
- Wizard navigates to "Next Steps" with Pay now / Add another / Close options
- Happy path customize accounts:
- Select "Customize accounts" on basic details
- Add multiple expense account rows
- Select account, location (auto-populated from account or Shared), amount
- Total and balance update dynamically via HTMX
- Location dropdown updates when account changes
- Save validates totals match
- Edit existing invoice:
- Open edit wizard for unpaid/paid invoice
- Vendor field disabled (cannot change)
- Modify amounts, add/remove accounts
- 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-schemavalidates payment structuredoes-amount-exceed-outstanding?catches over/under paymentscan-handwrite?validates single vendor and positive balance for handwritten checkscredit-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:
- Select unpaid invoices from same client
- Click Pay → Choose Payment Method modal opens
- Select bank account → click Debit
- Confirm "Pay in full" or switch to "Customize payments"
- Submit → payments created, checks printed if applicable
- Happy path - print check:
- Select check-type bank account
- Choose "Print check"
- Payment processed, PDF URL returned for printing
- Happy path - cash:
- Select cash-type bank account
- Payment processed immediately
- Happy path - handwrite check:
- Single vendor, positive balance required
- Enter check number and date
- Payment created with pending status
- Happy path - credit payment:
- Select invoices where vendor total is 0 (credit + debit balance)
- Pay button shows "Pay invoices using credit"
- Credit invoices offset payment invoices
- 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-upvalidates percentages sum to 100%maybe-code-accountsdistributes 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:
- Select invoices via checkboxes
- Click "Bulk Edit" → wizard opens
- Add expense account rows with account, location, percentage
- 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:
- Select invoices
- Click "Void selected" → confirmation modal
- 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->invoicemaps parsed data to invoice entityvalidate-invoicechecks required keys and client accessmatch-vendorfinds vendor by code or throws with search hintadmin-only-if-multiple-clientsflags source-url for admin-only when multiple clientsupload-schemavalidates force-client, force-vendor, force-location, force-chatgpt
Integration Tests
- Happy path CSV/PDF import:
- Upload file(s) via drag-and-drop or file input
- File parsed (CSV direct, PDF via OCR when allowed)
- Invoices created with pending import status
- 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-invoiceextracts fields from AWS Textract responsestack-ranksorts field descriptors by confidenceclean-customernormalizes customer names for searchdeduplicateremoves duplicate parsed valuestextract-invoice->invoiceconverts to invoice with vendor terms and autopay
Integration Tests
- Happy path:
- Upload PDF to Glimpse page
- File uploaded to S3, Textract job started
- Poll every 5s while IN_PROGRESS
- On SUCCESS, display extracted fields with alternatives
- User selects client, vendor, date, total, invoice number from alternatives
- 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-invoicesfor 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 clientassert-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-conflictingprevents 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-untildate - 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-invoicesDatomic 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.cljmaybe-spread-locations-test- 6 test cases for location spreading logic
Integration Tests
test/clj/auto_ap/integration/routes/invoice_test.cljimport-uploaded-invoices- import single, duplicate prevention, location override, coded invoice
test/clj/auto_ap/integration/graphql/invoices.cljtest-add-invoice- add with valid/invalid accounts, vendor special accountsedit-invoice- edit fields, vendor change blocked, conflicting invoice numbersedit-expense-accounts- modify expense accountsbulk-change-invoices- bulk coding with percentagesvoid-invoices- bulk void and unvoidvoid-invoice- single void and unvoid