# 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: 1. Fetch IDs via Datomic query with filters 2. Hydrate results via `pull-many` 3. Render table with sortable columns 4. Support selection (individual / all / all-filtered) 5. 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: 1. Each step is a GET that renders a form fragment 2. Form submissions are POST/PUT with validation 3. Navigation between steps updates the wizard state 4. 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: 1. `assert-can-see-client` — user has access to the client 2. `assert-not-locked` — invoice date >= client locked-until 3. `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 | [ ] | | 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 | [ ] | ### Pagination Behaviors | # | 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 | [ ] | | 8.3 | It should auto-calculate the scheduled payment date from vendor autopay settings | Unit | [ ] | | 8.4 | It should suggest the vendor's default expense account | Unit | [ ] | | 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 | [ ] | | 9.4 | It should validate that expense account amounts sum to the invoice total | Unit + Integration | [ ] | | 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 | [ ] | | 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 | [ ] | | 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 | [ ] | | 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 | [ ] | --- ## 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 | [ ] | | 19.3 | It should block undoing autopay for invoices with linked payments | Unit + Integration | [ ] | | 19.4 | It should block undoing autopay for invoices that are not paid | Unit + Integration | [ ] | | 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 | [ ] | ### Field Extraction Behaviors | # | Behavior | Test Strategy | Status | |---|----------|---------------|--------| | 24.1 | It should extract total from AMOUNT_DUE or TOTAL fields | Unit | [ ] | | 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 | [ ] | | 24.5 | It should extract invoice number from INVOICE_RECEIPT_ID or PO_NUMBER | Unit | [ ] | ### 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)