diff --git a/docs/testing/behaviors/invoice.md b/docs/testing/behaviors/invoice.md index 4258edd1..8c7003e4 100644 --- a/docs/testing/behaviors/invoice.md +++ b/docs/testing/behaviors/invoice.md @@ -2,495 +2,402 @@ ## 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. +Invoices are the core entity of Integreat. This document catalogs every observable behavior with its recommended test strategy and implementation status. -**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) +**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 -## 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 | +## Testing Patterns -## Behaviors by Page +### 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 -### Invoice List Page +**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end. -#### 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 +### 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 -#### 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 +**Test implications:** Unit test validation logic and state transitions. Integration test the full wizard flow once. UI test only the happy path. -#### 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 +### 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 -### New Invoice Wizard +**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user. -#### 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 +## Invoice List Page -#### 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 +### Display Behaviors -### Pay Wizard +| # | 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 | [ ] | -#### 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 +### Filtering Behaviors -#### 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 +| # | 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 | [ ] | -#### 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 +### Sorting Behaviors -### Bulk Edit +| # | 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 | [ ] | -#### 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 +### Pagination Behaviors -#### 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 +| # | 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 | [ ] | -#### 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 +### Selection Behaviors -### Bulk Delete/Void +| # | 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 | [ ] | -#### 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 +### Row Action Behaviors -#### 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 +| # | 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 | [ ] | -#### UI Tests (SSR) -- Red alert icon in confirmation modal -- Shows count of invoices to void -- Modal closes on success with notification +### Pay Button Behaviors -### Import Page +| # | 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 | [ ] | -#### 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 +## New Invoice Wizard -#### 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 +### Basic Details Step -### Glimpse (OCR Import) +| # | 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 | [ ] | -#### 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 +### Expense Accounts Step -#### 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 +| # | 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 | [ ] | -#### 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 +### 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 -### Filtering -All list pages support these filters (applied via HTMX with debounce): +### Permission Behaviors -- **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 +| # | 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 | [ ] | -### 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 +### Lock Date Behaviors -Default sort: descending by scan-invoices default. Toggle asc/desc. +| # | 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 | [ ] | -### Pagination -- Default 25 per page -- Configurable per-page -- Offset-based pagination -- Import table supports up to 250 for "all selected" operations +### Legacy Route Behaviors -### 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 +| # | Behavior | Test Strategy | Status | +|---|----------|---------------|--------| +| 28.1 | It should redirect old SPA routes (`/invoices`, `/invoices/unpaid`, etc.) to the new SSR routes | Integration | [ ] | -### 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 +| 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 -### Unit Tests -- `test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj` - - `maybe-spread-locations-test` - 6 test cases for location spreading logic +- `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 -### 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 +## Dependencies + +- Datomic (primary store, history for unvoid) +- AWS S3 (file storage) +- AWS Textract (OCR) +- Solr (search for Glimpse matching) +- HTMX/Alpine.js (frontend interactivity)