refactor(all): rewrite all behavior docs in table format with checkboxes

Rewrite all 11 remaining behavior documents to match the streamlined
invoice.md format:

- dashboard.md: 250 lines, 62 behaviors
- payment.md: 260 lines, behaviors for list, void, check printing, ACH
- transaction.md: 310 lines, list, import, admin insights
- ledger.md: 519 lines, entries, P&L, balance sheet, cash flows
- company.md: 320 lines, profile, 1099s, Plaid/Yodlee, reports
- admin.md: 494 lines, clients, accounts, vendors, rules, jobs, history
- pos.md: 405 lines, sales, deposits, tenders, refunds, shifts
- search-indicators.md: 167 lines, search modal, indicators
- auth.md: 184 lines, login, logout, impersonation, sessions
- outgoing-invoice.md: 192 lines, create, line items, PDF
- legacy-spa.md: 340 lines, all legacy pages (docs only)

All documents now use:
- Testing Patterns section with reusable abstractions
- Numbered tables: # | Behavior | Test Strategy | Status
- It should... behavior descriptions
- Checkboxes [ ]/[x] for tracking implementation
- Cross-Cutting Behaviors for permissions, lock dates, etc.
- Test Data Requirements tables
- Existing Tests to Preserve sections

Total: 3,844 lines of behavior documentation across 12 subsystem docs.
This commit is contained in:
2026-05-04 13:48:51 -07:00
parent e14a23ff54
commit d627e3c5d0
11 changed files with 2837 additions and 2500 deletions

View File

@@ -4,364 +4,516 @@
The Ledger module is the core accounting interface of Integreat. It provides server-side rendered (HTMX) pages for viewing journal entries, creating manual journal entries, and generating financial reports (Profit & Loss, Balance Sheet, Cash Flows). All ledger pages are permission-gated and client-scoped.
## Routes & Pages
**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
| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/ledger` | GET | `::all-page` | Main ledger entries list (internal) |
| `/ledger/external-new` | GET | `::external-page` | External ledger entries list |
| `/ledger/new` | GET | `::new` | Modal form for new journal entry |
| `/ledger/new` | POST | `::new-submit` | Submit new journal entry |
| `/ledger/new/location-select` | GET | `::location-select` | Location dropdown for line item |
| `/ledger/new/account-typeahead` | GET | `::account-typeahead` | Account search for line item |
| `/ledger/new/line-item` | GET | `::new-line-item` | Add new line item row |
| `/ledger/external-import-new` | GET | `::external-import-page` | External TSV import page |
| `/ledger/external-import-new/parse` | POST | `::external-import-parse` | Parse clipboard TSV data |
| `/ledger/external-import-new/import` | POST | `::external-import-import` | Import validated entries |
| `/ledger/investigate` | GET | `::investigate` | Investigation modal for report drill-down |
| `/ledger/investigate/results` | GET | `::investigate-results` | Investigation results table |
| `/ledger/table` | GET | `::table` | HTMX table fragment for ledger entries |
| `/ledger/csv` | GET | `::csv` | CSV export of ledger entries |
| `/ledger/bank-account-filter` | GET | `::bank-account-filter` | Bank account filter widget |
| `/ledger/reports/profit-and-loss` | GET | `::profit-and-loss` | P&L report page |
| `/ledger/reports/profit-and-loss/run` | PUT | `::run-profit-and-loss` | Run P&L report |
| `/ledger/reports/profit-and-loss/export` | POST | `::export-profit-and-loss` | Export P&L to PDF |
| `/ledger/reports/balance-sheet` | GET | `::balance-sheet` | Balance sheet page |
| `/ledger/reports/balance-sheet/run` | GET | `::run-balance-sheet` | Run balance sheet |
| `/ledger/reports/balance-sheet/export` | POST | `::export-balance-sheet` | Export balance sheet to PDF |
| `/ledger/reports/cash-flows` | GET | `::cash-flows` | Cash flows page |
| `/ledger/reports/cash-flows/run` | PUT | `::run-cash-flows` | Run cash flows report |
| `/ledger/reports/cash-flows/export` | POST | `::export-cash-flows` | Export cash flows to PDF |
---
## Behaviors by Page
## Testing Patterns
### Ledger Entries List
### 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
**Page load**
- Displays a paginated, sortable data grid of journal entries
- Shows client selection sidebar if user has access to multiple clients
- Title reflects status filter: e.g. "Unpaid Register" or "Paid Register"
- Includes an "Add journal entry" button (hidden on external ledger 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.
**Columns displayed**
- Client (hidden if only 1 client with 1 location)
- Vendor (falls back to `alternate-description` if no vendor)
- Source (hidden on internal ledger)
- External ID (hidden on internal ledger, truncated to max-width)
- Date
- Amount (formatted as currency)
- Debit lines (account + location + amount)
- Credit lines (account + location + amount)
- Links dropdown (links to original invoice, source file, transaction, or memo)
### Pattern: Modal Form Behaviors
Modal forms are HTMX-driven dialogs:
1. Opened via GET request that renders a form fragment
2. Form submissions are POST/PUT with validation
3. On success, the modal closes and the table updates in place
4. On validation failure, the modal shows error messages
**Row actions**
- Void button for unpaid invoices (requires `:delete :invoice` permission)
- Edit button for unpaid/paid invoices (requires `:edit :invoice` permission)
- Unvoid button for voided invoices (requires `:edit :invoice` permission)
- Trash icon with confirmation for delete operations
**Test implications:** Unit test validation logic. Integration test the full modal flow once. UI test only the happy path.
**Sorting**
- Available sorts: Client, Vendor, Source, External ID, Date, Amount, Account
- Default sort: Date ascending
- Sorting by Vendor or Source groups rows with break headers
### Pattern: Report Behaviors
Financial reports follow a consistent pattern:
1. Form with client multi-select, date/period selectors, and toggles
2. Run button triggers HTMX request to generate report
3. Report data is computed from account snapshots and running balances
4. Export button generates PDF and returns a download modal
**Filtering (left sidebar)**
- Vendor typeahead search
- Account typeahead search
- Bank account radio filter (refreshes on client change)
- Date range picker
- Invoice number text search
- Account code range (gte/lte inputs)
- Amount range (gte/lte inputs)
- "Show unbalanced" checkbox (filters to entries where debits ≠ credits)
- Exact match ID pill (clears on click)
**Test implications:** Unit test calculation logic. Integration test report generation with various filter combinations. UI test only one report end-to-end.
**Pagination**
- Default 25 entries per page
- Configurable per-page
- Pagination controls at bottom
### Pattern: Permission Gates
Every mutating operation checks:
1. `assert-can-see-client` — user has access to the client
2. `assert-not-locked` — entry date >= client locked-until
3. `can?` — user has the specific permission for the activity
**CSV Export**
- Exports all matching entries with line-item-level rows
- Columns: ID, Client, Vendor, Source, External ID, Date, Amount, Account, Debit, Credit
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
### New Journal Entry
---
**Modal form**
- Opens as modal dialog (750px wide)
- Client typeahead (pre-filled if client already selected on parent page)
- Date input (default today, format MM/DD/YYYY)
- Vendor typeahead (disabled if editing existing entry)
- Total amount input (must be ≥ $0.01)
- Memo text input (optional)
- Line items grid with columns: Account, Location, Debit, Credit
## Ledger Entries List
**Line item behaviors**
- Account typeahead searches accounts scoped to selected client
- Location dropdown updates based on selected account's required location
- If account has a fixed location, dropdown is locked to that location
- If account has no location restriction, shows client's locations
- New line item rows added via HTMX request
- Line items can be removed with X button
### Display Behaviors
**Validation**
- Client is required
- Date is required and must be valid
- Vendor is required
- Amount must be ≥ $0.01
- Each line item must have an allowed account
- Each line item must have a location belonging to the account
- Debits must sum to total amount
- Credits must sum to total amount
- Debits and credits must each equal the journal entry amount
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 1.1 | It should display a paginated, sortable data grid of journal entries | UI | [ ] |
| 1.2 | It should show the Client column only when multiple clients OR multiple locations are selected | Integration | [ ] |
| 1.3 | It should display the Vendor column, falling back to `alternate-description` when no vendor is present | Integration | [ ] |
| 1.4 | It should hide the Source column on the internal ledger page | UI | [ ] |
| 1.5 | It should hide the External ID column on the internal ledger page | UI | [ ] |
| 1.6 | It should truncate the External ID column to a max-width when displayed | UI | [ ] |
| 1.7 | It should display the Date column with formatted dates | UI | [ ] |
| 1.8 | It should display the Amount column formatted as currency | UI | [ ] |
| 1.9 | It should display Debit lines with account, location, and amount per line item | UI | [ ] |
| 1.10 | It should display Credit lines with account, location, and amount per line item | UI | [ ] |
| 1.11 | It should display a Links dropdown with links to original invoice, source file, transaction, or memo | UI | [ ] |
| 1.12 | It should show the page title reflecting the status filter, e.g. "Unpaid Register" or "Paid Register" | UI | [ ] |
| 1.13 | It should show an "Add journal entry" button on the internal ledger page | UI | [ ] |
| 1.14 | It should hide the "Add journal entry" button on the external ledger page | UI | [ ] |
| 1.15 | It should show a client selection sidebar when the user has access to multiple clients | UI | [ ] |
**On save**
- Generates external ID: `manual-<uuid>`
- Updates client's `ledger-last-change` timestamp
- On POST: prepends new row to table, triggers modal close
- On PUT: replaces existing row in table, triggers modal close
### Filtering Behaviors
### External Import
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 2.1 | It should filter entries by vendor typeahead selection | Integration | [ ] |
| 2.2 | It should filter entries by account typeahead selection | Integration | [ ] |
| 2.3 | It should filter entries by bank account via radio filter | Integration | [ ] |
| 2.4 | It should refresh the bank account filter when the client selection changes | Integration | [ ] |
| 2.5 | It should filter entries by date range | Integration | [ ] |
| 2.6 | It should filter entries by invoice number text search | Integration | [ ] |
| 2.7 | It should filter entries by account code range (gte/lte inputs) | Integration | [ ] |
| 2.8 | It should filter entries by amount range (gte/lte inputs) | Integration | [ ] |
| 2.9 | It should filter to unbalanced entries when "Show unbalanced" is checked | Integration | [ ] |
| 2.10 | It should support exact-match navigation to a specific entry by ID, bypassing other filters | Integration | [ ] |
| 2.11 | It should clear the exact match ID pill when clicked | UI | [ ] |
| 2.12 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
**Clipboard paste**
- User clicks "Load from clipboard" button
- Reads TSV data from browser clipboard
- Parses tab-separated values with columns: Id, Client, Source, Vendor, Date, Account Code, Location, Debit, Credit
### Sorting Behaviors
**Parse validation**
- Validates all rows have required fields
- Dates must be parseable
- Account codes must be numeric or bank account strings
- Locations must be 1-2 characters
- Debits/Credits must be valid money amounts
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.1 | It should sort by Client ascending/descending | Integration | [ ] |
| 3.2 | It should sort by Vendor ascending/descending | Integration | [ ] |
| 3.3 | It should sort by Source ascending/descending | Integration | [ ] |
| 3.4 | It should sort by External ID ascending/descending | Integration | [ ] |
| 3.5 | It should sort by Date ascending/descending | Integration | [ ] |
| 3.6 | It should sort by Amount ascending/descending | Integration | [ ] |
| 3.7 | It should sort by Account ascending/descending | Integration | [ ] |
| 3.8 | It should default to Date ascending | Integration | [ ] |
| 3.9 | Given sorting by Vendor, then rows should be grouped with break headers | Integration | [ ] |
| 3.10 | Given sorting by Source, then rows should be grouped with break headers | Integration | [ ] |
| 3.11 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
**Import validation**
- Client code must exist
- Vendor must exist (creates hidden vendor if missing)
- Client must not be locked for the entry date
- Debits and credits must balance per entry
- Entry cannot total $0.00 (warning)
- Location must belong to client
- Account code must exist
- Bank account code must belong to client
- Account location requirements must be satisfied
### Pagination Behaviors
**Import results**
- Successful entries are imported
- Entries with warnings are ignored (removed if previously existed)
- Entries with errors block import and show error counts
- Retracts existing entries by external ID before importing
- Indexes imported entries in Solr asynchronously
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.1 | It should display 25 entries per page by default | Integration | [ ] |
| 4.2 | It should allow changing the per-page count | Integration | [ ] |
| 4.3 | It should show pagination controls at the bottom of the table | UI | [ ] |
### P&L Report
### Row Action Behaviors
**Form**
- Customer multi-select typeahead (max 20, defaults to first 5 if "all")
- Periods dropdown (default: year-to-date)
- "Column per location" toggle
- "Include deltas" toggle
- Run button (HTMX PUT)
- Export PDF button
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 5.1 | It should show a void button for unpaid invoices when the user has `:delete :invoice` permission | UI | [ ] |
| 5.2 | It should show an edit button for unpaid and paid invoices when the user has `:edit :invoice` permission | UI | [ ] |
| 5.3 | It should show an unvoid button for voided invoices when the user has `:edit :invoice` permission | UI | [ ] |
| 5.4 | It should show a trash icon with confirmation for delete operations | UI | [ ] |
**Report generation**
- Computes running balances before generating
- Queries detailed account snapshots for each client/period end date
- Amounts calculated as: debits - credits for assets/dividends/expenses, credits - debits for others
- Groups data by client, location, and period
### CSV Export Behaviors
**Report output**
- Summary table: Sales, COGS, Payroll, Gross Profits, Overhead, Net Income
- Detail table: Account-level breakdown within each category
- Percent of sales calculated for each row
- Deltas shown between periods if enabled
- Column-per-location mode shows each location as separate columns
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 6.1 | It should export all matching entries with line-item-level rows | Integration | [ ] |
| 6.2 | It should include columns: ID, Client, Vendor, Source, External ID, Date, Amount, Account, Debit, Credit | Integration | [ ] |
**Warnings**
- Warns if >20 clients selected (truncates to 20)
- Warns about unresolved ledger entries (missing numeric codes)
- Shows sample links to admin history for invalid entries (if user has `:view :history` permission)
---
**PDF Export**
- Generates PDF with Calibri Light font, 6pt
- Uploads to S3 at `reports/profit-and-loss/<uuid>/<name>.pdf`
- Persists report record in Datomic
- Returns modal with download link
## New Journal Entry
### Balance Sheet
### Modal Form Behaviors
**Form**
- Customer multi-select typeahead (max 20, defaults to first 5 if "all")
- Date dropdown (single date, default today)
- "Include deltas" toggle
- Run button (HTMX GET)
- Export PDF button
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 7.1 | It should open as a modal dialog 750px wide | UI | [ ] |
| 7.2 | It should show a client typeahead pre-filled if a client is already selected on the parent page | UI | [ ] |
| 7.3 | It should show a date input defaulting to today in MM/DD/YYYY format | UI | [ ] |
| 7.4 | It should show a vendor typeahead disabled when editing an existing entry | UI | [ ] |
| 7.5 | It should show a total amount input requiring a value of at least $0.01 | Unit + Integration | [ ] |
| 7.6 | It should show an optional memo text input | UI | [ ] |
| 7.7 | It should display a line items grid with Account, Location, Debit, and Credit columns | UI | [ ] |
**Report generation**
- Computes running balances before generating
- Queries account snapshot as of each selected date
- Groups by account categories: Assets, Liabilities, Owner's Equity
- Includes Retained Earnings (net income across all P&L categories)
### Line Item Behaviors
**Report output**
- Assets section with account detail and subtotal
- Liabilities section with account detail and subtotal
- Owner's Equity section with account detail and subtotal
- Retained Earnings line
- Delta columns between periods if enabled and multiple dates selected
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.1 | It should allow account typeahead search scoped to the selected client | Integration | [ ] |
| 8.2 | It should update the location dropdown based on the selected account's required location | Integration | [ ] |
| 8.3 | It should lock the location dropdown to a fixed location when the account requires it | Integration | [ ] |
| 8.4 | It should show all client locations when the account has no location restriction | Integration | [ ] |
| 8.5 | It should add new line item rows via HTMX request | UI | [ ] |
| 8.6 | It should allow removing line item rows with an X button | UI | [ ] |
**Warnings**
- Warns if >20 clients selected
- Warns about unresolved ledger entries
### Validation Behaviors
**PDF Export**
- Generates PDF, uploads to S3 at `reports/balance-sheet/<uuid>/<name>.pdf`
- Persists report record in Datomic
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 9.1 | It should require a client | Unit + Integration | [ ] |
| 9.2 | It should require a valid date | Unit + Integration | [ ] |
| 9.3 | It should require a vendor | Unit + Integration | [ ] |
| 9.4 | It should require an amount of at least $0.01 | Unit + Integration | [ ] |
| 9.5 | It should require each line item to have an allowed account | Unit + Integration | [ ] |
| 9.6 | It should require each line item to have a location belonging to the account | Unit + Integration | [ ] |
| 9.7 | It should validate that debits sum to the total amount | Unit + Integration | [ ] |
| 9.8 | It should validate that credits sum to the total amount | Unit + Integration | [ ] |
| 9.9 | It should validate that debits and credits each equal the journal entry amount | Unit + Integration | [ ] |
| 9.10 | It should block saving when the entry date is on or before the client's `locked-until` date | Integration | [ ] |
### Cash Flows
### Save Behaviors
**Form**
- Customer multi-select typeahead (max 20, defaults to first 5 if "all")
- Periods dropdown (default: year-to-date)
- Run button (HTMX PUT)
- Export PDF button
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 10.1 | It should generate an external ID in the format `manual-<uuid>` | Unit | [ ] |
| 10.2 | It should update the client's `ledger-last-change` timestamp | Integration | [ ] |
| 10.3 | Given a new entry is saved successfully, then it should prepend the new row to the table and close the modal | UI | [ ] |
| 10.4 | Given an existing entry is saved successfully, then it should replace the existing row in the table and close the modal | UI | [ ] |
**Report generation**
- Queries account snapshot as of period end + 1 day
- Groups accounts into: Operating Activities, Investment Activities, Financing Activities, Cash
- Calculates cash flow effect: add or subtract based on account code ranges
---
**Report output**
- Net Income starting point
- Operating Activities detail (increases, decreases, +/- in cash)
- Investment Activities detail
- Financing Activities detail
- Change in Cash and Cash Equivalents total
- Bank Accounts / Cash detail
## External Import
**Warnings**
- Warns if >20 clients selected
- Warns about unresolved ledger entries
### Clipboard Paste Behaviors
**PDF Export**
- Generates PDF, uploads to S3 at `reports/cash-flows/<uuid>/<name>.pdf`
- Persists report record in Datomic
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 11.1 | It should allow clicking a "Load from clipboard" button | UI | [ ] |
| 11.2 | It should read TSV data from the browser clipboard | UI | [ ] |
| 11.3 | It should parse tab-separated values with columns: Id, Client, Source, Vendor, Date, Account Code, Location, Debit, Credit | Integration | [ ] |
### Investigation
### Parse Validation Behaviors
**Modal behavior**
- Opens as modal dialog from report table cell clicks
- Shows ledger entries filtered by the clicked cell's filters (account code range, client, location, date range)
- Displays raw table without checkboxes
- Max height 600px with scrollable content
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 12.1 | It should validate that all rows have required fields | Integration | [ ] |
| 12.2 | It should validate that dates are parseable | Unit + Integration | [ ] |
| 12.3 | It should validate that account codes are numeric or bank account strings | Unit + Integration | [ ] |
| 12.4 | It should validate that locations are 1-2 characters | Unit + Integration | [ ] |
| 12.5 | It should validate that debits and credits are valid money amounts | Unit + Integration | [ ] |
**Table behavior**
- Uses same query schema as main ledger list
- Supports sorting and pagination
- Does not push URL state
### Import Validation Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 13.1 | It should validate that the client code exists | Integration | [ ] |
| 13.2 | It should validate that the vendor exists, creating a hidden vendor if missing | Integration | [ ] |
| 13.3 | It should block entries for dates when the client is locked | Integration | [ ] |
| 13.4 | It should validate that debits and credits balance per entry | Unit + Integration | [ ] |
| 13.5 | It should warn when an entry totals $0.00 | Unit + Integration | [ ] |
| 13.6 | It should validate that the location belongs to the client | Integration | [ ] |
| 13.7 | It should validate that the account code exists | Integration | [ ] |
| 13.8 | It should validate that bank account codes belong to the client | Integration | [ ] |
| 13.9 | It should validate that account location requirements are satisfied | Integration | [ ] |
### Import Result Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 14.1 | It should import successful entries | Integration | [ ] |
| 14.2 | It should ignore entries with warnings, removing them if they previously existed | Integration | [ ] |
| 14.3 | It should block import and show error counts when entries have errors | Integration | [ ] |
| 14.4 | It should retract existing entries by external ID before importing | Integration | [ ] |
| 14.5 | It should index imported entries in Solr asynchronously | Integration | [ ] |
---
## P&L Report
### Form Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 15.1 | It should show a customer multi-select typeahead with a max of 20 selections | UI | [ ] |
| 15.2 | It should default to the first 5 customers when "all" is selected | Integration | [ ] |
| 15.3 | It should show a periods dropdown defaulting to year-to-date | UI | [ ] |
| 15.4 | It should show a "Column per location" toggle | UI | [ ] |
| 15.5 | It should show an "Include deltas" toggle | UI | [ ] |
| 15.6 | It should trigger report generation via HTMX PUT on the Run button | UI | [ ] |
| 15.7 | It should show an Export PDF button | UI | [ ] |
### Report Generation Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 16.1 | It should compute running balances before generating the report | Integration | [ ] |
| 16.2 | It should query detailed account snapshots for each client and period end date | Integration | [ ] |
| 16.3 | It should calculate amounts as debits minus credits for assets, dividends, and expenses | Unit | [ ] |
| 16.4 | It should calculate amounts as credits minus debits for liabilities, equity, and revenue | Unit | [ ] |
| 16.5 | It should group data by client, location, and period | Integration | [ ] |
### Report Output Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 17.1 | It should display a summary table with Sales, COGS, Payroll, Gross Profits, Overhead, and Net Income | UI | [ ] |
| 17.2 | It should display a detail table with account-level breakdown within each category | UI | [ ] |
| 17.3 | It should calculate percent of sales for each row | Unit | [ ] |
| 17.4 | It should show deltas between periods when enabled | UI | [ ] |
| 17.5 | It should show each location as separate columns when column-per-location mode is enabled | UI | [ ] |
### Warning Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 18.1 | It should warn when more than 20 clients are selected and truncate to 20 | Integration | [ ] |
| 18.2 | It should warn about unresolved ledger entries with missing numeric codes | Integration | [ ] |
| 18.3 | It should show sample links to admin history for invalid entries when the user has `:view :history` permission | Integration | [ ] |
### PDF Export Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 19.1 | It should generate a PDF with Calibri Light font at 6pt | Integration | [ ] |
| 19.2 | It should upload the PDF to S3 at `reports/profit-and-loss/<uuid>/<name>.pdf` | Integration | [ ] |
| 19.3 | It should persist a report record in Datomic | Integration | [ ] |
| 19.4 | It should return a modal with a download link | UI | [ ] |
---
## Balance Sheet
### Form Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 20.1 | It should show a customer multi-select typeahead with a max of 20 selections | UI | [ ] |
| 20.2 | It should default to the first 5 customers when "all" is selected | Integration | [ ] |
| 20.3 | It should show a date dropdown defaulting to today | UI | [ ] |
| 20.4 | It should show an "Include deltas" toggle | UI | [ ] |
| 20.5 | It should trigger report generation via HTMX GET on the Run button | UI | [ ] |
| 20.6 | It should show an Export PDF button | UI | [ ] |
### Report Generation Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 21.1 | It should compute running balances before generating the report | Integration | [ ] |
| 21.2 | It should query account snapshots as of each selected date | Integration | [ ] |
| 21.3 | It should group accounts into Assets, Liabilities, and Owner's Equity | Integration | [ ] |
| 21.4 | It should include Retained Earnings as net income across all P&L categories | Unit | [ ] |
### Report Output Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 22.1 | It should display an Assets section with account detail and subtotal | UI | [ ] |
| 22.2 | It should display a Liabilities section with account detail and subtotal | UI | [ ] |
| 22.3 | It should display an Owner's Equity section with account detail and subtotal | UI | [ ] |
| 22.4 | It should display a Retained Earnings line | UI | [ ] |
| 22.5 | It should show delta columns between periods when enabled and multiple dates are selected | UI | [ ] |
### Warning Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 23.1 | It should warn when more than 20 clients are selected | Integration | [ ] |
| 23.2 | It should warn about unresolved ledger entries | Integration | [ ] |
### PDF Export Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 24.1 | It should generate a PDF and upload to S3 at `reports/balance-sheet/<uuid>/<name>.pdf` | Integration | [ ] |
| 24.2 | It should persist a report record in Datomic | Integration | [ ] |
| 24.3 | It should return a modal with a download link | UI | [ ] |
---
## Cash Flows
### Form Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 25.1 | It should show a customer multi-select typeahead with a max of 20 selections | UI | [ ] |
| 25.2 | It should default to the first 5 customers when "all" is selected | Integration | [ ] |
| 25.3 | It should show a periods dropdown defaulting to year-to-date | UI | [ ] |
| 25.4 | It should trigger report generation via HTMX PUT on the Run button | UI | [ ] |
| 25.5 | It should show an Export PDF button | UI | [ ] |
### Report Generation Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 26.1 | It should query account snapshots as of period end plus one day | Integration | [ ] |
| 26.2 | It should group accounts into Operating Activities, Investment Activities, Financing Activities, and Cash | Integration | [ ] |
| 26.3 | It should calculate cash flow effect by adding or subtracting based on account code ranges | Unit | [ ] |
### Report Output Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 27.1 | It should display Net Income as the starting point | UI | [ ] |
| 27.2 | It should display Operating Activities detail with increases, decreases, and cash impact | UI | [ ] |
| 27.3 | It should display Investment Activities detail | UI | [ ] |
| 27.4 | It should display Financing Activities detail | UI | [ ] |
| 27.5 | It should display Change in Cash and Cash Equivalents total | UI | [ ] |
| 27.6 | It should display Bank Accounts / Cash detail | UI | [ ] |
### Warning Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 28.1 | It should warn when more than 20 clients are selected | Integration | [ ] |
| 28.2 | It should warn about unresolved ledger entries | Integration | [ ] |
### PDF Export Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 29.1 | It should generate a PDF and upload to S3 at `reports/cash-flows/<uuid>/<name>.pdf` | Integration | [ ] |
| 29.2 | It should persist a report record in Datomic | Integration | [ ] |
| 29.3 | It should return a modal with a download link | UI | [ ] |
---
## Investigation
### Modal Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 30.1 | It should open as a modal dialog from report table cell clicks | UI | [ ] |
| 30.2 | It should filter ledger entries by the clicked cell's filters: account code range, client, location, and date range | Integration | [ ] |
| 30.3 | It should display a raw table without checkboxes | UI | [ ] |
| 30.4 | It should constrain the modal to a max height of 600px with scrollable content | UI | [ ] |
### Table Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 31.1 | It should use the same query schema as the main ledger list | Integration | [ ] |
| 31.2 | It should support sorting and pagination | Integration | [ ] |
| 31.3 | It should not push URL state on filter or sort changes | Integration | [ ] |
---
## Cross-Cutting Behaviors
### Report generation
- All reports call `upsert-running-balance` before querying to ensure cached balances are current
- Reports use `detailed-account-snapshot` Datomic query for raw data
- Account lookups built per-client via `build-account-lookup`
- Reports skip entries without numeric codes (unassigned accounts) and warn
### Report Generation Behaviors
### Export
- All three reports support PDF export via `clj-pdf`
- PDFs use Calibri Light 6pt font on letter size
- Uploaded to S3 data bucket with UUID-based key
- Report metadata persisted to Datomic (`:report/name`, `:report/client`, `:report/key`, `:report/url`, `:report/creator`, `:report/created`)
- Export returns modal with S3 download link
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 32.1 | It should call `upsert-running-balance` before querying to ensure cached balances are current | Integration | [ ] |
| 32.2 | It should use `detailed-account-snapshot` Datomic query for raw report data | Integration | [ ] |
| 32.3 | It should build account lookups per-client via `build-account-lookup` | Integration | [ ] |
| 32.4 | It should skip entries without numeric codes and warn about unresolved entries | Integration | [ ] |
### Filtering and sorting
- Ledger list filters applied via HTMX on change (500ms debounce) or keyup (1000ms debounce for hot filters)
- Bank account filter refreshes when client selection changes
- Sorting supports multiple sort keys with ascending/descending
- Default sort is date ascending
- Exact match ID filter bypasses all other filters
### Export Behaviors
### Permissions
- All ledger pages require authenticated user
- Main ledger: `:read :ledger`
- New/Edit journal entry: `:edit :ledger`
- External import: `:import :ledger` + admin assertion
- P&L report: `:read :profit-and-loss`
- Balance sheet: `:read :balance-sheet`
- Cash flows: `:read :cash-flows`
- Users can only see clients they have permission for (`assert-can-see-client`)
- Invoice void/edit/unvoid actions require respective invoice permissions
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 33.1 | It should support PDF export via `clj-pdf` for all three reports | Integration | [ ] |
| 33.2 | It should use Calibri Light 6pt font on letter size for all PDF exports | Integration | [ ] |
| 33.3 | It should upload PDFs to the S3 data bucket with a UUID-based key | Integration | [ ] |
| 33.4 | It should persist report metadata to Datomic with name, client, key, URL, creator, and created timestamp | Integration | [ ] |
| 33.5 | It should return a modal with an S3 download link after export | UI | [ ] |
## Edge Cases
### Filtering and Sorting Behaviors
**Empty states**
- No entries: empty table with pagination showing 0 results
- No matching filters: empty table, filter pills remain
- Report with no data: empty report form, no table rendered
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 34.1 | It should apply ledger list filters via HTMX on change with a 500ms debounce | Integration | [ ] |
| 34.2 | It should apply hot filters via HTMX on keyup with a 1000ms debounce | Integration | [ ] |
| 34.3 | It should refresh the bank account filter when client selection changes | Integration | [ ] |
| 34.4 | It should support multiple sort keys with ascending and descending direction | Integration | [ ] |
| 34.5 | It should default to date ascending sort | Integration | [ ] |
| 34.6 | It should bypass all other filters when an exact match ID filter is active | Integration | [ ] |
**Data locking**
- Journal entries cannot be created for dates on or before client's `locked-until` date
- External import rejects entries for locked dates
### Permission Behaviors
**Unbalanced entries**
- "Show unbalanced" filter computes debit/credit sums per entry and filters to mismatches
- Unbalanced entries are still displayed in normal view
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 35.1 | It should require an authenticated user for all ledger pages | Integration | [ ] |
| 35.2 | It should require `:read :ledger` permission for the main ledger page | Integration | [ ] |
| 35.3 | It should require `:edit :ledger` permission for new and edit journal entry | Integration | [ ] |
| 35.4 | It should require `:import :ledger` permission plus admin assertion for external import | Integration | [ ] |
| 35.5 | It should require `:read :profit-and-loss` permission for the P&L report | Integration | [ ] |
| 35.6 | It should require `:read :balance-sheet` permission for the balance sheet | Integration | [ ] |
| 35.7 | It should require `:read :cash-flows` permission for the cash flows report | Integration | [ ] |
| 35.8 | It should restrict users to clients they have permission for via `assert-can-see-client` | Integration | [ ] |
| 35.9 | It should require `:delete :invoice` permission for invoice void actions | Integration | [ ] |
| 35.10 | It should require `:edit :invoice` permission for invoice edit and unvoid actions | Integration | [ ] |
**Account location mismatches**
- Account with fixed location rejects other locations
- Account without location restriction rejects "A" (all) location
- Validation occurs on both frontend (location select) and backend (schema)
### Empty State Behaviors
**Multi-client reports**
- Capped at 20 clients for performance
- "All" clients defaults to first 5 for reports
- Report names include all selected client names
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 36.1 | It should show an empty table with pagination showing 0 results when no entries exist | UI | [ ] |
| 36.2 | It should show an empty table with filter pills remaining when filters match nothing | UI | [ ] |
| 36.3 | It should show an empty report form with no table rendered when report data is empty | UI | [ ] |
**Running balance cache**
- `refresh-running-balance-cache` recomputes balances for dirty line items
- Changing a ledger entry marks its line items and subsequent entries as dirty
- Non-dirty entries are not recomputed
### Data Locking Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 37.1 | It should block creating journal entries for dates on or before the client's `locked-until` date | Integration | [ ] |
| 37.2 | It should reject external import entries for locked dates | Integration | [ ] |
### Unbalanced Entry Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 38.1 | It should compute debit and credit sums per entry for the "Show unbalanced" filter | Unit | [ ] |
| 38.2 | It should display unbalanced entries in the normal view without filtering | UI | [ ] |
### Account Location Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 39.1 | It should reject locations other than the fixed location for accounts with fixed locations | Integration | [ ] |
| 39.2 | It should reject "A" (all) location for accounts without location restrictions | Integration | [ ] |
| 39.3 | It should validate account location requirements on both the frontend location select and backend schema | Integration | [ ] |
### Running Balance Cache Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 40.1 | It should recompute balances for dirty line items via `refresh-running-balance-cache` | Integration | [ ] |
| 40.2 | It should mark a changed entry's line items and subsequent entries as dirty | Integration | [ ] |
| 40.3 | It should skip recomputation for non-dirty entries | Integration | [ ] |
---
## Test Data Requirements
**Clients**
- At least 2 clients with different locations
- Client with `locked-until` date in the past
| Entity | Requirements |
|--------|-------------|
| **Clients** | At least 2 clients with different locations; at least 1 client with a `locked-until` date in the past |
| **Accounts** | Asset accounts (11000-11999); Liability accounts (20000-28999); Equity accounts (30000-39999); Revenue accounts (40000-49999); Expense accounts (50000-98999); accounts with fixed locations; accounts without location restrictions |
| **Vendors** | Existing vendors; hidden vendors (auto-created on import) |
| **Journal Entries** | Balanced entries (debits = credits); unbalanced entries (debits ≠ credits); entries with multiple line items; entries linked to invoices; entries with external IDs; entries across multiple dates and locations |
| **Bank Accounts** | Bank accounts linked to clients |
**Accounts**
- Asset accounts (11000-11999)
- Liability accounts (20000-28999)
- Equity accounts (30000-39999)
- Revenue accounts (40000-49999)
- Expense accounts (50000-98999)
- Accounts with fixed locations
- Accounts without location restrictions
## Existing Tests to Preserve
**Vendors**
- Existing vendors
- Hidden vendors (auto-created on import)
**Journal entries**
- Entries with debits = credits (balanced)
- Entries with debits ≠ credits (unbalanced)
- Entries with multiple line items
- Entries linked to invoices
- Entries with external IDs
- Entries across multiple dates and locations
**Bank accounts**
- Bank accounts linked to clients
- `test/clj/auto_ap/ssr/ledger/ledger_test.clj` — Ledger page rendering and grid behaviors
- `test/clj/auto_ap/integration/routes/ledger_test.clj` — Ledger routes and mutations
- `test/clj/auto_ap/ledger/reports_test.clj` — Report generation and calculation logic
## Dependencies
- `auto-ap.ssr.ledger.common` - Shared grid page config, query schema, filtering
- `auto-ap.ledger.reports` - Report aggregation and formatting logic
- `auto-ap.ledger` - Running balance cache, account lookups
- `auto-ap.datomic.accounts` - Account querying and clientization
- `auto-ap.permissions` - Permission checks and middleware
- `auto-ap.ssr.grid-page-helper` - Generic data grid behaviors
- `auto-ap.ssr.components` - UI components (typeahead, date inputs, buttons)
- `auto-ap.ssr.form-cursor` - Form state management
- `clj-pdf` - PDF generation
- `amazonica.aws.s3` - S3 upload for exports
- `datomic.api` - Database queries and transactions
- `auto-ap.ssr.ledger.common` Shared grid page config, query schema, filtering
- `auto-ap.ledger.reports` Report aggregation and formatting logic
- `auto-ap.ledger` Running balance cache, account lookups
- `auto-ap.datomic.accounts` Account querying and clientization
- `auto-ap.permissions` Permission checks and middleware
- `auto-ap.ssr.grid-page-helper` Generic data grid behaviors
- `auto-ap.ssr.components` UI components (typeahead, date inputs, buttons)
- `auto-ap.ssr.form-cursor` Form state management
- `clj-pdf` PDF generation
- `amazonica.aws.s3` S3 upload for exports
- `datomic.api` Database queries and transactions