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:
- Fetch IDs via Datomic query with filters
- Hydrate results via
pull-many
- Render table with sortable columns
- Support selection (individual / all / all-filtered)
- 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:
- Each step is a GET that renders a form fragment
- Form submissions are POST/PUT with validation
- Navigation between steps updates the wizard state
- 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:
assert-can-see-client — user has access to the client
assert-not-locked — invoice date >= client locked-until
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 |
[ ] |
| 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 |
[ ] |
| 2.2 |
It should filter sales orders by total amount range (min / max) |
Integration |
[ ] |
| 2.3 |
It should filter sales orders by payment method via radio cards: All, Cash, Card, Gift Card, Other |
Integration |
[ ] |
| 2.4 |
It should filter sales orders by processor via radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor |
Integration |
[ ] |
| 2.5 |
It should filter sales orders by category text input matching order line item category |
Integration |
[ ] |
| 2.6 |
Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set |
Integration |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 3.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 3.2 |
It should sort by date ascending/descending |
Integration |
[ ] |
| 3.3 |
It should sort by total amount ascending/descending |
Integration |
[ ] |
| 3.4 |
It should sort by tax amount ascending/descending |
Integration |
[ ] |
| 3.5 |
It should sort by tip amount ascending/descending |
Integration |
[ ] |
| 3.6 |
It should sort by source ascending/descending |
Integration |
[ ] |
| 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 |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 4.1 |
It should display 25 sales orders per page by default |
Integration |
[ ] |
| 4.2 |
It should allow changing the per-page count |
Integration |
[ ] |
| 4.3 |
It should calculate the total amount and tax across ALL matching sales orders, not just the current page |
Unit |
[ ] |
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 |
[ ] |
| 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 |
[ ] |
| 6.2 |
It should support exact match ID to jump to a specific record, showing a removable pill when active |
Integration |
[ ] |
| 6.3 |
Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set |
Integration |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 7.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 7.2 |
It should sort by location ascending/descending |
Integration |
[ ] |
| 7.3 |
It should sort by date ascending/descending |
Integration |
[ ] |
| 7.4 |
It should sort by total amount ascending/descending |
Integration |
[ ] |
| 7.5 |
It should sort by fee amount ascending/descending |
Integration |
[ ] |
| 7.6 |
Given the user clicks a column header twice, then the sort direction should toggle |
Integration |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 8.1 |
It should display 25 expected deposits per page by default |
Integration |
[ ] |
| 8.2 |
It should allow changing the per-page count |
Integration |
[ ] |
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 |
[ ] |
| 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 |
[ ] |
| 10.2 |
It should filter tenders by processor via radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor |
Integration |
[ ] |
| 10.3 |
It should filter tenders by total amount range (min / max) |
Integration |
[ ] |
| 10.4 |
Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set |
Integration |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 11.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 11.2 |
It should sort by date ascending/descending |
Integration |
[ ] |
| 11.3 |
It should sort by total amount ascending/descending |
Integration |
[ ] |
| 11.4 |
It should sort by tip amount ascending/descending |
Integration |
[ ] |
| 11.5 |
It should sort by processor ascending/descending |
Integration |
[ ] |
| 11.6 |
Given the user clicks a column header twice, then the sort direction should toggle |
Integration |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 12.1 |
It should display 25 tenders per page by default |
Integration |
[ ] |
| 12.2 |
It should allow changing the per-page count |
Integration |
[ ] |
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 |
[ ] |
Filtering Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 14.1 |
It should filter refunds by date range |
Integration |
[ ] |
| 14.2 |
It should filter refunds by total amount range (min / max) |
Integration |
[ ] |
| 14.3 |
Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set |
Integration |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 15.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 15.2 |
It should sort by date ascending/descending |
Integration |
[ ] |
| 15.3 |
It should sort by total amount ascending/descending |
Integration |
[ ] |
| 15.4 |
It should sort by fee amount ascending/descending |
Integration |
[ ] |
| 15.5 |
It should sort by type ascending/descending |
Integration |
[ ] |
| 15.6 |
Given the user clicks a column header twice, then the sort direction should toggle |
Integration |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 16.1 |
It should display 25 refunds per page by default |
Integration |
[ ] |
| 16.2 |
It should allow changing the per-page count |
Integration |
[ ] |
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 |
[ ] |
Filtering Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 18.1 |
It should filter cash drawer shifts by date range |
Integration |
[ ] |
| 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 |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 19.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 19.2 |
It should sort by date ascending/descending |
Integration |
[ ] |
| 19.3 |
It should sort by paid-in amount ascending/descending |
Integration |
[ ] |
| 19.4 |
It should sort by paid-out amount ascending/descending |
Integration |
[ ] |
| 19.5 |
It should sort by expected-cash amount ascending/descending |
Integration |
[ ] |
| 19.6 |
It should sort by opened-cash amount ascending/descending |
Integration |
[ ] |
| 19.7 |
Given the user clicks a column header twice, then the sort direction should toggle |
Integration |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 20.1 |
It should display 25 cash drawer shifts per page by default |
Integration |
[ ] |
| 20.2 |
It should allow changing the per-page count |
Integration |
[ ] |
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 |
[ ] |
| 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 |
[ ] |
| 22.2 |
Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set |
Integration |
[ ] |
Sorting Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 23.1 |
It should sort by client name ascending/descending |
Integration |
[ ] |
| 23.2 |
It should sort by date ascending/descending |
Integration |
[ ] |
| 23.3 |
It should sort by debits ascending/descending |
Integration |
[ ] |
| 23.4 |
It should sort by credits ascending/descending |
Integration |
[ ] |
| 23.5 |
Given the user clicks a column header twice, then the sort direction should toggle |
Integration |
[ ] |
| # |
Behavior |
Test Strategy |
Status |
| 24.1 |
It should display 25 sales summaries per page by default |
Integration |
[ ] |
| 24.2 |
It should allow changing the per-page count |
Integration |
[ ] |
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 |
[ ] |
| 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 |
[ ] |
| 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 |
[ ] |
| 26.2 |
It should trigger table refresh on hot-filter keyup with a 1000ms debounce |
Integration |
[ ] |
| 26.3 |
It should POST to the table route and swap the grid contents |
Integration |
[ ] |
| 26.4 |
It should update the browser URL via hx-push-url when filters change |
Integration |
[ ] |
Date Range Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 27.1 |
It should support start-date and end-date query params on all pages |
Integration |
[ ] |
| 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 |
[ ] |
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 |
[ ] |
| 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 |
[ ] |
| 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 |
[ ] |
| 30.2 |
It should support multi-sort with active sorts appearing as removable pills above the grid |
Integration |
[ ] |
| 30.3 |
It should remove a sort when the X on its pill is clicked |
Integration |
[ ] |
| 30.4 |
It should default to sort by date descending for most pages |
Integration |
[ ] |
| # |
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 |
[ ] |
| 32.2 |
It should hide the Client column when only one client is in scope |
Integration |
[ ] |
| 32.3 |
It should support client-id and client-code URL params |
Integration |
[ ] |
Permission Behaviors
| # |
Behavior |
Test Strategy |
Status |
| 33.1 |
It should require (can? identity {:subject :sales :activity :read}) to access POS pages |
Integration |
[ ] |
| 33.2 |
It should require admin access (wrap-admin) to access Sales Summaries |
Integration |
[ ] |
| 33.3 |
It should redirect unauthenticated users |
Integration |
[ ] |
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
test/clj/auto_ap/ssr/pos/expected_deposits_test.clj — Expected deposits grid behaviors
test/clj/auto_ap/ssr/pos/tenders_test.clj — Tenders grid behaviors
test/clj/auto_ap/ssr/pos/refunds_test.clj — Refunds grid behaviors
test/clj/auto_ap/ssr/pos/cash_drawer_shifts_test.clj — Cash drawer shifts grid behaviors
test/clj/auto_ap/ssr/admin/sales_summaries_test.clj — Sales summaries admin behaviors
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