Invoice Behaviors
Overview
Invoices are the core entity of Integreat. This document catalogs every observable behavior with its recommended test strategy and implementation status.
Testing Philosophy
- Prefer unit tests for pure business logic (calculations, validations, transformations)
- Use integration tests for database interactions and cross-system flows
- Use UI tests only for end-to-end happy paths that touch multiple pages
- Every behavior must be user-visible; no tests for implementation details
Testing Patterns
Pattern: Grid Page Behaviors
Most list pages in Integreat follow the same pattern:
- Fetch IDs via Datomic query with filters
- Hydrate results via
pull-many
- Render table with sortable columns
- Support selection (individual / all / all-filtered)
- Action buttons appear conditionally based on permissions and selection state
Test implications: Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
Pattern: Wizard Behaviors
Wizards are multi-step forms with HTMX-driven navigation:
- Each step is a GET that renders a form fragment
- Form submissions are POST/PUT with validation
- Navigation between steps updates the wizard state
- Final submit creates/updates the entity
Test implications: Unit test validation logic and state transitions. Integration test the full wizard flow once. UI test only the happy path.
Pattern: Permission Gates
Every mutating operation checks:
assert-can-see-client — user has access to the client
assert-not-locked — invoice date >= client locked-until
can? — user has the specific permission for the activity
Test implications: Integration test each gate independently. UI tests only verify the happy path with a permitted user.
Invoice List Page
Display Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 1.1 |
It should display a table with columns: Client, Vendor, Invoice #, Date, Due, Status, Account, Outstanding, Links |
UI |
[ ] |
| 1.2 |
It should show the Client column only when multiple clients OR multiple locations are selected |
Integration |
[ ] |
| 1.3 |
It should show "Paid" status as a primary-colored pill |
UI |
[ ] |
| 1.4 |
It should show "Voided" status as a red pill |
UI |
[ ] |
| 1.5 |
It should show "Scheduled" status as a yellow pill when a scheduled payment exists |
UI |
[ ] |
| 1.6 |
It should show "Unpaid" status as a secondary-colored pill |
UI |
[ ] |
| 1.7 |
It should display due dates relative to today: "today", "in X days", or "X days ago" with appropriate color coding |
Unit + UI |
[x] |
| 1.8 |
It should show a partial payment indicator "of $X.XX" when outstanding balance differs from total |
UI |
[ ] |
| 1.9 |
It should display a links dropdown showing payments, transactions, ledger entries, and source files for each invoice |
UI |
[ ] |
| 1.10 |
It should group table rows by vendor name when sorted by vendor |
Integration |
[ ] |
Filtering Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 2.1 |
It should filter invoices by vendor typeahead selection |
Integration |
[ ] |
| 2.2 |
It should filter invoices by expense account typeahead selection |
Integration |
[ ] |
| 2.3 |
It should filter invoices by date range (invoice date) |
Integration |
[ ] |
| 2.4 |
It should filter invoices by due date range |
Integration |
[ ] |
| 2.5 |
It should filter invoices by amount range (min/max total) |
Integration |
[ ] |
| 2.6 |
It should filter invoices by invoice number partial match |
Integration |
[ ] |
| 2.7 |
It should filter invoices by check number |
Integration |
[ ] |
| 2.8 |
It should filter invoices by status via route (all/unpaid/paid/voided) |
Integration |
[ ] |
| 2.9 |
It should filter invoices by import status (pending/imported) |
Integration |
[ ] |
| 2.10 |
It should support exact-match navigation to a specific invoice by ID, bypassing other filters |
Integration |
[ ] |
| 2.11 |
It should filter to invoices with scheduled payments |
Integration |
[ ] |
| 2.12 |
It should filter to unresolved invoices (missing or unassigned expense accounts) |
Integration |
[ ] |
| 2.13 |
It should filter by expense account location |
Integration |
[ ] |
| 2.14 |
Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set |
Integration |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 3.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 3.2 |
It should sort by vendor name ascending/descending |
Integration |
[ ] |
| 3.3 |
It should sort by description original ascending/descending |
Integration |
[ ] |
| 3.4 |
It should sort by expense account location ascending/descending |
Integration |
[ ] |
| 3.5 |
It should sort by invoice date ascending/descending |
Integration |
[ ] |
| 3.6 |
It should sort by due date ascending/descending, with nulls last |
Integration |
[ ] |
| 3.7 |
It should sort by invoice number ascending/descending |
Integration |
[ ] |
| 3.8 |
It should sort by total amount ascending/descending |
Integration |
[ ] |
| 3.9 |
It should sort by outstanding balance ascending/descending |
Integration |
[ ] |
| 3.10 |
Given the user clicks a column header twice, then the sort direction should toggle |
Integration |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 4.1 |
It should display 25 invoices per page by default |
Integration |
[ ] |
| 4.2 |
It should allow changing the per-page count |
Integration |
[ ] |
| 4.3 |
It should calculate the total outstanding balance and total amount across ALL matching invoices, not just the current page |
Unit |
[ ] |
Selection Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 5.1 |
It should allow selecting individual invoices via checkboxes |
UI |
[ ] |
| 5.2 |
It should allow selecting all visible invoices via a header checkbox |
UI |
[ ] |
| 5.3 |
It should allow selecting all filtered invoices (up to 250) for bulk operations |
Integration |
[ ] |
| 5.4 |
Given invoices are selected, when the user applies a filter, then the selection should be cleared |
Integration |
[ ] |
Row Action Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 6.1 |
It should show a void button for unpaid invoices when the user has delete permission |
UI |
[ ] |
| 6.2 |
It should show an edit button for unpaid and paid invoices when the user has edit permission |
UI |
[ ] |
| 6.3 |
It should show an unvoid button for voided invoices when the user has edit permission |
UI |
[ ] |
| 6.4 |
It should show an undo-autopay button for paid invoices with scheduled payments and no linked payments, when the user has edit permission |
UI |
[ ] |
| 6.5 |
Given a paid invoice with linked non-voided payments, when the user attempts to void it, then it should be blocked with a message to void payments first |
Integration |
[ ] |
Pay Button Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 7.1 |
It should display the pay button disabled when no invoices are selected |
UI |
[ ] |
| 7.2 |
It should display the pay button disabled when invoices from multiple clients are selected |
UI |
[ ] |
| 7.3 |
It should display the pay button disabled when selected invoices have mixed positive and negative vendor totals |
UI |
[ ] |
| 7.4 |
It should display "Pay N invoices ($X.XX)" when valid invoices are selected |
UI |
[ ] |
| 7.5 |
It should display "Pay invoices using credit" when selected invoices for a single vendor have a net zero balance |
UI |
[ ] |
| 7.6 |
It should show a tooltip explaining why the pay button is disabled |
UI |
[ ] |
New Invoice Wizard
Basic Details Step
| # |
Behavior |
Test Strategy |
Status |
| 8.1 |
It should require client, vendor, date, invoice number, and total |
Integration |
[ ] |
| 8.2 |
It should auto-calculate the due date from vendor terms when client, date, and vendor are selected |
Unit |
[x] |
| 8.3 |
It should auto-calculate the scheduled payment date from vendor autopay settings |
Unit |
[x] |
| 8.4 |
It should suggest the vendor's default expense account |
Unit |
[x] |
| 8.5 |
It should prevent duplicate invoice numbers for the same vendor and client |
Unit + Integration |
[ ] |
| 8.6 |
It should allow editing all fields when creating a new invoice |
UI |
[ ] |
Expense Accounts Step
| # |
Behavior |
Test Strategy |
Status |
| 9.1 |
It should allow adding multiple expense account rows |
UI |
[ ] |
| 9.2 |
It should allow selecting an account, location, and amount per row |
UI |
[ ] |
| 9.3 |
It should auto-populate the location from the account's configured location, or default to "Shared" |
Unit |
[x] |
| 9.4 |
It should validate that expense account amounts sum to the invoice total |
Unit + Integration |
[x] |
| 9.5 |
Given a "Shared" location, when the invoice is saved, then the amount should be spread equally across all client locations |
Unit |
[ ] |
| 9.6 |
Given a "Shared" location with an odd total, when spread across N locations, then the remainder should be distributed 1 cent at a time to the first locations |
Unit |
[x] |
| 9.7 |
Given a negative total, when spread across locations, then negative amounts should be distributed correctly |
Unit |
[x] |
| 9.8 |
It should allow removing individual account rows |
UI |
[ ] |
| 9.9 |
It should update the total and balance dynamically when amounts change |
UI |
[ ] |
Next Steps
| # |
Behavior |
Test Strategy |
Status |
| 10.1 |
Given a new invoice is saved successfully, then the wizard should show "Next Steps" with Pay now, Add another, and Close options |
UI |
[ ] |
| 10.2 |
Given the user clicks "Pay now", then the pay wizard should open for the newly created invoice |
UI |
[ ] |
Edit Invoice
| # |
Behavior |
Test Strategy |
Status |
| 11.1 |
It should allow editing unpaid and paid invoices |
Integration |
[ ] |
| 11.2 |
It should disable the vendor field when editing |
UI |
[ ] |
| 11.3 |
It should allow modifying expense account amounts, adding/removing accounts |
Integration |
[ ] |
| 11.4 |
It should validate that modified amounts still sum to the total |
Unit + Integration |
[x] |
| 11.5 |
Given the user saves changes, then the invoice row should update in place without a full page reload |
UI |
[ ] |
| 11.6 |
It should block editing invoices with dates before the client's locked-until date |
Integration |
[ ] |
Pay Wizard
Payment Method Selection
| # |
Behavior |
Test Strategy |
Status |
| 12.1 |
It should show bank account cards with type-appropriate icons (cash=green, check=blue, credit=purple) |
UI |
[ ] |
| 12.2 |
It should allow selecting "Print check" for check-type bank accounts |
UI |
[ ] |
| 12.3 |
It should allow selecting "With cash" for cash-type bank accounts |
UI |
[ ] |
| 12.4 |
It should allow selecting "Debit" for any bank account |
UI |
[ ] |
| 12.5 |
It should allow selecting "Handwrite check" when a single vendor is selected with positive balance |
UI |
[ ] |
Payment Details
| # |
Behavior |
Test Strategy |
Status |
| 13.1 |
It should display a grid of selected invoices with vendor, number, total, and pay amount |
UI |
[ ] |
| 13.2 |
It should default to "Pay in full" mode, paying the outstanding balance of each invoice |
Integration |
[ ] |
| 13.3 |
It should allow switching to "Customize payments" mode to set individual pay amounts |
UI |
[ ] |
| 13.4 |
It should validate that custom payment amounts do not exceed the outstanding balance |
Unit + Integration |
[x] |
| 13.5 |
It should require a check number for handwritten checks |
Integration |
[ ] |
| 13.6 |
It should block payment if the invoice date is before the client's locked-until date |
Integration |
[ ] |
| 13.7 |
Given the user submits a check payment, when successful, then a PDF download link should be provided |
Integration |
[ ] |
Credit Payment
| # |
Behavior |
Test Strategy |
Status |
| 14.1 |
Given selected invoices for a single vendor with a net zero balance, when the user clicks pay, then a credit payment should be created offsetting credit invoices against payment invoices |
Integration |
[ ] |
| 14.2 |
It should block credit payment when multiple vendors are selected |
Integration |
[ ] |
| 14.3 |
It should block credit payment when the net balance is positive |
Integration |
[ ] |
Bulk Edit
| # |
Behavior |
Test Strategy |
Status |
| 15.1 |
It should allow selecting multiple invoices and opening the bulk edit wizard |
UI |
[ ] |
| 15.2 |
It should allow adding expense account rows with account, location, and percentage |
UI |
[ ] |
| 15.3 |
It should validate that percentages sum to 100% |
Unit + Integration |
[x] |
| 15.4 |
Given valid percentages, when submitted, then all selected invoices should be coded with the new expense accounts |
Integration |
[ ] |
| 15.5 |
It should exclude invoices with dates before the client's locked-until date |
Integration |
[ ] |
| 15.6 |
It should spread "Shared" locations across all client locations, rounding cents correctly |
Unit |
[x] |
Bulk Void
| # |
Behavior |
Test Strategy |
Status |
| 16.1 |
It should show a confirmation modal with the count of invoices to void |
UI |
[ ] |
| 16.2 |
It should require admin permission for bulk void operations |
Integration |
[ ] |
| 16.3 |
Given confirmed, when voiding, then linked cash payments should be voided automatically |
Integration |
[ ] |
| 16.4 |
Given confirmed, when voiding, then each invoice's total, outstanding balance, and expense account amounts should be set to 0 |
Integration |
[ ] |
| 16.5 |
It should exclude invoices with linked non-cash payments |
Integration |
[ ] |
| 16.6 |
It should exclude invoices with dates before the client's locked-until date |
Integration |
[ ] |
| 16.7 |
Given successful voiding, then the table should refresh with a success notification |
UI |
[ ] |
Single Void
| # |
Behavior |
Test Strategy |
Status |
| 17.1 |
Given an unpaid invoice with no linked payments, when the user voids it, then the invoice status should change to voided with zero amounts |
Integration |
[ ] |
| 17.2 |
Given a paid invoice with linked payments, when the user attempts to void it, then it should be blocked with an error message |
Integration |
[ ] |
| 17.3 |
It should block voiding invoices with dates before the client's locked-until date |
Integration |
[ ] |
| 17.4 |
Given successful voiding, then the row should update in place with a "live-removed" animation |
UI |
[ ] |
Unvoid
| # |
Behavior |
Test Strategy |
Status |
| 18.1 |
Given a voided invoice, when the user unvoids it, then it should restore the original status, total, outstanding balance, and expense accounts from Datomic history |
Integration |
[ ] |
| 18.2 |
It should require edit permission and client access |
Integration |
[ ] |
| 18.3 |
It should block unvoiding invoices with dates before the client's locked-until date |
Integration |
[ ] |
| 18.4 |
Given successful unvoiding, then the row should update in place with a flash animation |
UI |
[ ] |
Undo Autopay
| # |
Behavior |
Test Strategy |
Status |
| 19.1 |
Given a paid invoice with a scheduled payment and no linked payments, when the user undoes autopay, then the status should reset to unpaid and outstanding should equal total |
Integration |
[ ] |
| 19.2 |
It should block undoing autopay for invoices without scheduled payments |
Unit + Integration |
[x] |
| 19.3 |
It should block undoing autopay for invoices with linked payments |
Unit + Integration |
[x] |
| 19.4 |
It should block undoing autopay for invoices that are not paid |
Unit + Integration |
[x] |
| 19.5 |
It should block undoing autopay for invoices with dates before the client's locked-until date |
Integration |
[ ] |
Import Page
Upload Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 20.1 |
It should allow uploading CSV and PDF files via drag-and-drop |
UI |
[ ] |
| 20.2 |
It should parse CSV files directly |
Integration |
[ ] |
| 20.3 |
It should send PDF files to AWS Textract for OCR parsing when enabled |
Integration |
[ ] |
| 20.4 |
It should create invoices with pending import status |
Integration |
[ ] |
| 20.5 |
It should display results with success/failure per file |
UI |
[ ] |
| 20.6 |
It should allow force-overriding client, vendor, location, and ChatGPT parsing mode |
UI |
[ ] |
Validation Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 21.1 |
It should reject uploads missing required fields (client, vendor, date, total) |
Integration |
[ ] |
| 21.2 |
It should reject uploads where the user has no access to the client |
Integration |
[ ] |
| 21.3 |
It should reject uploads with unmatchable vendors, showing a search hint |
Integration |
[ ] |
Approve/Disapprove Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 22.1 |
Given a pending imported invoice, when approved, then its status should change to imported |
Integration |
[ ] |
| 22.2 |
Given a pending imported invoice, when disapproved, then it should be deleted |
Integration |
[ ] |
| 22.3 |
It should support bulk approve/disapprove with selection |
Integration |
[ ] |
Glimpse (OCR Import)
Upload Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 23.1 |
It should allow uploading PDF files |
UI |
[ ] |
| 23.2 |
It should upload the file to S3 and start an AWS Textract job |
Integration |
[ ] |
| 23.3 |
It should poll every 5 seconds while the Textract job is in progress |
Integration |
[ ] |
| 23.4 |
Given a successful Textract job, then it should display extracted fields with confidence scores |
UI |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 24.1 |
It should extract total from AMOUNT_DUE or TOTAL fields |
Unit |
[x] |
| 24.2 |
It should extract customer from CUSTOMER_NUMBER or RECEIVER_NAME, falling back to Solr search |
Unit + Integration |
[ ] |
| 24.3 |
It should extract vendor from VENDOR_NAME, falling back to Solr search |
Unit + Integration |
[ ] |
| 24.4 |
It should extract date from INVOICE_RECEIPT_DATE, ORDER_DATE, or DELIVERY_DATE |
Unit |
[x] |
| 24.5 |
It should extract invoice number from INVOICE_RECEIPT_ID or PO_NUMBER |
Unit |
[x] |
Form Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 25.1 |
It should show a side-by-side layout with PDF preview and form |
UI |
[ ] |
| 25.2 |
It should display alternative values as clickable pills for each field |
UI |
[ ] |
| 25.3 |
It should require selecting client and vendor from alternatives (fields disabled until selected) |
UI |
[ ] |
| 25.4 |
Given the user saves, then it should create an invoice linked to the textract job |
Integration |
[ ] |
Cross-Cutting Behaviors
Permission Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 26.1 |
It should block invoice creation for users without :create permission |
Integration |
[ ] |
| 26.2 |
It should block invoice editing for users without :edit permission |
Integration |
[ ] |
| 26.3 |
It should block invoice voiding for users without :delete permission |
Integration |
[ ] |
| 26.4 |
It should block invoice payment for users without :pay permission |
Integration |
[ ] |
| 26.5 |
It should block bulk delete for non-admin users |
Integration |
[ ] |
| 26.6 |
It should block bulk edit for users without :bulk-edit permission |
Integration |
[ ] |
| 26.7 |
It should block import for users without :import permission |
Integration |
[ ] |
| 26.8 |
It should verify the user has access to the invoice's client before any mutation |
Integration |
[ ] |
Lock Date Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 27.1 |
It should block editing invoices dated before the client's locked-until date |
Integration |
[ ] |
| 27.2 |
It should block paying invoices dated before the client's locked-until date |
Integration |
[ ] |
| 27.3 |
It should block voiding invoices dated before the client's locked-until date |
Integration |
[ ] |
| 27.4 |
It should block importing invoices dated before the client's locked-until date |
Integration |
[ ] |
| 27.5 |
It should block approving imported invoices dated before the client's locked-until date |
Integration |
[ ] |
| 27.6 |
It should filter out locked invoices from bulk operations |
Integration |
[ ] |
| 27.7 |
It should show a warning when some selected invoices are locked |
UI |
[ ] |
Legacy Route Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 28.1 |
It should redirect old SPA routes (/invoices, /invoices/unpaid, etc.) to the new SSR routes |
Integration |
[ ] |
Test Data Requirements
| Entity |
Requirements |
| Clients |
Multiple clients with different locations; some with locked-until dates |
| Vendors |
With/without default accounts; with/without terms and autopay settings |
| Accounts |
Expense accounts with/without invoice allowance; different locations |
| Bank Accounts |
Check, cash, and credit types |
| Invoices |
Various statuses (unpaid, paid, voided, scheduled), dates, amounts |
| Payments |
Linked to invoices; cash and check types |
| Files |
Valid CSV, PDF with text, PDF requiring OCR |
Existing Tests to Preserve
test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj — Location spreading logic
test/clj/auto_ap/integration/routes/invoice_test.clj — Import routes
test/clj/auto_ap/integration/graphql/invoices.clj — GraphQL invoice operations
Dependencies
- Datomic (primary store, history for unvoid)
- AWS S3 (file storage)
- AWS Textract (OCR)
- Solr (search for Glimpse matching)
- HTMX/Alpine.js (frontend interactivity)