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,172 +4,247 @@
The Dashboard is an admin-only, SSR-based overview page that displays financial summary cards across selected clients. It provides at-a-glance visibility into bank account balances, outstanding tasks, recent sales trends, expense breakdowns, and profit/loss summaries. Cards are loaded lazily via HTMX for progressive rendering.
## Routes
**Testing Philosophy**
- Prefer unit tests for pure business logic (calculations, formatting, client trimming)
- Use integration tests for database interactions and cross-system flows (Datomic queries, GraphQL calls)
- Use UI tests only for end-to-end happy paths and visual states
- Every behavior must be user-visible; no tests for implementation details
| Route | Handler | Purpose |
|-------|---------|---------|
| `GET /` | `::page` | Main dashboard page with stub cards |
| `GET /expense-card` | `::expense-card` | Expense pie chart (top 5 accounts, last month) |
| `GET /pnl-card` | `::pnl-card` | Profit & Loss bar chart (last month) |
| `GET /sales-card` | `::sales-card` | Gross sales bar chart (last 14 days) |
| `GET /bank-accounts-card` | `::bank-accounts-card` | Bank account balances and sync status |
| `GET /tasks-card` | `::tasks-card` | Unpaid invoices and uncategorized transactions |
---
## Behaviors
## Testing Patterns
### Unit Test Behaviors
### Pattern: Progressive Card Loading
Dashboard cards follow a progressive loading pattern:
1. SSR renders stub cards with loading spinners
2. Each stub triggers an independent HTMX request on load
3. Card endpoints return HTML fragments that replace the stub content
4. Cards load independently without blocking each other
- `extract-client-ids` returns intersection of user's allowed clients and requested clients
- `is-admin?` returns true only when `user/role` equals `"admin"`
- Client trimming takes only first 20 clients from the valid set
- `clients-trimmed?` is true when the count of trimmed clients differs from valid clients
- Bank account card excludes accounts with `bank-account-type/cash`
- Bank account card displays ledger balance formatted as `$X,XXX.XX`
- Bank account card shows sync timestamp in `standard-time` format when present
- Sales chart queries use ISO date range from 14 days ago to tomorrow
- Expense pie chart queries use date range from 1 month ago to tomorrow
- P&L card aggregates accounts into `:sales` and `:cogs :payroll :controllable :fixed-overhead :ownership-controllable` categories
- Tasks card queries invoices with `invoice-status/unpaid` and transactions with `transaction-approval-status/requires-feedback`
- Tasks card only renders task sections when counts are non-zero
**Test implications:** Unit test the card query logic. Integration test each card endpoint returns valid HTML. UI test only needs to verify the page loads and cards appear.
### Integration Test Behaviors
### Pattern: Client Context Propagation
All dashboard operations depend on selected clients:
1. Client selection triggers a page-wide refresh via HTMX
2. `wrap-trim-clients` middleware limits to 20 clients before queries execute
3. All card endpoints receive the same trimmed client set
- Main page `GET /` returns 200 with base layout and 6 stub cards for admin users
- Each card endpoint returns 200 with HTML response containing chart canvas or data
- Bank accounts card performs Datomic pull for each client's bank accounts with nested Intuit/Yodlee/Plaid data
- Sales card executes Datomic query summing `sales-order/total` by ISO date
- Expense card executes Datomic query summing `invoice-expense-account/amount` by account name
- P&L card calls GraphQL `get-profit-and-loss-raw` with trimmed client IDs and last-month date range
- Tasks card executes two separate Datomic queries for unpaid invoices and uncategorized transactions
- Expense breakdown card (from `company/reports/expense`) executes query with optional vendor and account filters
- All card endpoints apply `wrap-trim-clients` and `wrap-admin` middleware
- Non-admin requests to any dashboard route receive 302 redirect to `/login`
- Unauthenticated requests receive 302 redirect to `/login` with `redirect-to` parameter
**Test implications:** Integration test that changing clients updates all cards. Unit test the trimming logic.
### UI Test Behaviors (SSR)
### Pattern: Admin Permission Gating
The dashboard is restricted to admin users:
1. `wrap-admin` middleware checks user role before any data access
2. Non-admin users are redirected before reaching handlers
3. Middleware is applied consistently to all card endpoints
#### Happy Path: Dashboard Loads Successfully
1. Admin user navigates to `/`
2. Page renders with main navigation, client selector, and breadcrumb "Dashboard"
3. Six stub cards appear with loading spinners:
- Expenses (pie chart)
- Tasks
- Bank Accounts (tall card, row-span-2)
- Gross Sales, last 14 days (bar chart)
- Profit and Loss, last month (horizontal bar chart)
- Expense breakdown (bar chart, wide card)
4. Each stub card triggers `hx-get` on load to fetch its content
5. Cards progressively render with actual data
6. User sees bank account balances, task counts, sales chart, expense charts, and P&L summary
**Test implications:** Integration test each endpoint independently for permission enforcement.
#### Card Interactions
- Bank Accounts card displays each client's name, account name, ledger balance, and last sync time
- When a bank account has Intuit/Yodlee/Plaid sync, the external balance and sync time display alongside the ledger balance
- Yodlee accounts additionally show "Pending Txs" with pending balance
- Tasks card shows "Pay now" link for unpaid invoices (links to unpaid invoices page with `date-range=year`)
- Tasks card shows "Review now" link for uncategorized transactions (links to requires-feedback transactions page)
- Expense breakdown card has Vendor and Account typeahead filters
- Changing Vendor or Account filter triggers HTMX request to reload the breakdown chart with filtered data
- Expense breakdown chart updates URL via `hx-push-url` with query params
---
#### Client Selection Effects
- User selects different clients from the client dropdown
- Page triggers `clientSelected` event on body
- HTMX swaps `#app-contents` with fresh dashboard content for newly selected clients
- All cards re-fetch with new client context
- If more than 20 clients selected, a yellow warning banner appears: "Warning: These reports are only for twenty of the selected customers. Please select a specific customer to see more detail."
## Dashboard Page
## Edge Cases
### Page Loading Behaviors
### No Clients Selected
- Dashboard page renders but all cards show empty state
- Bank Accounts card renders empty list
- Sales chart shows empty bar chart with no data points
- Expense pie chart shows empty pie chart
- P&L card shows zero income and expenses
- Tasks card shows no task notifications (sections hidden when count is zero)
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 1.1 | It should render the main dashboard page with navigation, client selector, and "Dashboard" breadcrumb for admin users | UI | [ ] |
| 1.2 | It should display six stub cards with loading spinners for progressive rendering | UI | [ ] |
| 1.3 | It should trigger independent HTMX requests to load each card's content on page load | Integration | [ ] |
| 1.4 | It should progressively replace stub cards with actual data as responses arrive | UI | [ ] |
### Many Clients (Trimming Warning)
- When user has access to more than 20 clients and selects all, only first 20 are used
- Yellow warning banner displays below breadcrumb
- All card queries execute against trimmed client set only
- Warning persists across client selection changes until fewer than 21 clients selected
---
### No Data for Date Ranges
- Sales chart (14-day window): Empty chart renders with no bars when no sales orders exist
- Expense pie chart (last month): Empty pie chart renders when no invoices with expense accounts exist
- P&L card: Shows `$0.00` for both income and expenses
- Tasks card: No task sections render (conditional on non-zero counts)
- Bank Accounts: Accounts still display with zero/null balances if they exist but have no transactions
## Bank Accounts Card
### Error Loading Individual Cards
- Each card loads independently via separate HTMX request
- Failure in one card endpoint does not prevent other cards from loading
- Stub card shows spinner until successful response or timeout
- Server errors in card endpoints return appropriate HTTP status without breaking page layout
### Display Behaviors
### Permissions (Admin vs User)
- Only users with `user/role = "admin"` can access dashboard routes
- Non-admin authenticated users receive 302 redirect to `/login`
- Unauthenticated users receive 302 redirect to `/login` with `redirect-to` parameter
- Admin role is verified by `wrap-admin` middleware before any data queries execute
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 2.1 | It should display each client's name, account name, ledger balance, and last sync time | UI | [ ] |
| 2.2 | It should exclude bank accounts with cash type from the display | Integration | [ ] |
| 2.3 | It should format ledger balances as currency ($X,XXX.XX) | Unit + UI | [ ] |
| 2.4 | It should display the last sync timestamp in standard time format when present | Unit + UI | [ ] |
| 2.5 | It should display Intuit balance and sync time for Intuit-linked accounts | UI | [ ] |
| 2.6 | It should display Yodlee available balance, sync time, and pending balance for Yodlee-linked accounts | UI | [ ] |
| 2.7 | It should display Plaid balance and sync time for Plaid-linked accounts | UI | [ ] |
| 2.8 | It should display $0.00 for missing or null balances | Unit + UI | [ ] |
### Bank Account Sync States
- Account with no external sync (Intuit/Yodlee/Plaid): Shows only ledger balance and sync time
- Account with Intuit sync: Shows Intuit balance and last synced timestamp
- Account with Yodlee sync: Shows available balance, last synced timestamp, and pending balance
- Account with Plaid sync: Shows Plaid balance and last synced timestamp
- Missing/null balances display as `$0.00`
---
## Sales Card
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.1 | It should display a bar chart of gross sales for the last 14 days | UI | [ ] |
| 3.2 | It should render an empty bar chart when no sales orders exist in the date range | UI | [ ] |
### Data Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.3 | It should query and sum sales order totals by date for the selected clients | Integration | [ ] |
---
## Expense Card
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.1 | It should display a pie chart of the top 5 expense accounts for the last month | UI | [ ] |
| 4.2 | It should render an empty pie chart when no invoices with expense accounts exist in the date range | UI | [ ] |
### Data Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 4.3 | It should sum expense amounts by account name for the selected clients | Integration | [ ] |
---
## Profit & Loss Card
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 5.1 | It should display income and expenses aggregated by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable) | UI | [ ] |
| 5.2 | It should show $0.00 for both income and expenses when no data exists for the period | UI | [ ] |
### Data Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 5.3 | It should query P&L data via GraphQL for the selected clients and last month | Integration | [ ] |
---
## Tasks Card
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 6.1 | It should display the count of unpaid invoices when the count is non-zero | UI | [ ] |
| 6.2 | It should display the count of uncategorized transactions requiring feedback when the count is non-zero | UI | [ ] |
| 6.3 | It should provide a "Pay now" link for unpaid invoices linking to the unpaid invoices page with year date range | UI | [ ] |
| 6.4 | It should provide a "Review now" link for uncategorized transactions linking to the requires-feedback page | UI | [ ] |
| 6.5 | It should hide task sections entirely when their respective counts are zero | Integration | [ ] |
### Data Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 6.6 | It should query Datomic for invoices with unpaid status for the selected clients | Integration | [ ] |
| 6.7 | It should query Datomic for transactions with requires-feedback approval status for the selected clients | Integration | [ ] |
---
## Expense Breakdown Card
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 7.1 | It should display a bar chart breaking down expenses by account | UI | [ ] |
| 7.2 | It should render an empty chart when no expense data exists | UI | [ ] |
| 7.3 | It should provide Vendor and Account typeahead filters | UI | [ ] |
### Data Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 7.4 | It should reload the chart with filtered data when filter selections change | Integration | [ ] |
| 7.5 | It should update the URL with filter query parameters via hx-push-url | Integration | [ ] |
| 7.6 | It should exclude voided invoices from the breakdown | Integration | [ ] |
---
## Filtering Behaviors
### Expense Breakdown Filters
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 8.1 | It should filter the expense breakdown chart by vendor selection | Integration | [ ] |
| 8.2 | It should filter the expense breakdown chart by expense account selection | Integration | [ ] |
| 8.3 | It should trigger an HTMX request to reload the chart when any filter changes | Integration | [ ] |
---
## Client Selection Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 9.1 | It should update the dashboard content when the user selects different clients from the dropdown | UI | [ ] |
| 9.2 | It should trigger a clientSelected event on the body when client selection changes | Integration | [ ] |
| 9.3 | It should swap the dashboard content area with fresh content for the newly selected clients | Integration | [ ] |
| 9.4 | It should re-fetch all card data with the new client context | Integration | [ ] |
| 9.5 | It should limit reports to the first 20 selected clients from the valid set | Unit + Integration | [ ] |
| 9.6 | It should display a yellow warning banner when more than 20 clients are selected | UI | [ ] |
| 9.7 | It should persist the warning banner across client selection changes until fewer than 21 clients are selected | UI | [ ] |
| 9.8 | It should trim the client set before executing any card data queries | Integration | [ ] |
---
## Error Handling Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 10.1 | It should load each card independently via separate HTMX requests | Integration | [ ] |
| 10.2 | It should not prevent other cards from loading when one card endpoint fails | Integration | [ ] |
| 10.3 | It should display a loading spinner on stub cards until data loads or a timeout occurs | UI | [ ] |
| 10.4 | It should return appropriate HTTP status codes for card endpoint errors without breaking the page layout | Integration | [ ] |
---
## Cross-Cutting Behaviors
### Permission Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 11.1 | It should allow only admin users to access the dashboard page and card endpoints | Integration | [ ] |
| 11.2 | It should redirect non-admin authenticated users to /login with a 302 status | Integration | [ ] |
| 11.3 | It should redirect unauthenticated users to /login with a redirect-to parameter | Integration | [ ] |
| 11.4 | It should verify admin role via middleware before executing any data queries | Integration | [ ] |
### Empty State Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 12.1 | It should render the dashboard page when no clients are selected, with all cards showing empty states | UI | [ ] |
| 12.2 | It should display an empty bank accounts list when no clients are selected | UI | [ ] |
| 12.3 | It should display an empty sales chart when no clients are selected | UI | [ ] |
| 12.4 | It should display an empty expense pie chart when no clients are selected | UI | [ ] |
| 12.5 | It should show $0.00 income and expenses in the P&L card when no clients are selected | UI | [ ] |
| 12.6 | It should hide all task sections when no clients are selected | UI | [ ] |
---
## Test Data Requirements
### Users
- Admin user (`:user/role "admin"`) with access to multiple clients
- Non-admin user (`:user/role "user"`) to test permission denial
| Entity | Requirements |
|--------|-------------|
| **Users** | Admin user with access to multiple clients; non-admin user for permission denial |
| **Clients** | Minimum 2, ideally 25+; mix with/without bank accounts |
| **Bank Accounts** | Various types (checking, savings, cash); some linked to Intuit, Yodlee, Plaid; with/without balances and sync timestamps |
| **Sales Orders** | Orders within and outside the 14-day window with totals |
| **Invoices** | With expense accounts and unpaid status; voided invoices to test exclusion |
| **Transactions** | With requires-feedback approval status |
| **Chart of Accounts** | Categories: sales, COGS, payroll, controllable, fixed overhead, ownership controllable |
### Clients (minimum 2, ideally 25+)
- Client with `client/name` and at least one bank account
- Mix of clients with Intuit, Yodlee, and Plaid synced accounts
- Client with `:bank-account-type/cash` account (should be excluded from display)
## Existing Tests to Preserve
### Bank Accounts
- Bank account with `:bank-account/current-balance` and `:bank-account/current-balance-synced`
- Bank account linked to Intuit (`:bank-account/intuit-bank-account`)
- Bank account linked to Yodlee (`:bank-account/yodlee-account` with available + pending balance)
- Bank account linked to Plaid (`:bank-account/plaid-account`)
### Sales Orders
- Sales orders dated within last 14 days with `:sales-order/total`
- Sales orders dated outside 14-day window (should not appear)
### Invoices
- Invoices with `:invoice/expense-accounts` (with `:invoice-expense-account/amount` and `:account/name`) within last month
- Invoices with `:invoice/status :invoice-status/unpaid` within last year
- Invoices with `:invoice/outstanding-balance`
- Voided invoices (should be excluded from expense breakdown)
### Transactions
- Transactions with `:transaction/approval-status :transaction-approval-status/requires-feedback` within last year
- Transactions with `:transaction/amount`
### P&L Data
- Chart of accounts with categories `:sales`, `:cogs`, `:payroll`, `:controllable`, `:fixed-overhead`, `:ownership-controllable`
- Account balances within last month for selected clients
- No existing dashboard-specific tests have been identified in the current test suite. Any tests covering dashboard routes or card handlers should be preserved during refactoring.
## Dependencies
### External Services
- **Datomic**: All card data queried from Datomic database
- **GraphQL endpoint**: P&L card calls `get-profit-and-loss-raw` (Lacinia GraphQL)
- **Intuit/Yodlee/Plaid**: Bank account sync data for external balances (data stored in Datomic)
### Frontend Libraries
- **HTMX**: Progressive card loading via `hx-get`/`hx-trigger`/`hx-swap`
- **Chart.js**: Canvas-based chart rendering (bar, pie, horizontal bar charts)
- **Alpine.js**: Chart data binding via `x-data`/`x-init` attributes
### Middleware Stack
- `wrap-admin`: Enforces admin-only access
- `wrap-client-redirect-unauthenticated`: Handles auth redirects
- `wrap-trim-clients`: Limits client scope to 20
- `wrap-hydrate-clients`: Populates client data in request
- Datomic (primary data store for all card queries)
- GraphQL/Lacinia (P&L data via get-profit-and-loss-raw)
- HTMX (progressive card loading via hx-get/hx-trigger/hx-swap)
- Chart.js (canvas-based charts)
- Alpine.js (chart data binding)