Files
integreat/docs/testing/behaviors/pos.md
Bryce 6b5d33a32f feat(tests): implement integration and unit tests for auth, company, and ledger behaviors
- Auth: 30 tests (97 assertions) covering OAuth, sessions, JWT, impersonation, roles
- Company: 35 tests (92 assertions) covering profile, 1099, expense reports, permissions
- Ledger: 113 tests (148 assertions) covering grid, journal entries, import, reports
- Fix existing test failures in running_balance, insights, tx, plaid, graphql
- Fix InMemSolrClient to handle Solr query syntax properly
- Update behavior docs: auth (42 done), company (32 done), ledger (120 done)
- All 478 tests pass with 0 failures, 0 errors
2026-05-08 16:12:08 -07:00

422 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# POS Behaviors
## Overview
The POS (Point of Sale) module provides SSR (HTMX-driven) grid pages for viewing sales data imported from payment processors. All pages share a common grid layout with filters, sortable columns, and pagination. Data is read-only except for the admin Sales Summaries edit wizard.
**Testing Philosophy**
- Prefer unit tests for pure business logic (calculations, validations, transformations)
- Use integration tests for database interactions and cross-system flows
- Use UI tests only for end-to-end happy paths that touch multiple pages
- Every behavior must be user-visible; no tests for implementation details
---
## Testing Patterns
### Pattern: Grid Page Behaviors
Most list pages in Integreat follow the same pattern:
1. Fetch IDs via Datomic query with filters
2. Hydrate results via `pull-many`
3. Render table with sortable columns
4. Support selection (individual / all / all-filtered)
5. Action buttons appear conditionally based on permissions and selection state
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
### Pattern: Wizard Behaviors
Wizards are multi-step forms with HTMX-driven navigation:
1. Each step is a GET that renders a form fragment
2. Form submissions are POST/PUT with validation
3. Navigation between steps updates the wizard state
4. Final submit creates/updates the entity
**Test implications:** Unit test validation logic and state transitions. Integration test the full wizard flow once. UI test only the happy path.
### Pattern: Permission Gates
Every mutating operation checks:
1. `assert-can-see-client` — user has access to the client
2. `assert-not-locked` — invoice date >= client locked-until
3. `can?` — user has the specific permission for the activity
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
---
## Sales Orders
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 1.1 | It should display a table with columns: Client, Date, Source, Total, Tax, Tip, Payment Methods | UI | [ ] |
| 1.2 | It should show the Client column only when multiple clients are in scope | Integration | [x] |
| 1.3 | It should render the Source column as a pill badge | UI | [ ] |
| 1.4 | It should render each unique payment method as a pill in the Payment Methods column (cash, card, gift card, other) | UI | [ ] |
| 1.5 | It should display action buttons above the grid showing Total $ and Tax $ pills summarizing the currently filtered result set | UI | [ ] |
| 1.6 | It should show an external link icon row button when the sales order has a reference link | UI | [ ] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 2.1 | It should filter sales orders by date range (start date / end date) | Integration | [x] |
| 2.2 | It should filter sales orders by total amount range (min / max) | Integration | [x] |
| 2.3 | It should filter sales orders by payment method via radio cards: All, Cash, Card, Gift Card, Other | Integration | [x] |
| 2.4 | It should filter sales orders by processor via radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor | Integration | [x] |
| 2.5 | It should filter sales orders by category text input matching order line item category | Integration | [x] |
| 2.6 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [x] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 3.1 | It should sort by client name ascending/descending | Integration | [x] |
| 3.2 | It should sort by date ascending/descending | Integration | [x] |
| 3.3 | It should sort by total amount ascending/descending | Integration | [x] |
| 3.4 | It should sort by tax amount ascending/descending | Integration | [x] |
| 3.5 | It should sort by tip amount ascending/descending | Integration | [x] |
| 3.6 | It should sort by source ascending/descending | Integration | [x] |
| 3.7 | It should sort by processor ascending/descending | Integration | [ ] |
| 3.8 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [x] |
> **Note:** 3.7 is untestable because `:sales-order/processor` does not exist in the Datomic schema; processor info lives on `:charge/processor`.
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 4.1 | It should display 25 sales orders per page by default | Integration | [x] |
| 4.2 | It should allow changing the per-page count | Integration | [x] |
| 4.3 | It should calculate the total amount and tax across ALL matching sales orders, not just the current page | Unit | [x] |
---
## Expected Deposits
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 5.1 | It should display a table with columns: Client, Date, Sales Date, Total, Fee | UI | [ ] |
| 5.2 | It should show the Client column only when multiple clients are in scope | Integration | [x] |
| 5.3 | It should show a totals breakdown per expected deposit, aggregating charges by sales date with count and amount | UI | [ ] |
| 5.4 | It should show an external link icon row button when the expected deposit has a reference link | UI | [ ] |
| 5.5 | It should show a "Transaction" button linking to the associated transaction when one exists | UI | [ ] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 6.1 | It should filter expected deposits by date range | Integration | [x] |
| 6.2 | It should support exact match ID to jump to a specific record, showing a removable pill when active | Integration | [x] |
| 6.3 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [x] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 7.1 | It should sort by client name ascending/descending | Integration | [x] |
| 7.2 | It should sort by location ascending/descending | Integration | [x] |
| 7.3 | It should sort by date ascending/descending | Integration | [x] |
| 7.4 | It should sort by total amount ascending/descending | Integration | [x] |
| 7.5 | It should sort by fee amount ascending/descending | Integration | [x] |
| 7.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [x] |
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 8.1 | It should display 25 expected deposits per page by default | Integration | [x] |
| 8.2 | It should allow changing the per-page count | Integration | [x] |
---
## Tenders
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 9.1 | It should display a table with columns: Client, Date, Total, Processor, Tip, Links | UI | [ ] |
| 9.2 | It should show the Client column only when multiple clients are in scope | Integration | [x] |
| 9.3 | It should render the Processor column as a pill badge | UI | [ ] |
| 9.4 | It should show an external link icon row button when the tender has a reference link | UI | [ ] |
| 9.5 | It should show an "expected deposit" pill in the Links column when an associated expected deposit exists | UI | [ ] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 10.1 | It should filter tenders by date range | Integration | [x] |
| 10.2 | It should filter tenders by processor via radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor | Integration | [x] |
| 10.3 | It should filter tenders by total amount range (min / max) | Integration | [x] |
| 10.4 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [x] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 11.1 | It should sort by client name ascending/descending | Integration | [x] |
| 11.2 | It should sort by date ascending/descending | Integration | [x] |
| 11.3 | It should sort by total amount ascending/descending | Integration | [x] |
| 11.4 | It should sort by tip amount ascending/descending | Integration | [x] |
| 11.5 | It should sort by processor ascending/descending | Integration | [x] |
| 11.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [x] |
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 12.1 | It should display 25 tenders per page by default | Integration | [x] |
| 12.2 | It should allow changing the per-page count | Integration | [x] |
---
## Refunds
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 13.1 | It should display a table with columns: Client, Date, Total, Type, Fee | UI | [ ] |
| 13.2 | It should show the Client column only when multiple clients are in scope | Integration | [x] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 14.1 | It should filter refunds by date range | Integration | [x] |
| 14.2 | It should filter refunds by total amount range (min / max) | Integration | [x] |
| 14.3 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [x] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 15.1 | It should sort by client name ascending/descending | Integration | [x] |
| 15.2 | It should sort by date ascending/descending | Integration | [x] |
| 15.3 | It should sort by total amount ascending/descending | Integration | [x] |
| 15.4 | It should sort by fee amount ascending/descending | Integration | [ ] |
| 15.5 | It should sort by type ascending/descending | Integration | [x] |
| 15.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [x] |
> **Note:** 15.4 is blocked by a source bug: `refunds.clj` line 62 uses `?sort-tip` instead of `?sort-fee` for fee sorting, causing an unbound variable error.
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 16.1 | It should display 25 refunds per page by default | Integration | [x] |
| 16.2 | It should allow changing the per-page count | Integration | [x] |
---
## Cash Drawer Shifts
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 17.1 | It should display a table with columns: Client, Date, Paid in, Paid out, Expected cash, Opened cash | UI | [ ] |
| 17.2 | It should show the Client column only when multiple clients are in scope | Integration | [x] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 18.1 | It should filter cash drawer shifts by date range | Integration | [x] |
| 18.2 | It should filter cash drawer shifts by total amount range (min / max) | Integration | [ ] |
| 18.3 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [x] |
> **Note:** 18.2 is not implemented: the UI shows `total-field*` but `fetch-ids` in `cash_drawer_shifts.clj` does not handle `total-gte`/`total-lte`.
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 19.1 | It should sort by client name ascending/descending | Integration | [x] |
| 19.2 | It should sort by date ascending/descending | Integration | [x] |
| 19.3 | It should sort by paid-in amount ascending/descending | Integration | [x] |
| 19.4 | It should sort by paid-out amount ascending/descending | Integration | [x] |
| 19.5 | It should sort by expected-cash amount ascending/descending | Integration | [x] |
| 19.6 | It should sort by opened-cash amount ascending/descending | Integration | [x] |
| 19.7 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [x] |
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 20.1 | It should display 25 cash drawer shifts per page by default | Integration | [x] |
| 20.2 | It should allow changing the per-page count | Integration | [x] |
---
## Sales Summaries
### Display Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 21.1 | It should display a table with columns: Client, Date, Debits, Credits | UI | [ ] |
| 21.2 | It should show the Client column only when multiple clients are in scope | Integration | [x] |
| 21.3 | It should display debit-line items with category and amount | UI | [ ] |
| 21.4 | It should display credit-line items with category and amount | UI | [ ] |
| 21.5 | It should show a red "missing account" warning pill for unmapped items | UI | [ ] |
| 21.6 | It should show a green "Total" pill for balanced summaries (debits equal credits) | UI | [ ] |
| 21.7 | It should show a red "Total" pill for unbalanced summaries (debits do not equal credits) | UI | [ ] |
| 21.8 | It should show an edit (pencil) icon row button opening the edit wizard modal | UI | [ ] |
### Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 22.1 | It should filter sales summaries by date range | Integration | [x] |
| 22.2 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [x] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 23.1 | It should sort by client name ascending/descending | Integration | [x] |
| 23.2 | It should sort by date ascending/descending | Integration | [x] |
| 23.3 | It should sort by debits ascending/descending | Integration | [x] |
| 23.4 | It should sort by credits ascending/descending | Integration | [x] |
| 23.5 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [x] |
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 24.1 | It should display 25 sales summaries per page by default | Integration | [x] |
| 24.2 | It should allow changing the per-page count | Integration | [x] |
### Edit Wizard Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 25.1 | It should open a modal dialog when the edit button is clicked | UI | [ ] |
| 25.2 | It should display a data grid of summary items with columns: Category, Account, Debits, Credits | UI | [ ] |
| 25.3 | It should render auto items (non-manual) with read-only Category and amount, editable Account via typeahead | UI | [ ] |
| 25.4 | It should render manual items with editable Category (text input), Account (typeahead), Debit amount, and Credit amount | UI | [ ] |
| 25.5 | It should allow adding new manual items via a "New Summary Item" row | UI | [ ] |
| 25.6 | It should allow removing manual items via an X button | UI | [ ] |
| 25.7 | It should validate that an item cannot have both credit and debit amounts | Unit + Integration | [x] |
| 25.8 | It should display a total row with running totals for debits and credits | UI | [ ] |
| 25.9 | It should display an unbalanced row showing the difference when debits do not equal credits | UI | [ ] |
| 25.10 | It should search accounts scoped to the client with purpose "invoice" in the account typeahead | Integration | [x] |
| 25.11 | It should flash the row and close the modal after successful save | UI | [ ] |
---
## Cross-Cutting Behaviors
### HTMX Live Filtering Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 26.1 | It should trigger table refresh on filter form change with a 500ms debounce | Integration | SKIPPED |
| 26.2 | It should trigger table refresh on hot-filter keyup with a 1000ms debounce | Integration | SKIPPED |
| 26.3 | It should POST to the table route and swap the grid contents | Integration | SKIPPED |
| 26.4 | It should update the browser URL via hx-push-url when filters change | Integration | SKIPPED |
> **Note:** 26.126.4 test frontend JavaScript/HTMX behavior and cannot be validated server-side.
### Date Range Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 27.1 | It should support start-date and end-date query params on all pages | Integration | [x] |
| 27.2 | It should render the date range filter consistently across all pages | UI | [ ] |
| 27.3 | Given a date range with no start or end date, then the query should use scan functions with nil boundaries | Integration | [x] |
### Total Range Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 28.1 | It should support total-gte and total-lte query params on pages with amount filters | Integration | [x] |
| 28.2 | It should render money inputs for the total range filter | UI | [ ] |
### Exact Match ID Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 29.1 | It should support exact-match-id to jump to a specific record on applicable pages | Integration | [x] |
| 29.2 | It should show a removable pill when exact-match-id is active | UI | [ ] |
### Sorting Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 30.1 | It should toggle ascending/descending sort when a sortable column header is clicked | Integration | [x] |
| 30.2 | It should support multi-sort with active sorts appearing as removable pills above the grid | Integration | [x] |
| 30.3 | It should remove a sort when the X on its pill is clicked | Integration | [x] |
| 30.4 | It should default to sort by date descending for most pages | Integration | [x] |
### Pagination Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 31.1 | It should display first/previous/next/last pagination controls | UI | [ ] |
| 31.2 | It should display the total count above the grid | UI | [ ] |
### Client Scoping Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 32.1 | It should scope all queries to the user's accessible clients (trimmed to max 20) | Integration | [x] |
| 32.2 | It should hide the Client column when only one client is in scope | Integration | [x] |
| 32.3 | It should support client-id and client-code URL params | Integration | [x] |
### Permission Behaviors
| # | Behavior | Test Strategy | Status |
|---|---|---|---|
| 33.1 | It should require `(can? identity {:subject :sales :activity :read})` to access POS pages | Integration | SKIPPED |
| 33.2 | It should require admin access (`wrap-admin`) to access Sales Summaries | Integration | [x] |
| 33.3 | It should redirect unauthenticated users | Integration | SKIPPED |
> **Note:** 33.1 tests the GraphQL `can?` gate which is not directly reachable at the unit/integration level; 33.3 tests middleware redirect behavior that is covered elsewhere.
---
## Test Data Requirements
| Entity | Requirements |
|---|---|
| **Clients** | Multiple clients with `db/id`, `client/name`, `client/code`; some with locked-until dates |
| **Sales Orders** | With `:sales-order/date`, `:sales-order/total`, `:sales-order/tax`, `:sales-order/tip`, `:sales-order/source`, `:sales-order/charges` (with `charge/type-name`, `charge/processor`), `:sales-order/line-items` (with `order-line-item/category`) |
| **Expected Deposits** | With `:expected-deposit/date`, `:expected-deposit/total`, `:expected-deposit/fee`, `:expected-deposit/client`, optional `transaction/_expected-deposit` |
| **Tenders (Charges)** | With `:charge/date`, `:charge/total`, `:charge/tip`, `:charge/processor`, optional `expected-deposit/_charges` |
| **Refunds** | With `:sales-refund/date`, `:sales-refund/total`, `:sales-refund/fee`, `:sales-refund/type` |
| **Cash Drawer Shifts** | With `:cash-drawer-shift/date`, `:cash-drawer-shift/paid-in`, `:cash-drawer-shift/paid-out`, `:cash-drawer-shift/expected-cash`, `:cash-drawer-shift/opened-cash` |
| **Sales Summaries** | With `:sales-summary/date`, `:sales-summary/client`, `:sales-summary/items` (with `ledger-mapped/ledger-side`, `ledger-mapped/amount`, `sales-summary-item/category`, optional `ledger-mapped/account`) |
| **Accounts** | With purpose "invoice", scoped to clients, for Sales Summaries edit wizard |
## Existing Tests to Preserve
- `test/clj/auto_ap/ssr/pos/sales_orders_test.clj` — Sales orders grid behaviors (20 tests, 43 assertions)
- `test/clj/auto_ap/ssr/pos/expected_deposits_test.clj` — Expected deposits grid behaviors (9 tests, 23 assertions)
- `test/clj/auto_ap/ssr/pos/tenders_test.clj` — Tenders grid behaviors (8 tests, 20 assertions)
- `test/clj/auto_ap/ssr/pos/refunds_test.clj` — Refunds grid behaviors (7 tests, 17 assertions)
- `test/clj/auto_ap/ssr/pos/cash_drawer_shifts_test.clj` — Cash drawer shifts grid behaviors (7 tests, 19 assertions)
- `test/clj/auto_ap/ssr/admin/sales_summaries_test.clj` — Sales summaries admin behaviors (9 tests, 24 assertions)
## Dependencies
- Datomic (primary store)
- HTMX/Alpine.js (frontend interactivity)
- Grid system: `auto-ap.ssr.grid-page-helper` provides `build`, `page-route`, `table-route`, `row*`, `table*`
- Components: `auto-ap.ssr.components` for data grids, pills, buttons, inputs
- Querying: `auto-ap.datomic` for `query2`, `pull-many`, `apply-pagination`, `apply-sort-3`
- Ions: `iol-ion.query/scan-sales-orders`, `scan-expected-deposits`, `scan-charges`, `scan-sales-refunds`, `scan-cash-drawer-shifts`
- Permissions: `auto-ap.permissions/can?`
- Time: `auto-ap.time` for date formatting and localization
- Schema validation: Malli schemas enforce query params on every request
## Known Discrepancies
1. **`sales_orders.clj`** references `:sales-order/processor` in sort logic (behavior 3.7), but this attribute does not exist in the Datomic schema; processor info lives on `:charge/processor`.
2. **`refunds.clj` line 62** uses `?sort-tip` instead of `?sort-fee` for fee sorting, causing an unbound variable error (behavior 15.4).
3. **`cash_drawer_shifts.clj`** renders `total-field*` in the UI filters but does not implement `total-gte`/`total-lte` filtering in `fetch-ids` (behavior 18.2).